001    package hirondelle.web4j.model;
002    
003    import hirondelle.web4j.BuildImpl;
004    import hirondelle.web4j.readconfig.Config;
005    import hirondelle.web4j.request.DateConverter;
006    import hirondelle.web4j.request.Formats;
007    import hirondelle.web4j.security.SafeText;
008    import hirondelle.web4j.util.Util;
009    
010    import java.io.InputStream;
011    import java.math.BigDecimal;
012    import java.util.ArrayList;
013    import java.util.Arrays;
014    import java.util.Date;
015    import java.util.List;
016    import java.util.Locale;
017    import java.util.TimeZone;
018    import java.util.logging.Logger;
019    import java.util.regex.Pattern;
020    
021    /** Default implementation of {@link ConvertParam}.*/
022    public class ConvertParamImpl implements ConvertParam {
023      
024      /**
025       Return <tt>true</tt> only if <tt>aTargetClass</tt> is supported by this implementation. 
026       <P>
027       The following classes are supported by this implementation as building block classes: 
028       <ul>
029       <li><tt>{@link SafeText}</tt>
030       <li><tt>String</tt> (conditionally, see below)
031       <li><tt>Integer</tt>
032       <li><tt>Long</tt>
033       <li><tt>Boolean</tt>
034       <li><tt>BigDecimal</tt>
035       <li><tt>{@link Decimal}</tt>
036       <li><tt>{@link Id}</tt>
037       <li><tt>{@link DateTime}</tt>
038       <li><tt>java.util.Date</tt>
039       <li><tt>Locale</tt> 
040       <li><tt>TimeZone</tt> 
041       <li><tt>InputStream</tt>
042       </ul>
043       
044       <P><i>You are not obliged to use this class to model Locale and TimeZone. 
045       Many will choose to implement them as just another 
046       <a href='http://www.web4j.com/UserGuide.jsp#StartupTasksAndCodeTables'>code table</a>
047        instead.</i> In this case, your model object constructors would usually take an {@link Id} parameter for these 
048        items, and translate them into a {@link Code}. See the example apps for a demonstration of this technique. 
049       
050       <P><b>String is supported only when explicitly allowed.</b> 
051       The <tt>AllowStringAsBuildingBlock</tt> setting in <tt>web.xml</tt>
052       controls whether or not this class allows <tt>String</tt> as a supported class.
053       By default, its value is <tt>FALSE</tt>, since {@link SafeText} is the recommended 
054       replacement for <tt>String</tt>.  
055      */
056      public final boolean isSupported(Class<?> aTargetClass){
057        boolean result = false;
058        if (String.class.equals(aTargetClass)){
059          result = fConfig.getAllowStringAsBuildingBlock();
060        }
061        else {
062          for (Class standardClass : STANDARD_CLASSES){
063            if (standardClass.isAssignableFrom(aTargetClass)){
064              result = true;
065              break;
066            }
067          }
068        }
069        return result;
070      }
071    
072      /**
073       Coerce all parameters with no visible content to <tt>null</tt>.
074       
075       <P>In addition, any raw input value that matches <tt>IgnorableParamValue</tt> in <tt>web.xml</tt> is 
076       also coerced to <tt>null</tt>. See <tt>web.xml</tt> for more information.
077       
078       <P>Any non-<tt>null</tt> result is trimmed. 
079       This method can be overridden, if desired.
080      */
081      public String filter(String aRawInputValue){
082        String result = aRawInputValue;
083        if ( ! Util.textHasContent(aRawInputValue) || aRawInputValue.equals(getIgnorableParamValue()) ){
084          result = null;
085        }
086        return Util.trimPossiblyNull(result); //some apps may elect to trim elsewhere 
087      }
088    
089      /**
090       Apply reasonable parsing policies, suitable for most applications.
091       
092       <P>Roughly, the policies are: 
093       <ul>
094       <li><tt>SafeText</tt> uses {@link SafeText#SafeText(String)}
095       <li><tt>String</tt> just return the filtered value as is
096       <li><tt>Integer</tt> uses {@link Integer#Integer(String)}
097       <li><tt>BigDecimal</tt> uses {@link Formats#getDecimalInputFormat()}
098       <li><tt>Decimal</tt> uses {@link Formats#getDecimalInputFormat()}
099       <li><tt>Boolean</tt> uses {@link Util#parseBoolean(String)}
100       <li><tt>DateTime</tt> uses {@link DateConverter#parseEyeFriendlyDateTime(String, Locale)} 
101       and {@link DateConverter#parseHandFriendlyDateTime(String, Locale)}
102       <li><tt>Date</tt> uses {@link DateConverter#parseEyeFriendly(String, Locale, TimeZone)} 
103       and {@link DateConverter#parseHandFriendly(String, Locale, TimeZone)}
104       <li><tt>Long</tt> uses {@link Long#Long(String)}
105       <li><tt>Id</tt> uses {@link Id#Id(String)}
106       <li><tt>Locale</tt> uses {@link Locale#getAvailableLocales()} and {@link Locale#toString()}, case sensitive.
107       <li><tt>TimeZone</tt> uses {@link TimeZone#getAvailableIDs()}, case sensitive.
108      </ul>
109      <tt>InputStream</tt>s are not converted by this class, and need to be handled separately by the caller.
110      */
111      public final <T> T convert(String aFilteredInputValue, Class<T> aSupportedTargetClass, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
112        // Defensive : this check should have already been performed by the calling framework class.
113        if( ! isSupported(aSupportedTargetClass) ) {
114          throw new AssertionError("Unsupported type cannot be translated to an object: " + aSupportedTargetClass + ". If you're trying to use String, consider using SafeText instead. Otherwise, change the AllowStringAsBuildingBlock setting in web.xml.");
115        }
116        
117        Object result = null;
118        if (aSupportedTargetClass == SafeText.class){
119          //no translation needed; some impl's might trim here, or force CAPS
120          result = parseSafeText(aFilteredInputValue);
121        }
122        else if (aSupportedTargetClass == String.class) {
123          result = aFilteredInputValue; //no translation needed; some impl's might trim here, or force CAPS
124        }
125        else if (aSupportedTargetClass == Integer.class || aSupportedTargetClass == int.class){
126          result = parseInteger(aFilteredInputValue);
127        }
128        else if (aSupportedTargetClass == Boolean.class || aSupportedTargetClass == boolean.class){
129          result = Util.parseBoolean(aFilteredInputValue);
130        }
131        else if (aSupportedTargetClass == BigDecimal.class){
132          result = parseBigDecimal(aFilteredInputValue, aLocale, aTimeZone);
133        }
134        else if (aSupportedTargetClass == Decimal.class){
135          result = parseDecimal(aFilteredInputValue, aLocale, aTimeZone);
136        }
137        else if (aSupportedTargetClass == java.util.Date.class){
138          result = parseDate(aFilteredInputValue, aLocale, aTimeZone);
139        }
140        else if (aSupportedTargetClass == DateTime.class){
141          result = parseDateTime(aFilteredInputValue, aLocale);
142        }
143        else if (aSupportedTargetClass == Long.class || aSupportedTargetClass == long.class){
144          result = parseLong(aFilteredInputValue);
145        }
146        else if (aSupportedTargetClass == Id.class){
147          result = new Id(aFilteredInputValue.trim());
148        }
149        else if (aSupportedTargetClass == Locale.class){
150          result = parseLocale(aFilteredInputValue);
151        }
152        else if (TimeZone.class.isAssignableFrom(aSupportedTargetClass)){
153          //the above style is needed since TimeZone is abstract
154          result = parseTimeZone(aFilteredInputValue);
155        }
156        else {
157           throw new AssertionError("Failed to build object for ostensibly supported class: " + aSupportedTargetClass);
158        }
159        fLogger.finer("Converted request param into a " + aSupportedTargetClass.getName());
160        return (T)result; //this cast is unavoidable, and safe.
161      }
162      
163      /**
164       Return the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
165       See <tt>web.xml</tt> for more information.
166      */
167      public final String getIgnorableParamValue(){
168        return fConfig.getIgnorableParamValue();
169      }
170      
171      // PRIVATE 
172      
173      private Config fConfig = new Config();
174      private static List<Class<?>> STANDARD_CLASSES; //always the same 
175      private static final ModelCtorException PROBLEM_FOUND = new ModelCtorException();
176      private static final Logger fLogger = Util.getLogger(ConvertParamImpl.class);
177      static {  
178        STANDARD_CLASSES = new ArrayList<Class<?>>();
179        STANDARD_CLASSES.add(Integer.class);
180        STANDARD_CLASSES.add(int.class);
181        STANDARD_CLASSES.add(Boolean.class);
182        STANDARD_CLASSES.add(boolean.class);
183        STANDARD_CLASSES.add(BigDecimal.class);
184        STANDARD_CLASSES.add(java.util.Date.class);
185        STANDARD_CLASSES.add(Long.class);
186        STANDARD_CLASSES.add(long.class);
187        STANDARD_CLASSES.add(Id.class);
188        STANDARD_CLASSES.add(SafeText.class);
189        STANDARD_CLASSES.add(Locale.class);
190        STANDARD_CLASSES.add(TimeZone.class); 
191        STANDARD_CLASSES.add(Decimal.class);
192        STANDARD_CLASSES.add(DateTime.class);
193        STANDARD_CLASSES.add(InputStream.class);
194      }
195      
196      private Integer parseInteger(String aUserInputValue) throws ModelCtorException {
197        try {
198          return new Integer(aUserInputValue);
199        }
200        catch (NumberFormatException ex){
201          throw PROBLEM_FOUND;
202        }
203      }
204      
205      private BigDecimal parseBigDecimal(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
206        BigDecimal result = null;
207        Formats formats = new Formats(aLocale, aTimeZone);
208        Pattern pattern = formats.getDecimalInputFormat();
209        if ( Util.matches(pattern, aUserInputValue)) {
210          //BigDecimal ctor only takes '.' as decimal sign, never ','          
211          result = new BigDecimal(aUserInputValue.replace(',', '.'));
212        }
213        else {
214          throw PROBLEM_FOUND;
215        }
216        return result;
217      }
218      
219      private Decimal parseDecimal(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
220        Decimal result = null;
221        BigDecimal amount = null;
222        Formats formats = new Formats(aLocale, aTimeZone);
223        Pattern pattern = formats.getDecimalInputFormat();
224        if ( Util.matches(pattern, aUserInputValue)) {
225          //BigDecimal ctor only takes '.' as decimal sign, never ','          
226          amount = new BigDecimal(aUserInputValue.replace(',', '.'));
227          try {
228             result = new Decimal(amount);
229          }
230          catch(IllegalArgumentException ex){
231            throw PROBLEM_FOUND;
232          }
233        }
234        else {
235          throw PROBLEM_FOUND;
236        }
237        return result;
238      }
239      
240      private Date parseDate(String aUserInputValue, Locale aLocale, TimeZone aTimeZone) throws ModelCtorException {
241        Date result = null;
242        DateConverter dateConverter = BuildImpl.forDateConverter();
243        result = dateConverter.parseHandFriendly(aUserInputValue, aLocale, aTimeZone);
244        if ( result == null ){
245          result = dateConverter.parseEyeFriendly(aUserInputValue, aLocale, aTimeZone);
246        }
247        if ( result == null ) {
248          throw PROBLEM_FOUND;
249        }
250        return result;
251      }
252      
253      private DateTime parseDateTime(String aUserInputValue, Locale aLocale) throws ModelCtorException {
254        DateTime result = null;
255        DateConverter dateConverter = BuildImpl.forDateConverter();
256        result = dateConverter.parseHandFriendlyDateTime(aUserInputValue, aLocale);
257        if ( result == null ){
258          result = dateConverter.parseEyeFriendlyDateTime(aUserInputValue, aLocale);
259        }
260        if ( result == null ) {
261          throw PROBLEM_FOUND;
262        }
263        return result;
264      }
265      
266      private Long parseLong(String aUserInputValue) throws ModelCtorException {
267        Long result = null;
268        if ( Util.textHasContent(aUserInputValue) ){
269          try {
270            result = new Long(aUserInputValue);
271          }
272          catch (NumberFormatException ex){
273            throw PROBLEM_FOUND;
274          }
275        }
276        return result;
277      }
278      
279      private SafeText parseSafeText(String aUserInputValue) throws ModelCtorException {
280        SafeText result = null;
281        if( Util.textHasContent(aUserInputValue) ) {
282          try {
283            result = new SafeText(aUserInputValue);
284          }
285          catch(IllegalArgumentException ex){
286            throw PROBLEM_FOUND;
287          }
288        }
289        return result;
290      }
291    
292      /** Translate user input into a known time zone id. Case sensitive. */
293      private TimeZone parseTimeZone(String aUserInputValue) throws ModelCtorException {
294        TimeZone result = null;
295        if ( Util.textHasContent(aUserInputValue) ){
296          List<String> allTimeZoneIds = Arrays.asList(TimeZone.getAvailableIDs());
297          for(String id : allTimeZoneIds){
298            if (id.equals(aUserInputValue)){
299              result = TimeZone.getTimeZone(id);
300              break;
301            }
302          }
303          if(result == null){ //has content, but no match found
304            throw PROBLEM_FOUND;
305          }
306        }
307        return result;
308      }
309      
310      /** Translate user input into a known Locale id. Case sensitive. */
311      private Locale parseLocale(String aUserInputValue) throws ModelCtorException {
312        Locale result = null;
313        if ( Util.textHasContent(aUserInputValue) ){
314          List<Locale> allLocales = Arrays.asList(Locale.getAvailableLocales());
315          for(Locale locale: allLocales){
316            if (locale.toString().equals(aUserInputValue)){
317              result = locale;
318              break;
319            }
320          }
321          if(result == null){ //has content, but no match found
322            throw PROBLEM_FOUND;
323          }
324        }
325        return result;
326      }
327    }