1 /*
2 * Copyright (c) 2002-2007 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.conversion.impl;
6
7 import java.lang.reflect.Array;
8 import java.lang.reflect.Constructor;
9 import java.lang.reflect.Member;
10 import java.math.BigDecimal;
11 import java.math.BigInteger;
12 import java.text.DateFormat;
13 import java.text.NumberFormat;
14 import java.text.ParseException;
15 import java.text.ParsePosition;
16 import java.text.SimpleDateFormat;
17 import java.util.ArrayList;
18 import java.util.Calendar;
19 import java.util.Collection;
20 import java.util.Date;
21 import java.util.HashSet;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.SortedSet;
28 import java.util.TreeSet;
29
30 import com.opensymphony.xwork2.ActionContext;
31 import com.opensymphony.xwork2.ObjectFactory;
32 import com.opensymphony.xwork2.XWorkException;
33 import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer;
34 import com.opensymphony.xwork2.conversion.TypeConverter;
35 import com.opensymphony.xwork2.inject.Inject;
36 import com.opensymphony.xwork2.util.TextUtils;
37 import com.opensymphony.xwork2.util.XWorkList;
38
39
40 /**
41 * <!-- START SNIPPET: javadoc -->
42 * <p/>
43 * XWork will automatically handle the most common type conversion for you. This includes support for converting to
44 * and from Strings for each of the following:
45 * <p/>
46 * <ul>
47 * <li>String</li>
48 * <li>boolean / Boolean</li>
49 * <li>char / Character</li>
50 * <li>int / Integer, float / Float, long / Long, double / Double</li>
51 * <li>dates - uses the SHORT format for the Locale associated with the current request</li>
52 * <li>arrays - assuming the individual strings can be coverted to the individual items</li>
53 * <li>collections - if not object type can be determined, it is assumed to be a String and a new ArrayList is
54 * created</li>
55 * </ul>
56 * <p/> Note that with arrays the type conversion will defer to the type of the array elements and try to convert each
57 * item individually. As with any other type conversion, if the conversion can't be performed the standard type
58 * conversion error reporting is used to indicate a problem occured while processing the type conversion.
59 * <p/>
60 * <!-- END SNIPPET: javadoc -->
61 *
62 * @author <a href="mailto:plightbo@gmail.com">Pat Lightbody</a>
63 * @author Mike Mosiewicz
64 * @author Rainer Hermanns
65 * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
66 */
67 public class XWorkBasicConverter extends DefaultTypeConverter {
68
69 private static String MILLISECOND_FORMAT = ".SSS";
70
71 private ObjectTypeDeterminer objectTypeDeterminer;
72 private XWorkConverter xworkConverter;
73 private ObjectFactory objectFactory;
74
75 @Inject
76 public void setObjectTypeDeterminer(ObjectTypeDeterminer det) {
77 this.objectTypeDeterminer = det;
78 }
79
80 @Inject
81 public void setXWorkConverter(XWorkConverter conv) {
82 this.xworkConverter = conv;
83 }
84
85 @Inject
86 public void setObjectFactory(ObjectFactory fac) {
87 this.objectFactory = fac;
88 }
89
90 public Object convertValue(Map context, Object o, Member member, String s, Object value, Class toType) {
91 Object result = null;
92
93 if (value == null || toType.isAssignableFrom(value.getClass())) {
94 // no need to convert at all, right?
95 return value;
96 }
97
98 if (toType == String.class) {
99 /* the code below has been disabled as it causes sideffects in Struts2 (XW-512)
100 // if input (value) is a number then use special conversion method (XW-490)
101 Class inputType = value.getClass();
102 if (Number.class.isAssignableFrom(inputType)) {
103 result = doConvertFromNumberToString(context, value, inputType);
104 if (result != null) {
105 return result;
106 }
107 }*/
108 // okay use default string conversion
109 result = doConvertToString(context, value);
110 } else if (toType == boolean.class) {
111 result = doConvertToBoolean(value);
112 } else if (toType == Boolean.class) {
113 result = doConvertToBoolean(value);
114 } else if (toType.isArray()) {
115 result = doConvertToArray(context, o, member, s, value, toType);
116 } else if (Date.class.isAssignableFrom(toType)) {
117 result = doConvertToDate(context, value, toType);
118 } else if (Calendar.class.isAssignableFrom(toType)) {
119 Date dateResult = (Date) doConvertToDate(context, value, Date.class);
120 if (dateResult != null) {
121 Calendar calendar = Calendar.getInstance();
122 calendar.setTime(dateResult);
123 result = calendar;
124 }
125 } else if (Collection.class.isAssignableFrom(toType)) {
126 result = doConvertToCollection(context, o, member, s, value, toType);
127 } else if (toType == Character.class) {
128 result = doConvertToCharacter(value);
129 } else if (toType == char.class) {
130 result = doConvertToCharacter(value);
131 } else if (Number.class.isAssignableFrom(toType) || toType.isPrimitive()) {
132 result = doConvertToNumber(context, value, toType);
133 } else if (toType == Class.class) {
134 result = doConvertToClass(value);
135 }
136
137 if (result == null) {
138 if (value instanceof Object[]) {
139 Object[] array = (Object[]) value;
140
141 if (array.length >= 1) {
142 value = array[0];
143 }
144
145 // let's try to convert the first element only
146 result = convertValue(context, o, member, s, value, toType);
147 } else if (!"".equals(value)) { // we've already tried the types we know
148 result = super.convertValue(context, value, toType);
149 }
150
151 if (result == null && value != null && !"".equals(value)) {
152 throw new XWorkException("Cannot create type " + toType + " from value " + value);
153 }
154 }
155
156 return result;
157 }
158
159 private Locale getLocale(Map context) {
160 if (context == null) {
161 return Locale.getDefault();
162 }
163
164 Locale locale = (Locale) context.get(ActionContext.LOCALE);
165
166 if (locale == null) {
167 locale = Locale.getDefault();
168 }
169
170 return locale;
171 }
172
173 /**
174 * Creates a Collection of the specified type.
175 *
176 * @param fromObject
177 * @param propertyName
178 * @param toType the type of Collection to create
179 * @param memberType the type of object elements in this collection must be
180 * @param size the initial size of the collection (ignored if 0 or less)
181 * @return a Collection of the specified type
182 */
183 private Collection createCollection(Object fromObject, String propertyName, Class toType, Class memberType, int size) {
184 // try {
185 // Object original = Ognl.getValue(OgnlUtil.compile(propertyName),fromObject);
186 // if (original instanceof Collection) {
187 // Collection coll = (Collection) original;
188 // coll.clear();
189 // return coll;
190 // }
191 // } catch (Exception e) {
192 // // fail back to creating a new one
193 // }
194
195 Collection result;
196
197 if (toType == Set.class) {
198 if (size > 0) {
199 result = new HashSet(size);
200 } else {
201 result = new HashSet();
202 }
203 } else if (toType == SortedSet.class) {
204 result = new TreeSet();
205 } else {
206 if (size > 0) {
207 result = new XWorkList(objectFactory, xworkConverter, memberType, size);
208 } else {
209 result = new XWorkList(objectFactory, xworkConverter, memberType);
210 }
211 }
212
213 return result;
214 }
215
216 private Object doConvertToArray(Map context, Object o, Member member, String s, Object value, Class toType) {
217 Object result = null;
218 Class componentType = toType.getComponentType();
219
220 if (componentType != null) {
221 TypeConverter converter = getTypeConverter(context);
222
223 if (value.getClass().isArray()) {
224 int length = Array.getLength(value);
225 result = Array.newInstance(componentType, length);
226
227 for (int i = 0; i < length; i++) {
228 Object valueItem = Array.get(value, i);
229 Array.set(result, i, converter.convertValue(context, o, member, s, valueItem, componentType));
230 }
231 } else {
232 result = Array.newInstance(componentType, 1);
233 Array.set(result, 0, converter.convertValue(context, o, member, s, value, componentType));
234 }
235 }
236
237 return result;
238 }
239
240 private Object doConvertToCharacter(Object value) {
241 if (value instanceof String) {
242 String cStr = (String) value;
243
244 return (cStr.length() > 0) ? new Character(cStr.charAt(0)) : null;
245 }
246
247 return null;
248 }
249
250 private Object doConvertToBoolean(Object value) {
251 if (value instanceof String) {
252 String bStr = (String) value;
253
254 return Boolean.valueOf(bStr);
255 }
256
257 return null;
258 }
259
260 private Class doConvertToClass(Object value) {
261 Class clazz = null;
262
263 if (value instanceof String && value != null && ((String) value).length() > 0) {
264 try {
265 clazz = Class.forName((String) value);
266 } catch (ClassNotFoundException e) {
267 throw new XWorkException(e.getLocalizedMessage(), e);
268 }
269 }
270
271 return clazz;
272 }
273
274 private Collection doConvertToCollection(Map context, Object o, Member member, String prop, Object value, Class toType) {
275 Collection result;
276 Class memberType = String.class;
277
278 if (o != null) {
279 //memberType = (Class) XWorkConverter.getInstance().getConverter(o.getClass(), XWorkConverter.CONVERSION_COLLECTION_PREFIX + prop);
280 memberType = objectTypeDeterminer.getElementClass(o.getClass(), prop, null);
281
282 if (memberType == null) {
283 memberType = String.class;
284 }
285 }
286
287 if (toType.isAssignableFrom(value.getClass())) {
288 // no need to do anything
289 result = (Collection) value;
290 } else if (value.getClass().isArray()) {
291 Object[] objArray = (Object[]) value;
292 TypeConverter converter = getTypeConverter(context);
293 result = createCollection(o, prop, toType, memberType, objArray.length);
294
295 for (int i = 0; i < objArray.length; i++) {
296 result.add(converter.convertValue(context, o, member, prop, objArray[i], memberType));
297 }
298 } else if (Collection.class.isAssignableFrom(value.getClass())) {
299 Collection col = (Collection) value;
300 TypeConverter converter = getTypeConverter(context);
301 result = createCollection(o, prop, toType, memberType, col.size());
302
303 for (Iterator it = col.iterator(); it.hasNext();) {
304 result.add(converter.convertValue(context, o, member, prop, it.next(), memberType));
305 }
306 } else {
307 result = createCollection(o, prop, toType, memberType, -1);
308 result.add(value);
309 }
310
311 return result;
312 }
313
314 private Object doConvertToDate(Map context, Object value, Class toType) {
315 Date result = null;
316
317 if (value instanceof String && value != null && ((String) value).length() > 0) {
318 String sa = (String) value;
319 Locale locale = getLocale(context);
320
321 DateFormat df = null;
322 if (java.sql.Time.class == toType) {
323 df = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
324 } else if (java.sql.Timestamp.class == toType) {
325 Date check = null;
326 SimpleDateFormat dtfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT,
327 DateFormat.MEDIUM,
328 locale);
329 SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT,
330 locale);
331
332 SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT,
333 locale);
334
335 SimpleDateFormat[] fmts = {fullfmt, dtfmt, dfmt};
336 for (int i = 0; i < fmts.length; i++) {
337 try {
338 check = fmts[i].parse(sa);
339 df = fmts[i];
340 if (check != null) {
341 break;
342 }
343 } catch (ParseException ignore) {
344 }
345 }
346 } else if (java.util.Date.class == toType) {
347 Date check = null;
348 SimpleDateFormat d1 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG, locale);
349 SimpleDateFormat d2 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale);
350 SimpleDateFormat d3 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
351 SimpleDateFormat rfc3399 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
352 SimpleDateFormat[] dfs = {d1, d2, d3, rfc3399}; //added RFC 3339 date format (XW-473)
353 for (int i = 0; i < dfs.length; i++) {
354 try {
355 check = dfs[i].parse(sa);
356 df = dfs[i];
357 if (check != null) {
358 break;
359 }
360 }
361 catch (ParseException ignore) {
362 }
363 }
364 }
365 //final fallback for dates without time
366 if (df == null) {
367 df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
368 }
369 try {
370 df.setLenient(false); // let's use strict parsing (XW-341)
371 result = df.parse(sa);
372 if (!(Date.class == toType)) {
373 try {
374 Constructor constructor = toType.getConstructor(new Class[]{long.class});
375 return constructor.newInstance(new Object[]{new Long(result.getTime())});
376 } catch (Exception e) {
377 throw new XWorkException("Couldn't create class " + toType + " using default (long) constructor", e);
378 }
379 }
380 } catch (ParseException e) {
381 throw new XWorkException("Could not parse date", e);
382 }
383 } else if (Date.class.isAssignableFrom(value.getClass())) {
384 result = (Date) value;
385 }
386 return result;
387 }
388
389 private Object doConvertToNumber(Map context, Object value, Class toType) {
390 if (value instanceof String) {
391 if (toType == BigDecimal.class) {
392 return new BigDecimal((String) value);
393 } else if (toType == BigInteger.class) {
394 return new BigInteger((String) value);
395 } else if (toType.isPrimitive()) {
396 return super.convertValue(context, value, toType);
397 } else {
398 String stringValue = (String) value;
399 if (!toType.isPrimitive() && (stringValue == null || stringValue.length() == 0)) {
400 return null;
401 }
402 NumberFormat numFormat = NumberFormat.getInstance(getLocale(context));
403 ParsePosition parsePos = new ParsePosition(0);
404 if (isIntegerType(toType)) {
405 numFormat.setParseIntegerOnly(true);
406 }
407 numFormat.setGroupingUsed(true);
408 Number number = numFormat.parse(stringValue, parsePos);
409
410 if (parsePos.getIndex() != stringValue.length()) {
411 throw new XWorkException("Unparseable number: \"" + stringValue + "\" at position "
412 + parsePos.getIndex());
413 } else {
414 value = super.convertValue(context, number, toType);
415 }
416 }
417 } else if (value instanceof Object[]) {
418 Object[] objArray = (Object[]) value;
419
420 if (objArray.length == 1) {
421 return doConvertToNumber(context, objArray[0], toType);
422 }
423 }
424
425 // pass it through DefaultTypeConverter
426 return super.convertValue(context, value, toType);
427 }
428
429 protected boolean isIntegerType(Class type) {
430 if (double.class == type || float.class == type || Double.class == type || Float.class == type
431 || char.class == type || Character.class == type) {
432 return false;
433 }
434
435 return true;
436 }
437
438 /**
439 * Converts the input as a number using java's number formatter to a string output.
440 */
441 private String doConvertFromNumberToString(Map context, Object value, Class toType) {
442 // XW-409: If the input is a Number we should format it to a string using the choosen locale and use java's numberformatter
443 if (Number.class.isAssignableFrom(toType)) {
444 NumberFormat numFormat = NumberFormat.getInstance(getLocale(context));
445 if (isIntegerType(toType)) {
446 numFormat.setParseIntegerOnly(true);
447 }
448 numFormat.setGroupingUsed(true);
449 numFormat.setMaximumFractionDigits(99); // to be sure we include all digits after decimal seperator, otherwise some of the fractions can be chopped
450
451 String number = numFormat.format(value);
452 if (number != null) {
453 return number;
454 }
455 }
456
457 return null; // no number
458 }
459
460
461 private String doConvertToString(Map context, Object value) {
462 String result = null;
463
464 if (value instanceof int[]) {
465 int[] x = (int[]) value;
466 List intArray = new ArrayList(x.length);
467
468 for (int i = 0; i < x.length; i++) {
469 intArray.add(new Integer(x[i]));
470 }
471
472 result = TextUtils.join(", ", intArray);
473 } else if (value instanceof long[]) {
474 long[] x = (long[]) value;
475 List intArray = new ArrayList(x.length);
476
477 for (int i = 0; i < x.length; i++) {
478 intArray.add(new Long(x[i]));
479 }
480
481 result = TextUtils.join(", ", intArray);
482 } else if (value instanceof double[]) {
483 double[] x = (double[]) value;
484 List intArray = new ArrayList(x.length);
485
486 for (int i = 0; i < x.length; i++) {
487 intArray.add(new Double(x[i]));
488 }
489
490 result = TextUtils.join(", ", intArray);
491 } else if (value instanceof boolean[]) {
492 boolean[] x = (boolean[]) value;
493 List intArray = new ArrayList(x.length);
494
495 for (int i = 0; i < x.length; i++) {
496 intArray.add(new Boolean(x[i]));
497 }
498
499 result = TextUtils.join(", ", intArray);
500 } else if (value instanceof Date) {
501 DateFormat df = null;
502 if (value instanceof java.sql.Time) {
503 df = DateFormat.getTimeInstance(DateFormat.MEDIUM, getLocale(context));
504 } else if (value instanceof java.sql.Timestamp) {
505 SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT,
506 DateFormat.MEDIUM,
507 getLocale(context));
508 df = new SimpleDateFormat(dfmt.toPattern() + MILLISECOND_FORMAT);
509 } else {
510 df = DateFormat.getDateInstance(DateFormat.SHORT, getLocale(context));
511 }
512 result = df.format(value);
513 } else if (value instanceof String[]) {
514 result = TextUtils.join(", ", (String[]) value);
515 }
516
517 return result;
518 }
519 }