001: /*
002: * Copyright 2004 Jonathan M. Lehr
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
010: * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
011: * governing permissions and limitations under the License.
012: *
013: * MODIFIED BY THE KUALI FOUNDATION
014: */
015: // begin Kuali Foundation modification
016: package org.kuali.core.web.format;
017:
018: // end Kuali Foundation modification
019:
020: import java.io.Serializable;
021: import java.lang.reflect.Array;
022: import java.math.BigDecimal;
023: import java.sql.Date;
024: import java.sql.Timestamp;
025: import java.util.ArrayList;
026: import java.util.Collection;
027: import java.util.Collections;
028: import java.util.HashMap;
029: import java.util.HashSet;
030: import java.util.Iterator;
031: import java.util.List;
032: import java.util.Map;
033: import java.util.Set;
034:
035: import javax.servlet.http.HttpServletRequest;
036:
037: import org.apache.commons.beanutils.PropertyUtils;
038: import org.apache.commons.lang.StringUtils;
039: import org.kuali.core.util.KualiDecimal;
040: import org.kuali.core.util.KualiInteger;
041: import org.kuali.core.util.KualiPercent;
042: import org.kuali.core.web.struts.pojo.ArrayUtils;
043: import org.kuali.core.web.struts.pojo.PojoPropertyUtilsBean;
044:
045: // begin Kuali Foundation modification
046: /**
047: * This is the base class for all other Formatters.
048: */
049: /**
050: * It provides default formatting and conversion behavior for most value types, including primitives, arrays, and instances of most
051: * {@link Collection}types. <code>Formatter</code> and its subclasses were designed primarily to be used by web app framework
052: * components, though they can also be used in other contexts.
053: * <p>
054: * During request processing, the {@link PojoActionForm}uses <code>Formatter</code> instances to convert inbound request values
055: * to JavaBean property types. Whenever a given value cannot be converted to its target type, the conversion method
056: * {@link PojoPropertyUtilsBean#getProperty(Object, String)}throws a {@link FormatException}to signal this condition to the
057: * calling code.
058: * <p>
059: * During the response phase, Struts tags make calls to the {@link PojoRequestProcessor}in order to access bean property values.
060: * The <code>PojoRequestProcessor</code> then uses <code>Formatter</code> instances to format the bean values for presentation
061: * in the user interface.
062: * <p>
063: * In either case, <code>Formatter</code> instances are obtained by calling {@link #getFormatter(Class)}, which looks in an
064: * internal registry to determine which <code>Formatter</code> class to instantiate, and returns a new instance. The StrutsLive
065: * framework includes a number of <code>Formatter</code> classes that are registered statically; additional
066: * <code>Formatter classes can be registered at compile
067: * time or at run time.
068: * <p>
069: * Subclasses of <code>Formatter</code> typically override the callback methods
070: * {@link #convertToObject(String)} and {@link #formatObject(Object)}, which
071: * otherwise provide default conversion and formmating behavior needed for
072: * atomic values (i.e., an ordinary bean property such as a <code>String</code>
073: * or <code>Integer</code>, or else an element of a property typed as
074: * array or Collection).
075: *
076: * @see PojoActionForm#populate(HttpServletRequest)
077: * @see PojoPropertyUtilsBean#getProperty(Object, String)
078: */
079: // end Kuali Foundation modification
080: public class Formatter implements Serializable {
081: // begin Kuali Foundation modification
082: // removed serialVersionUID and logger members
083: // end Kuali Foundation modification
084:
085: static final String CREATE_MSG = "Couldn't create an instance of class ";
086: // begin Kuali Foundation modification
087: // registry changed from AppLocal instance to a Map
088: private static Map registry = Collections
089: .synchronizedMap(new HashMap());
090: // end Kuali Foundation modification
091:
092: protected Map settings;
093:
094: // begin Kuali Foundation modification
095: // removed keypath and rootObject variables
096: // end Kuali Foundation modification
097:
098: protected Class propertyType;
099:
100: static {
101: // begin Kuali Foundation modification
102: registerFormatter(String.class, Formatter.class);
103: registerFormatter(String[].class, Formatter.class);
104: registerFormatter(KualiDecimal.class, CurrencyFormatter.class);
105: registerFormatter(KualiInteger.class,
106: KualiIntegerCurrencyFormatter.class);
107: registerFormatter(KualiPercent.class, PercentageFormatter.class);
108: registerFormatter(BigDecimal.class, BigDecimalFormatter.class);
109: registerFormatter(Date.class, DateFormatter.class);
110: registerFormatter(Integer.class, IntegerFormatter.class);
111: registerFormatter(int.class, IntegerFormatter.class);
112: registerFormatter(int[].class, IntegerFormatter.class);
113: registerFormatter(Boolean.class, BooleanFormatter.class);
114: registerFormatter(Boolean.TYPE, BooleanFormatter.class);
115: registerFormatter(boolean[].class, BooleanFormatter.class);
116: registerFormatter(Long.class, LongFormatter.class);
117: registerFormatter(Timestamp.class,
118: DateViewTimestampObjectFormatter.class);
119: registerFormatter(boolean.class, LittleBooleanFormatter.class);
120: // end Kuali Foundation modification
121: }
122:
123: public static Formatter getFormatter(Class aType) {
124: return getFormatter(aType, null);
125: }
126:
127: // begin Kuali Foundation modification
128: // param aType was valueType, comment changes, major code changes
129: /**
130: * Returns an instance of the Formatter class to be used to format the provided value type.
131: *
132: * @param type the class of the value to be formatted
133: * @param settings parameters used by subclasses to customize behavior
134: * @return an instance of Formatter or one of its subclasses
135: */
136: public static Formatter getFormatter(Class aType, Map settings) {
137: // original code: return createFormatter(formatterForType(valueType), valueType, settings);
138:
139: Class type = formatterForType(aType);
140: Formatter formatter = null;
141: try {
142: formatter = (Formatter) type.newInstance();
143: } catch (InstantiationException e) {
144: throw new FormatException(CREATE_MSG + type, e);
145: } catch (IllegalAccessException e) {
146: throw new FormatException(CREATE_MSG + type, e);
147: }
148:
149: if (settings != null)
150: formatter
151: .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: Iterator typeIter = registry.keySet().iterator();
216: while (typeIter.hasNext()) {
217: Class currType = (Class) typeIter.next();
218: if (currType.isAssignableFrom(type)) {
219: return (Class) registry.get(currType);
220: }
221: }
222:
223: return null;
224: // end Kuali Foundation modification
225: }
226:
227: // begin Kuali Foundation modification
228: public String getImplementationClass() {
229: return this .getClass().getName();
230: }
231:
232: // end Kuali Foundation modification
233:
234: public Class getPropertyType() {
235: return propertyType;
236: }
237:
238: public void setPropertyType(Class propertyType) {
239: this .propertyType = propertyType;
240: }
241:
242: public Map getSettings() {
243: return settings;
244: }
245:
246: public void setSettings(Map settings) {
247: this .settings = settings;
248: }
249:
250: // begin Kuali Foundation modification
251: // removed getKeypath, setKeyPath, getRootObject, setRootObject, hasSettingForKey, settingForKey, typeForKey, getErrorKey
252: // end Kuali Foundation modification
253:
254: /**
255: * begin Kuali Foundation modification
256: * Returns a String representation of the given value. May be overridden by subclasses to provide customized behavior for
257: * different types, though generally the callback method {@link #format(Object)}provides a better customization hook.
258: * <p>
259: * Provides default handling for properties typed as array or Collection. Subclass implementations of this method must invoke
260: * <code>super.formatForPresentation()</code> to take advantage of this built-in behavior.
261: * <p>
262: * Delegates to callback method {@link formatObject}for all other types. This method in turn invokes the callback method
263: * <code>format</code>, which serves as an extension point for subclasses; the default implementation simply returns its
264: * argument. Overriding <code>format</code> allows subclasses to take advantage of all of the array, primitive type, and
265: * Collection handling functionality provided by the base class.
266: *
267: * @param value the object to be formatted
268: * @return a formatted string representation of the given object
269: * @see #formatObject(Object)
270: * end Kuali Foundation modification
271: */
272: public Object formatForPresentation(Object value) {
273: if (isNullValue(value))
274: return formatNull();
275:
276: // begin Kuali Foundation modification
277: // removed code
278: /*
279: // TODO: add registry for non-navigable classes so there's a way to
280: // disable formatting selectively for given types contained in arrays
281: // or Collections.
282: if (Collection.class.isAssignableFrom(value.getClass()))
283: return formatCollection((Collection) value);
284:
285: if (propertyType != null && propertyType.isArray())
286: return formatArray(value);
287: */
288: // end Kuali Foundation modification
289: return formatObject(value);
290: }
291:
292: /**
293: * May be overridden by subclasses to provide special handling for <code>null</code> values when formatting a bean property
294: * value for presentation. The default implementation simply returns <code>null</code>
295: */
296: protected Object formatNull() {
297: return null;
298: }
299:
300: /**
301: * May be overridden by subclasses to provide custom formatting behavior. Provides default formatting implementation for
302: * primitive types. (Note that primitive types are will always be wrapped in an array in order to be passed as an argument of
303: * type <code>Object</code>).
304: */
305: public Object formatObject(Object value) {
306: if (value == null)
307: return formatNull();
308:
309: // Collections and arrays have already been handled at this point, so
310: // if value is an array, assume it's a wrapper for a primitive type.
311: Class type = value.getClass();
312: if (type.isArray())
313: // begin Kuali Foundation modification
314: return ArrayUtils.toString(value, type.getComponentType());
315: // end begin Kuali Foundation modification
316:
317: if (!(isSupportedType(value.getClass())))
318: // begin Kuali Foundation modification
319: formatBean(value);
320: // end Kuali Foundation modification
321:
322: return format(value);
323: }
324:
325: /**
326: * If an element of the Collection isn't a supported type, assume it's a JavaBean, and format each of its properties. Returns a
327: * Map containing the formatted properties keyed by property name.
328: */
329: protected Object formatBean(Object bean) {
330: Map properties = null;
331: try {
332: // begin Kuali Foundation modification
333: properties = PropertyUtils.describe(bean);
334: // end Kuali Foundation modification
335: } catch (Exception e) {
336: throw new FormatException(
337: "Unable to format values for bean " + bean, e);
338: }
339:
340: Map formattedVals = new HashMap();
341: // begin Kuali Foundation modification
342: Iterator propIter = properties.entrySet().iterator();
343:
344: while (propIter.hasNext()) {
345: Map.Entry entry = (Map.Entry) propIter.next();
346: Object value = entry.getValue();
347: if (value != null && isSupportedType(value.getClass())) {
348: Formatter formatter = getFormatter(value.getClass());
349: formattedVals.put(entry.getKey(), formatter
350: .formatForPresentation(value));
351: }
352: }
353: // end Kuali Foundation modification
354: return formattedVals;
355: }
356:
357: public Object format(Object value) {
358: return value;
359: }
360:
361: public Object formatArray(Object value) {
362: // begin Kuali Foundation modification
363: Class elementType = value.getClass().getComponentType();
364: if (!isSupportedType(elementType))
365: return value;
366:
367: int length = Array.getLength(value);
368: Object[] formattedVals = new String[length];
369:
370: for (int i = 0; i < length; i++) {
371: Object element = Array.get(value, i);
372: Object objValue = ArrayUtils.toObject(element);
373: Formatter elementFormatter = getFormatter(elementType);
374: formattedVals[i] = elementFormatter
375: .formatForPresentation(objValue);
376: }
377:
378: return formattedVals;
379: // end Kuali Foundation modification
380: }
381:
382: public Object formatCollection(Collection value) {
383: List stringVals = new ArrayList();
384: Iterator iter = value.iterator();
385: while (iter.hasNext()) {
386: Object obj = iter.next();
387: Formatter formatter = getFormatter(obj.getClass());
388: // begin Kuali Foundation modification
389: stringVals.add(formatter.formatForPresentation(obj));
390: // end Kuali Foundation modification
391: }
392: return stringVals.toArray();
393: }
394:
395: /**
396: * Returns an object representation of the provided string after first removing any extraneous formatting characters. If the
397: * argument is a native array wrapping the actual value, the value is removed (unwrapped) from the array prior to invoking the
398: * callback method {@link #convertToObject(String)}, which performs the actual conversion.
399: * <p>
400: * If the provided object is <code>null</code>, a blank <code>String</code>, or a <code>String[]</code> of length <b>0
401: * </b> or that has <code>null</code> or a blank <code>String</code> in the first position, returns <code>null</code>.
402: * Otherwise, If the destination property is a <code>Collection</code>, returns an instance of that type containing the
403: * string values of the array elements.
404: * <p>
405: * If the provided object is an array, uses a Formatter corresponding to the array's component type to convert each of its
406: * elements, and returns a new array containing the converted values.
407: *
408: * May be overidden by subclasses to customize conversion, though ordinarily {@link #convertToObject(String)}is a better choice
409: * since it takes advantage of <code>convertFromPresentationFormat</code>'s built-in behavior.
410: *
411: * @param value the string value to be converted
412: * @return the object value corresponding to the provided string value
413: * @see convertToObject(String)
414: */
415: public Object convertFromPresentationFormat(Object value) {
416: if (isEmptyValue(value))
417: return getNullObjectValue();
418:
419: Class type = value.getClass();
420: boolean isArray = propertyType != null
421: && propertyType.isArray();
422: boolean isCollection = propertyType != null
423: && Collection.class.isAssignableFrom(propertyType);
424:
425: if (!(isArray || isCollection)) {
426: value = unwrapString(value);
427: return convertToObject((String) value);
428: }
429:
430: String[] strings = type.isArray() ? (String[]) value
431: : new String[] { (String) value };
432:
433: return isArray ? convertToArray(strings)
434: : convertToCollection(strings);
435: }
436:
437: /**
438: * May be overridden by subclasses to provide special handling for <code>null</code> values when converting from presentation
439: * format to a bean property type. The default implementation simply returns <code>null</code>
440: */
441: protected Object getNullObjectValue() {
442: return null;
443: }
444:
445: /**
446: * May be orverridden by subclasses to customize its behavior. The default implementation simply trims and returns the provided
447: * string.
448: */
449: protected Object convertToObject(String string) {
450: return string == null ? null : string.replace("\r\n", "\n")
451: .trim();
452: }
453:
454: /**
455: * Converts an array of strings to a Collection type corresponding to the value of <code>propertyType</code>. Since we don't
456: * have type information for the elements of the collection, no attempt is made to convert the elements from <code>String</code>
457: * to other types. However, subclasses can override this method if they need to provide the ability to convert the elements to a
458: * given type.
459: */
460: protected Collection convertToCollection(String[] strings) {
461: Collection collection = null;
462: Class type = propertyType;
463:
464: if (propertyType.isAssignableFrom(List.class))
465: type = ArrayList.class;
466: else if (propertyType.isAssignableFrom(Set.class))
467: type = HashSet.class;
468:
469: try {
470: collection = (Collection) type.newInstance();
471: } catch (Exception e) {
472: throw new FormatException(CREATE_MSG + propertyType, e);
473: }
474:
475: for (int i = 0; i < strings.length; i++)
476: collection.add(strings[i]);
477:
478: return collection;
479: }
480:
481: /**
482: * Converts an array of strings to an array of objects by calling {@link #convertToObject(String)}on each element of the
483: * provided array in turn, using instances of a Formatter class that corresponds to this Formatter's property type.
484: *
485: * @see #propertyType
486: */
487: protected Object convertToArray(String[] strings) {
488: Class type = propertyType.getComponentType();
489: // begin Kuali Foundation modification
490: Formatter formatter = getFormatter(type);
491: // end Kuali Foundation modification
492: Object array = null;
493: try {
494: array = Array.newInstance(type, strings.length);
495: } catch (Exception e) {
496: throw new FormatException(CREATE_MSG + type, e);
497: }
498:
499: for (int i = 0; i < strings.length; i++) {
500: Object value = formatter.convertToObject(strings[i]);
501: // begin Kuali Foundation modification
502: ArrayUtils.setArrayValue(array, type, value, i);
503: // end Kuali Foundation modification
504: }
505:
506: return array;
507: }
508:
509: public static String unwrapString(Object target) {
510:
511: if (target.getClass().isArray()) {
512: String wrapper[] = (String[]) target;
513: return wrapper.length > 0 ? wrapper[0] : null;
514: }
515: // begin Kuali Foundation modification
516: // if target object is null, return a null String
517: else if (target == null) {
518: return new String();
519: }
520:
521: // otherwise, return the string value of the object, with the hope
522: // that the toString() has been meaningfully overriden
523: else {
524: return target.toString();
525: }
526: // end Kuali Foundation modification
527: }
528:
529: public static boolean isNullValue(Object obj) {
530: if (obj == null)
531: return true;
532:
533: // begin Kuali Foundation modification
534: if ((obj instanceof String)
535: && StringUtils.isEmpty((String) obj))
536: return true;
537: // end Kuali Foundation modification
538:
539: return false;
540: }
541:
542: public static boolean isEmptyValue(Object obj) {
543: if (obj == null)
544: return true;
545: // begin Kuali Foundation modification
546: if ((obj instanceof String)
547: && StringUtils.isEmpty((String) obj))
548: return true;
549: // end Kuali Foundation modification
550: Class type = obj.getClass();
551: if (type.isArray()) {
552: Class compType = type.getComponentType();
553: if (compType.isPrimitive())
554: return false;
555: if (((Object[]) obj).length == 0)
556: return true;
557: if (((Object[]) obj)[0] == null)
558: return true;
559: if (String.class.isAssignableFrom(compType)) {
560: // begin Kuali Foundation modification
561: return StringUtils.isEmpty(((String[]) obj)[0]);
562: // end Kuali Foundation modification
563: }
564: }
565: return false;
566: }
567:
568: protected String trimString(Object target) {
569: String stringValue = null;
570: try {
571: stringValue = (String) target;
572: } catch (ClassCastException e) {
573: throw new FormatException("Can't cast " + target
574: + " to String", e);
575: }
576: return stringValue == null ? null : stringValue.trim();
577: }
578:
579: /**
580: * @deprecated in favor of {@link StringUtils#isEmptyString(String)}
581: */
582: protected boolean isBlank(String string) {
583: return string == null || string.trim().length() == 0;
584: }
585: }
|