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 }