View Javadoc

1   /*
2    * Copyright 2004 Jonathan M. Lehr
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
10   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
11   * governing permissions and limitations under the License.
12   * 
13   * MODIFIED BY THE KUALI FOUNDATION
14   */
15  // begin Kuali Foundation modification
16  package org.kuali.rice.kns.web.format;
17  // end Kuali Foundation modification
18  
19  import java.io.Serializable;
20  import java.lang.reflect.Array;
21  import java.math.BigDecimal;
22  import java.sql.Date;
23  import java.sql.Timestamp;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  
34  import javax.servlet.http.HttpServletRequest;
35  
36  import org.apache.commons.beanutils.PropertyUtils;
37  import org.apache.commons.lang.StringUtils;
38  import org.kuali.rice.kns.util.AbstractKualiDecimal;
39  import org.kuali.rice.kns.util.KualiDecimal;
40  import org.kuali.rice.kns.util.KualiInteger;
41  import org.kuali.rice.kns.util.KualiPercent;
42  import org.kuali.rice.kns.web.struts.pojo.ArrayUtils;
43  
44  
45  // begin Kuali Foundation modification
46  /**
47   * This is the base class for all other Formatters.
48   */
49  /**
50   * It provides default formatting and conversion behavior for most value types, including primitives, arrays, and instances of most
51   * {@link Collection}types. <code>Formatter</code> and its subclasses were designed primarily to be used by web app framework
52   * components, though they can also be used in other contexts.
53   * <p>
54   * During request processing, the {@link PojoActionForm}uses <code>Formatter</code> instances to convert inbound request values
55   * to JavaBean property types. Whenever a given value cannot be converted to its target type, the conversion method
56   * {@link PropertyUtils#getProperty(Object, String)}throws a {@link FormatException}to signal this condition to the
57   * calling code.
58   * <p>
59   * During the response phase, Struts tags make calls to the {@link PojoRequestProcessor}in order to access bean property values.
60   * The <code>PojoRequestProcessor</code> then uses <code>Formatter</code> instances to format the bean values for presentation
61   * in the user interface.
62   * <p>
63   * In either case, <code>Formatter</code> instances are obtained by calling {@link #getFormatter(Class)}, which looks in an
64   * internal registry to determine which <code>Formatter</code> class to instantiate, and returns a new instance. The StrutsLive
65   * framework includes a number of <code>Formatter</code> classes that are registered statically; additional
66   * <code>Formatter classes can be registered at compile
67   * time or at run time. 
68   * <p>
69   * Subclasses of <code>Formatter</code> typically override the callback methods
70   * {@link #convertToObject(String)} and {@link #formatObject(Object)}, which
71   * otherwise provide default conversion and formmating behavior needed for
72   * atomic values (i.e., an ordinary bean property such as a <code>String</code>
73   * or <code>Integer</code>, or else an element of a property typed as
74   * array or Collection).
75   * 
76   * @see PojoActionForm#populate(HttpServletRequest)
77   * @see PojoPropertyUtilsBean#getProperty(Object, String)
78   */
79  // end Kuali Foundation modification
80  public class Formatter implements Serializable {
81  	// begin Kuali Foundation modification
82  	// removed serialVersionUID and logger members
83  	// end Kuali Foundation modification
84  	
85      static final String CREATE_MSG = "Couldn't create an instance of class ";
86      // begin Kuali Foundation modification
87      // registry changed from AppLocal instance to a Map
88      private static Map registry = Collections.synchronizedMap(new HashMap());
89      // end Kuali Foundation modification
90      
91      protected Map settings;
92      
93      // begin Kuali Foundation modification
94      // removed keypath and rootObject variables
95      // end Kuali Foundation modification
96      
97      protected Class propertyType;
98  
99      static { 
100     	// begin Kuali Foundation modification
101         registerFormatter(String.class, Formatter.class);
102         registerFormatter(String[].class, Formatter.class);
103         registerFormatter(AbstractKualiDecimal.class, BigDecimalFormatter.class);
104         registerFormatter(KualiDecimal.class, CurrencyFormatter.class); 
105         registerFormatter(KualiInteger.class, KualiIntegerCurrencyFormatter.class);
106         registerFormatter(KualiPercent.class, PercentageFormatter.class);
107         registerFormatter(BigDecimal.class, BigDecimalFormatter.class);
108         registerFormatter(Date.class, DateFormatter.class);
109         registerFormatter(Integer.class, IntegerFormatter.class);
110         registerFormatter(int.class, IntegerFormatter.class);
111         registerFormatter(int[].class, IntegerFormatter.class);
112         registerFormatter(Boolean.class, BooleanFormatter.class);
113         registerFormatter(Boolean.TYPE, BooleanFormatter.class);
114         registerFormatter(boolean[].class, BooleanFormatter.class);
115         registerFormatter(Long.class, LongFormatter.class);
116         registerFormatter(Timestamp.class, DateViewTimestampObjectFormatter.class);
117         registerFormatter(boolean.class, LittleBooleanFormatter.class);
118         registerFormatter(Collection.class, ArrayFormatter.class);
119         // end Kuali Foundation modification
120     }
121 
122     public static Formatter getFormatter(Class aType) {
123         return getFormatter(aType, null);
124     }
125 
126     // begin Kuali Foundation modification
127     // param aType was valueType, comment changes, major code changes
128     /**
129      * Returns an instance of the Formatter class to be used to format the provided value type.
130      * 
131      * @param type the class of the value to be formatted
132      * @param settings parameters used by subclasses to customize behavior
133      * @return an instance of Formatter or one of its subclasses
134      */
135     public static Formatter getFormatter(Class aType, Map settings) {
136     	// original code: return createFormatter(formatterForType(valueType), valueType, settings);
137 			
138         Class type = formatterForType(aType);
139         Formatter formatter = null;
140         try {
141             formatter = (Formatter) type.newInstance();
142         }
143         catch (InstantiationException e) {
144             throw new FormatException(CREATE_MSG + type, e);
145         }
146         catch (IllegalAccessException e) {
147             throw new FormatException(CREATE_MSG + type, e);
148         }
149 
150         if (settings != null)
151             formatter.setSettings(Collections.unmodifiableMap(settings));
152         formatter.propertyType = aType;
153 
154         return formatter;
155     }
156 
157     // removed getFormatterByName, formatterClassForName, createFormatter methods
158     // end Kuali Foundation modification
159 
160     /**
161      * Binds the provided value type to a Formatter type. Note that a single Formatter class can be associated with more than one
162      * type.
163      * 
164      * @param type a value type
165      * @param formatterType a Formatter type
166      */
167     public static void registerFormatter(Class type, Class formatterType) {
168         registry.put(type, formatterType);
169     }
170 
171     /**
172      * Returns <code>true</code> if the provided class is an array type, implements either the {@link List}or {@link Set}
173      * interfaces, or is one of the Formatter classes currently registered.
174      * 
175      * @see registerFormatter(Class, Class)
176      */
177     public static boolean isSupportedType(Class type) {
178         // begin Kuali Foundation modification
179         if (type == null)
180             return false;
181         // end Kuali Foundation modification
182         if (List.class.isAssignableFrom(type))
183             return true;
184         if (Set.class.isAssignableFrom(type))
185             return true;
186 
187         return findFormatter(type) != null;
188     }
189 
190     /**
191      * Return the Formatter associated with the given type, by consulting an internal registry. Additional associations can be made
192      * by calling {@link registerFormatter(Class, Class)}.
193      * 
194      * @return a new Formatter instance
195      */
196     public static Class formatterForType(Class type) {
197         if (type == null)
198             throw new IllegalArgumentException("Type can not be null");
199 
200         Class formatterType = findFormatter(type);
201 
202         return formatterType == null ? Formatter.class : formatterType;
203     }
204 
205 	// Kuali Foundation modification: comment removed
206     public static Class findFormatter(Class type) {
207     	// begin Kuali Foundation modification
208         if (type == null)
209             return null;
210 
211         if (registry.containsKey(type)) {
212             return (Class) registry.get(type);
213         }
214 
215 
216         Iterator typeIter = registry.keySet().iterator();
217         while (typeIter.hasNext()) {
218             Class currType = (Class) typeIter.next();
219             if (currType.isAssignableFrom(type)) {
220                 Class currFormatter = (Class) registry.get(currType);
221                 registerFormatter(type, currFormatter);
222                 return currFormatter;
223             }
224         }
225 
226         return null;
227         // end Kuali Foundation modification
228     }
229 
230 	// begin Kuali Foundation modification
231     public String getImplementationClass() {
232         return this.getClass().getName();
233     }
234     // end Kuali Foundation modification
235 
236     public Class getPropertyType() {
237         return propertyType;
238     }
239 
240     public void setPropertyType(Class propertyType) {
241         this.propertyType = propertyType;
242     }
243 
244     public Map getSettings() {
245         return settings;
246     }
247 
248     public void setSettings(Map settings) {
249         this.settings = settings;
250     }
251 
252 	// begin Kuali Foundation modification
253 	// removed getKeypath, setKeyPath, getRootObject, setRootObject, hasSettingForKey, settingForKey, typeForKey, getErrorKey
254 	// end Kuali Foundation modification
255 	
256     /**
257      * begin Kuali Foundation modification
258      * Returns a String representation of the given value. May be overridden by subclasses to provide customized behavior for
259      * different types, though generally the callback method {@link #format(Object)}provides a better customization hook.
260      * <p>
261      * Provides default handling for properties typed as array or Collection. Subclass implementations of this method must invoke
262      * <code>super.formatForPresentation()</code> to take advantage of this built-in behavior.
263      * <p>
264      * Delegates to callback method {@link formatObject}for all other types. This method in turn invokes the callback method
265      * <code>format</code>, which serves as an extension point for subclasses; the default implementation simply returns its
266      * argument. Overriding <code>format</code> allows subclasses to take advantage of all of the array, primitive type, and
267      * Collection handling functionality provided by the base class.
268      * 
269      * @param value the object to be formatted
270      * @return a formatted string representation of the given object
271      * @see #formatObject(Object)
272      * end Kuali Foundation modification
273      */
274     public Object formatForPresentation(Object value) {
275         if (isNullValue(value))
276             return formatNull();
277 
278 		// begin Kuali Foundation modification
279 		// removed code
280 		/*
281 	    // TODO: add registry for non-navigable classes so there's a way to
282         // disable formatting selectively for given types contained in arrays
283         // or Collections.
284         if (Collection.class.isAssignableFrom(value.getClass()))
285             return formatCollection((Collection) value);
286         
287         if (propertyType != null && propertyType.isArray())
288             return formatArray(value);
289 		*/
290 		// end Kuali Foundation modification
291 		
292         return formatObject(value);
293     }
294 
295     /**
296      * May be overridden by subclasses to provide special handling for <code>null</code> values when formatting a bean property
297      * value for presentation. The default implementation simply returns <code>null</code>
298      */
299     protected Object formatNull() {
300         return null;
301     }
302 
303     /**
304      * May be overridden by subclasses to provide custom formatting behavior. Provides default formatting implementation for
305      * primitive types. (Note that primitive types are will always be wrapped in an array in order to be passed as an argument of
306      * type <code>Object</code>).
307      */
308     public Object formatObject(Object value) {
309         if (value == null)
310             return formatNull();
311 
312         // Collections and arrays have already been handled at this point, so
313         // if value is an array, assume it's a wrapper for a primitive type.
314         Class type = value.getClass();
315         if (type.isArray())
316         	// begin Kuali Foundation modification
317             return ArrayUtils.toString(value, type.getComponentType());
318             // end begin Kuali Foundation modification
319 
320         if (!(isSupportedType(value.getClass())))
321             // begin Kuali Foundation modification
322             formatBean(value);
323             // end Kuali Foundation modification
324 
325         return format(value);
326     }
327 
328     /**
329      * If an element of the Collection isn't a supported type, assume it's a JavaBean, and format each of its properties. Returns a
330      * Map containing the formatted properties keyed by property name.
331      */
332     protected Object formatBean(Object bean) {
333         Map properties = null;
334         try {
335         	// begin Kuali Foundation modification
336             properties = PropertyUtils.describe(bean);
337             // end Kuali Foundation modification
338         }
339         catch (Exception e) {
340             throw new FormatException("Unable to format values for bean " + bean, e);
341         }
342 
343         Map formattedVals = new HashMap();
344         // begin Kuali Foundation modification
345         Iterator propIter = properties.entrySet().iterator();
346 
347         while (propIter.hasNext()) {
348             Map.Entry entry = (Map.Entry) propIter.next();
349             Object value = entry.getValue();
350             if (value != null && isSupportedType(value.getClass())) {
351                 Formatter formatter = getFormatter(value.getClass());
352                 formattedVals.put(entry.getKey(), formatter.formatForPresentation(value));
353             }
354         }
355         // end Kuali Foundation modification
356         return formattedVals;
357     }
358 
359     public Object format(Object value) {
360         return value;
361     }
362 
363     public Object formatArray(Object value) {
364     	// begin Kuali Foundation modification
365         Class elementType = value.getClass().getComponentType();
366         if (!isSupportedType(elementType))
367             return value;
368 
369         int length = Array.getLength(value);
370         Object[] formattedVals = new String[length];
371 
372         for (int i = 0; i < length; i++) {
373             Object element = Array.get(value, i);
374             Object objValue = ArrayUtils.toObject(element);
375             Formatter elementFormatter = getFormatter(elementType);
376             formattedVals[i] = elementFormatter.formatForPresentation(objValue);
377         }
378 
379         return formattedVals;
380         // end Kuali Foundation modification
381     }
382 
383     public Object formatCollection(Collection value) {
384         List stringVals = new ArrayList();
385         Iterator iter = value.iterator();
386         while (iter.hasNext()) {
387             Object obj = iter.next();
388             Formatter formatter = getFormatter(obj.getClass());
389             // begin Kuali Foundation modification
390             stringVals.add(formatter.formatForPresentation(obj));
391             // end Kuali Foundation modification
392         }
393         return stringVals.toArray();
394     }
395 
396     /**
397      * Returns an object representation of the provided string after first removing any extraneous formatting characters. If the
398      * argument is a native array wrapping the actual value, the value is removed (unwrapped) from the array prior to invoking the
399      * callback method {@link #convertToObject(String)}, which performs the actual conversion.
400      * <p>
401      * If the provided object is <code>null</code>, a blank <code>String</code>, or a <code>String[]</code> of length <b>0
402      * </b> or that has <code>null</code> or a blank <code>String</code> in the first position, returns <code>null</code>.
403      * Otherwise, If the destination property is a <code>Collection</code>, returns an instance of that type containing the
404      * string values of the array elements.
405      * <p>
406      * If the provided object is an array, uses a Formatter corresponding to the array's component type to convert each of its
407      * elements, and returns a new array containing the converted values.
408      * 
409      * May be overidden by subclasses to customize conversion, though ordinarily {@link #convertToObject(String)}is a better choice
410      * since it takes advantage of <code>convertFromPresentationFormat</code>'s built-in behavior.
411      * 
412      * @param value the string value to be converted
413      * @return the object value corresponding to the provided string value
414      * @see convertToObject(String)
415      */
416     public Object convertFromPresentationFormat(Object value) {
417         if (isEmptyValue(value))
418             return getNullObjectValue();
419 
420         Class type = value.getClass();
421         boolean isArray = propertyType != null && propertyType.isArray();
422         boolean isCollection = propertyType != null && Collection.class.isAssignableFrom(propertyType);
423 
424         if (!(isArray || isCollection)) {
425             value = unwrapString(value);
426             return convertToObject((String) value);
427         }
428 
429         String[] strings = type.isArray() ? (String[]) value : new String[] { (String) value };
430 
431         return isArray ? convertToArray(strings) : convertToCollection(strings);
432     }
433 
434     /**
435      * May be overridden by subclasses to provide special handling for <code>null</code> values when converting from presentation
436      * format to a bean property type. The default implementation simply returns <code>null</code>
437      */
438     protected Object getNullObjectValue() {
439         return null;
440     }
441 
442     /**
443      * May be orverridden by subclasses to customize its behavior. The default implementation simply trims and returns the provided
444      * string.
445      */
446     protected Object convertToObject(String string) {
447         return string == null ? null : string.replace( "\r\n", "\n" ).trim();        
448     }
449 
450     /**
451      * Converts an array of strings to a Collection type corresponding to the value of <code>propertyType</code>. Since we don't
452      * have type information for the elements of the collection, no attempt is made to convert the elements from <code>String</code>
453      * to other types. However, subclasses can override this method if they need to provide the ability to convert the elements to a
454      * given type.
455      */
456     protected Collection convertToCollection(String[] strings) {
457         Collection collection = null;
458         Class type = propertyType;
459 
460         if (propertyType.isAssignableFrom(List.class))
461             type = ArrayList.class;
462         else if (propertyType.isAssignableFrom(Set.class))
463             type = HashSet.class;
464 
465         try {
466             collection = (Collection) type.newInstance();
467         }
468         catch (Exception e) {
469             throw new FormatException(CREATE_MSG + propertyType, e);
470         }
471 
472         for (int i = 0; i < strings.length; i++)
473             collection.add(strings[i]);
474 
475         return collection;
476     }
477 
478     /**
479      * Converts an array of strings to an array of objects by calling {@link #convertToObject(String)}on each element of the
480      * provided array in turn, using instances of a Formatter class that corresponds to this Formatter's property type.
481      * 
482      * @see #propertyType
483      */
484     protected Object convertToArray(String[] strings) {
485         Class type = propertyType.getComponentType();
486         // begin Kuali Foundation modification
487         Formatter formatter = getFormatter(type);
488         // end Kuali Foundation modification
489         Object array = null;
490         try {
491             array = Array.newInstance(type, strings.length);
492         }
493         catch (Exception e) {
494             throw new FormatException(CREATE_MSG + type, e);
495         }
496 
497         for (int i = 0; i < strings.length; i++) {
498             Object value = formatter.convertToObject(strings[i]);
499             // begin Kuali Foundation modification
500             ArrayUtils.setArrayValue(array, type, value, i);
501             // end Kuali Foundation modification
502         }
503 
504         return array;
505     }
506 
507     public static String unwrapString(Object target) {
508 
509         if (target.getClass().isArray()) {
510             String wrapper[] = (String[]) target;
511             return wrapper.length > 0 ? wrapper[0] : null;
512         }
513 		// begin Kuali Foundation modification
514         // if target object is null, return a null String
515         else if (target == null) {
516             return new String();
517         }
518 
519         // otherwise, return the string value of the object, with the hope
520         // that the toString() has been meaningfully overriden
521         else {
522             return target.toString();
523         }
524         // end Kuali Foundation modification
525     }
526 
527     public static boolean isNullValue(Object obj) {
528         if (obj == null)
529             return true;
530 
531 		// begin Kuali Foundation modification
532         if ((obj instanceof String) && StringUtils.isEmpty((String) obj))
533             return true;
534         // end Kuali Foundation modification
535 
536         return false;
537     }
538 
539     public static boolean isEmptyValue(Object obj) {
540         if (obj == null)
541             return true;
542         // begin Kuali Foundation modification
543         if ((obj instanceof String) && StringUtils.isEmpty((String) obj))
544             return true;
545         // end Kuali Foundation modification
546         Class type = obj.getClass();
547         if (type.isArray()) {
548             Class compType = type.getComponentType();
549             if (compType.isPrimitive())
550                 return false;
551             if (((Object[]) obj).length == 0)
552                 return true;
553             if (((Object[]) obj)[0] == null)
554                 return true;
555             if (String.class.isAssignableFrom(compType)) {
556             	// begin Kuali Foundation modification
557                 return StringUtils.isEmpty(((String[]) obj)[0]);
558                 // end Kuali Foundation modification
559             }
560         }
561         return false;
562     }
563 
564     protected String trimString(Object target) {
565         String stringValue = null;
566         try {
567             stringValue = (String) target;
568         }
569         catch (ClassCastException e) {
570             throw new FormatException("Can't cast " + target + " to String", e);
571         }
572         return stringValue == null ? null : stringValue.trim();
573     }
574 
575     /**
576      * @deprecated in favor of {@link StringUtils#isEmptyString(String)}
577      */
578     protected boolean isBlank(String string) {
579         return string == null || string.trim().length() == 0;
580     }
581 }