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 }