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