View Javadoc

1   /*
2    * $Id: JSONPopulator.java 799110 2009-07-29 22:44:26Z musachy $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts2.json;
22  
23  import java.beans.BeanInfo;
24  import java.beans.IntrospectionException;
25  import java.beans.Introspector;
26  import java.beans.PropertyDescriptor;
27  import java.lang.reflect.Array;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.Method;
30  import java.lang.reflect.Modifier;
31  import java.lang.reflect.ParameterizedType;
32  import java.lang.reflect.Type;
33  import java.math.BigDecimal;
34  import java.math.BigInteger;
35  import java.text.DateFormat;
36  import java.text.ParseException;
37  import java.text.SimpleDateFormat;
38  import java.util.ArrayList;
39  import java.util.Collection;
40  import java.util.Date;
41  import java.util.HashMap;
42  import java.util.HashSet;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Set;
48  
49  import org.apache.struts2.json.annotations.JSON;
50  
51  import com.opensymphony.xwork2.util.logging.Logger;
52  import com.opensymphony.xwork2.util.logging.LoggerFactory;
53  
54  /***
55   * Isolate the process of populating JSON objects from the Interceptor class
56   * itself.
57   */
58  public class JSONPopulator {
59  
60      private static final Logger LOG = LoggerFactory.getLogger(JSONPopulator.class);
61  
62      private String dateFormat = JSONUtil.RFC3339_FORMAT;
63  
64      public JSONPopulator() {
65      }
66  
67      public JSONPopulator(String dateFormat) {
68          this.dateFormat = dateFormat;
69      }
70  
71      public String getDateFormat() {
72          return dateFormat;
73      }
74  
75      public void setDateFormat(String dateFormat) {
76          this.dateFormat = dateFormat;
77      }
78  
79      @SuppressWarnings("unchecked")
80      public void populateObject(Object object, final Map elements) throws IllegalAccessException,
81              InvocationTargetException, NoSuchMethodException, IntrospectionException,
82              IllegalArgumentException, JSONException, InstantiationException {
83          Class clazz = object.getClass();
84  
85          BeanInfo info = Introspector.getBeanInfo(clazz);
86          PropertyDescriptor[] props = info.getPropertyDescriptors();
87  
88          // iterate over class fields
89          for (int i = 0; i < props.length; ++i) {
90              PropertyDescriptor prop = props[i];
91              String name = prop.getName();
92  
93              if (elements.containsKey(name)) {
94                  Object value = elements.get(name);
95                  Method method = prop.getWriteMethod();
96  
97                  if (method != null) {
98                      JSON json = method.getAnnotation(JSON.class);
99                      if ((json != null) && !json.deserialize()) {
100                         continue;
101                     }
102 
103                     // use only public setters
104                     if (Modifier.isPublic(method.getModifiers())) {
105                         Class[] paramTypes = method.getParameterTypes();
106                         Type[] genericTypes = method.getGenericParameterTypes();
107 
108                         if (paramTypes.length == 1) {
109                             Object convertedValue = this.convert(paramTypes[0], genericTypes[0], value,
110                                     method);
111                             method.invoke(object, new Object[] { convertedValue });
112                         }
113                     }
114                 }
115             }
116         }
117     }
118 
119     @SuppressWarnings("unchecked")
120     public Object convert(Class clazz, Type type, Object value, Method method)
121             throws IllegalArgumentException, JSONException, IllegalAccessException,
122             InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException {
123 
124         if (value == null) {
125             // if it is a java primitive then get a default value, otherwise
126             // leave it as null
127             return clazz.isPrimitive() ? convertPrimitive(clazz, value, method) : null;
128         } else if (isJSONPrimitive(clazz))
129             return convertPrimitive(clazz, value, method);
130         else if (Collection.class.isAssignableFrom(clazz))
131             return convertToCollection(clazz, type, value, method);
132         else if (Map.class.isAssignableFrom(clazz))
133             return convertToMap(clazz, type, value, method);
134         else if (clazz.isArray())
135             return convertToArray(clazz, type, value, method);
136         else if (value instanceof Map) {
137             // nested bean
138             Object convertedValue = clazz.newInstance();
139             this.populateObject(convertedValue, (Map) value);
140             return convertedValue;
141         } else if (BigDecimal.class.equals(clazz)) {
142             return new BigDecimal(value != null ? value.toString() : "0");
143         } else if (BigInteger.class.equals(clazz)) {
144             return new BigInteger(value != null ? value.toString() : "0");
145         } else
146             throw new JSONException("Incompatible types for property " + method.getName());
147     }
148 
149     private static boolean isJSONPrimitive(Class clazz) {
150         return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Date.class)
151                 || clazz.equals(Boolean.class) || clazz.equals(Byte.class) || clazz.equals(Character.class)
152                 || clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Integer.class)
153                 || clazz.equals(Long.class) || clazz.equals(Short.class) || clazz.equals(Locale.class)
154                 || clazz.isEnum();
155     }
156 
157     @SuppressWarnings("unchecked")
158     private Object convertToArray(Class clazz, Type type, Object value, Method accessor)
159             throws JSONException, IllegalArgumentException, IllegalAccessException,
160             InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException {
161         if (value == null)
162             return null;
163         else if (value instanceof List) {
164             Class arrayType = clazz.getComponentType();
165             List values = (List) value;
166             Object newArray = Array.newInstance(arrayType, values.size());
167 
168             // create an object for each element
169             for (int j = 0; j < values.size(); j++) {
170                 Object listValue = values.get(j);
171 
172                 if (arrayType.equals(Object.class)) {
173                     // Object[]
174                     Array.set(newArray, j, listValue);
175                 } else if (isJSONPrimitive(arrayType)) {
176                     // primitive array
177                     Array.set(newArray, j, this.convertPrimitive(arrayType, listValue, accessor));
178                 } else if (listValue instanceof Map) {
179                     // array of other class
180                     Object newObject = null;
181                     if (Map.class.isAssignableFrom(arrayType)) {
182                         newObject = convertToMap(arrayType, type, listValue, accessor);
183                     } else if (List.class.isAssignableFrom(arrayType)) {
184                         newObject = convertToCollection(arrayType, type, listValue, accessor);
185                     } else {
186                         newObject = arrayType.newInstance();
187                         this.populateObject(newObject, (Map) listValue);
188                     }
189 
190                     Array.set(newArray, j, newObject);
191                 } else
192                     throw new JSONException("Incompatible types for property " + accessor.getName());
193             }
194 
195             return newArray;
196         } else
197             throw new JSONException("Incompatible types for property " + accessor.getName());
198     }
199 
200     @SuppressWarnings("unchecked")
201     private Object convertToCollection(Class clazz, Type type, Object value, Method accessor)
202             throws JSONException, IllegalArgumentException, IllegalAccessException,
203             InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException {
204         if (value == null)
205             return null;
206         else if (value instanceof List) {
207             Class itemClass = Object.class;
208             Type itemType = null;
209             if ((type != null) && (type instanceof ParameterizedType)) {
210                 ParameterizedType ptype = (ParameterizedType) type;
211                 itemType = ptype.getActualTypeArguments()[0];
212                 if (itemType.getClass().equals(Class.class)) {
213                     itemClass = (Class) itemType;
214                 } else {
215                     itemClass = (Class) ((ParameterizedType) itemType).getRawType();
216                 }
217             }
218             List values = (List) value;
219 
220             Collection newCollection = null;
221             try {
222                 newCollection = (Collection) clazz.newInstance();
223             } catch (InstantiationException ex) {
224                 // fallback if clazz represents an interface or abstract class
225                 if (Set.class.isAssignableFrom(clazz)) {
226                     newCollection = new HashSet();
227                 } else {
228                     newCollection = new ArrayList();
229                 }
230             }
231 
232             // create an object for each element
233             for (int j = 0; j < values.size(); j++) {
234                 Object listValue = values.get(j);
235 
236                 if (itemClass.equals(Object.class)) {
237                     // Object[]
238                     newCollection.add(listValue);
239                 } else if (isJSONPrimitive(itemClass)) {
240                     // primitive array
241                     newCollection.add(this.convertPrimitive(itemClass, listValue, accessor));
242                 } else if (Map.class.isAssignableFrom(itemClass)) {
243                     Object newObject = convertToMap(itemClass, itemType, listValue, accessor);
244                     newCollection.add(newObject);
245                 } else if (List.class.isAssignableFrom(itemClass)) {
246                     Object newObject = convertToCollection(itemClass, itemType, listValue, accessor);
247                     newCollection.add(newObject);
248                 } else if (listValue instanceof Map) {
249                     // array of beans
250                     Object newObject = itemClass.newInstance();
251                     this.populateObject(newObject, (Map) listValue);
252                     newCollection.add(newObject);
253                 } else
254                     throw new JSONException("Incompatible types for property " + accessor.getName());
255             }
256 
257             return newCollection;
258         } else
259             throw new JSONException("Incompatible types for property " + accessor.getName());
260     }
261 
262     @SuppressWarnings("unchecked")
263     private Object convertToMap(Class clazz, Type type, Object value, Method accessor) throws JSONException,
264             IllegalArgumentException, IllegalAccessException, InvocationTargetException,
265             InstantiationException, NoSuchMethodException, IntrospectionException {
266         if (value == null)
267             return null;
268         else if (value instanceof Map) {
269             Class itemClass = Object.class;
270             Type itemType = null;
271             if ((type != null) && (type instanceof ParameterizedType)) {
272                 ParameterizedType ptype = (ParameterizedType) type;
273                 itemType = ptype.getActualTypeArguments()[1];
274                 if (itemType.getClass().equals(Class.class)) {
275                     itemClass = (Class) itemType;
276                 } else {
277                     itemClass = (Class) ((ParameterizedType) itemType).getRawType();
278                 }
279             }
280             Map values = (Map) value;
281 
282             Map newMap = null;
283             try {
284                 newMap = (Map) clazz.newInstance();
285             } catch (InstantiationException ex) {
286                 // fallback if clazz represents an interface or abstract class
287                 newMap = new HashMap();
288             }
289 
290             // create an object for each element
291             Iterator iter = values.entrySet().iterator();
292             while (iter.hasNext()) {
293                 Map.Entry entry = (Map.Entry) iter.next();
294                 String key = (String) entry.getKey();
295                 Object v = entry.getValue();
296 
297                 if (itemClass.equals(Object.class)) {
298                     // String, Object
299                     newMap.put(key, v);
300                 } else if (isJSONPrimitive(itemClass)) {
301                     // primitive map
302                     newMap.put(key, this.convertPrimitive(itemClass, v, accessor));
303                 } else if (Map.class.isAssignableFrom(itemClass)) {
304                     Object newObject = convertToMap(itemClass, itemType, v, accessor);
305                     newMap.put(key, newObject);
306                 } else if (List.class.isAssignableFrom(itemClass)) {
307                     Object newObject = convertToCollection(itemClass, itemType, v, accessor);
308                     newMap.put(key, newObject);
309                 } else if (v instanceof Map) {
310                     // map of beans
311                     Object newObject = itemClass.newInstance();
312                     this.populateObject(newObject, (Map) v);
313                     newMap.put(key, newObject);
314                 } else
315                     throw new JSONException("Incompatible types for property " + accessor.getName());
316             }
317 
318             return newMap;
319         } else
320             throw new JSONException("Incompatible types for property " + accessor.getName());
321     }
322 
323     /***
324      * Converts numbers to the desired class, if possible
325      * 
326      * @throws JSONException
327      */
328     @SuppressWarnings("unchecked")
329     private Object convertPrimitive(Class clazz, Object value, Method method) throws JSONException {
330         if (value == null) {
331             if (Short.TYPE.equals(clazz) || Short.class.equals(clazz))
332                 return (short) 0;
333             else if (Byte.TYPE.equals(clazz) || Byte.class.equals(clazz))
334                 return (byte) 0;
335             else if (Integer.TYPE.equals(clazz) || Integer.class.equals(clazz))
336                 return 0;
337             else if (Long.TYPE.equals(clazz) || Long.class.equals(clazz))
338                 return 0L;
339             else if (Float.TYPE.equals(clazz) || Float.class.equals(clazz))
340                 return 0f;
341             else if (Double.TYPE.equals(clazz) || Double.class.equals(clazz))
342                 return 0d;
343             else if (Boolean.TYPE.equals(clazz) || Boolean.class.equals(clazz))
344                 return Boolean.FALSE;
345             else
346                 return null;
347         } else if (value instanceof Number) {
348             Number number = (Number) value;
349 
350             if (Short.TYPE.equals(clazz))
351                 return number.shortValue();
352             else if (Short.class.equals(clazz))
353                 return new Short(number.shortValue());
354             else if (Byte.TYPE.equals(clazz))
355                 return number.byteValue();
356             else if (Byte.class.equals(clazz))
357                 return new Byte(number.byteValue());
358             else if (Integer.TYPE.equals(clazz))
359                 return number.intValue();
360             else if (Integer.class.equals(clazz))
361                 return new Integer(number.intValue());
362             else if (Long.TYPE.equals(clazz))
363                 return number.longValue();
364             else if (Long.class.equals(clazz))
365                 return new Long(number.longValue());
366             else if (Float.TYPE.equals(clazz))
367                 return number.floatValue();
368             else if (Float.class.equals(clazz))
369                 return new Float(number.floatValue());
370             else if (Double.TYPE.equals(clazz))
371                 return number.doubleValue();
372             else if (Double.class.equals(clazz))
373                 return new Double(number.doubleValue());
374             else if (String.class.equals(clazz))
375                 return value.toString();
376         } else if (clazz.equals(Date.class)) {
377             try {
378                 JSON json = method.getAnnotation(JSON.class);
379 
380                 DateFormat formatter = new SimpleDateFormat(
381                         (json != null) && (json.format().length() > 0) ? json.format() : this.dateFormat);
382                 return formatter.parse((String) value);
383             } catch (ParseException e) {
384                 LOG.error(e.getMessage(), e);
385                 throw new JSONException("Unable to parse date from: " + value);
386             }
387         } else if (clazz.isEnum()) {
388             String sValue = (String) value;
389             return Enum.valueOf(clazz, sValue);
390         } else if (value instanceof String) {
391             String sValue = (String) value;
392             if (Boolean.TYPE.equals(clazz))
393                 return Boolean.parseBoolean(sValue);
394             else if (Boolean.class.equals(clazz))
395                 return Boolean.valueOf(sValue);
396             else if (Short.TYPE.equals(clazz))
397                 return Short.parseShort(sValue);
398             else if (Short.class.equals(clazz))
399                 return Short.valueOf(sValue);
400             else if (Byte.TYPE.equals(clazz))
401                 return Byte.parseByte(sValue);
402             else if (Byte.class.equals(clazz))
403                 return Byte.valueOf(sValue);
404             else if (Integer.TYPE.equals(clazz))
405                 return Integer.parseInt(sValue);
406             else if (Integer.class.equals(clazz))
407                 return Integer.valueOf(sValue);
408             else if (Long.TYPE.equals(clazz))
409                 return Long.parseLong(sValue);
410             else if (Long.class.equals(clazz))
411                 return Long.valueOf(sValue);
412             else if (Float.TYPE.equals(clazz))
413                 return Float.parseFloat(sValue);
414             else if (Float.class.equals(clazz))
415                 return Float.valueOf(sValue);
416             else if (Double.TYPE.equals(clazz))
417                 return Double.parseDouble(sValue);
418             else if (Double.class.equals(clazz))
419                 return Double.valueOf(sValue);
420             else if (Character.TYPE.equals(clazz) || Character.class.equals(clazz)) {
421                 char charValue = 0;
422                 if (sValue.length() > 0) {
423                     charValue = sValue.charAt(0);
424                 }
425                 if (Character.TYPE.equals(clazz))
426                     return charValue;
427                 else
428                     return new Character(charValue);
429             } else if (clazz.equals(Locale.class)) {
430                 String[] components = sValue.split("_", 2);
431                 if (components.length == 2) {
432                     return new Locale(components[0], components[1]);
433                 } else {
434                     return new Locale(sValue);
435                 }
436             } else if (Enum.class.isAssignableFrom(clazz)) {
437                 return Enum.valueOf(clazz, sValue);
438             }
439         }
440 
441         return value;
442     }
443 
444 }