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 }