| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.struts2.json; |
| |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.struts2.json.annotations.JSON; |
| |
| import java.beans.BeanInfo; |
| import java.beans.IntrospectionException; |
| import java.beans.Introspector; |
| import java.beans.PropertyDescriptor; |
| import java.lang.reflect.*; |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| import java.text.DateFormat; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.*; |
| |
| /** |
| * Isolate the process of populating JSON objects from the Interceptor class |
| * itself. |
| */ |
| public class JSONPopulator { |
| |
| private static final Logger LOG = LogManager.getLogger(JSONPopulator.class); |
| |
| private String dateFormat = JSONUtil.RFC3339_FORMAT; |
| |
| public JSONPopulator() { |
| } |
| |
| public JSONPopulator(String dateFormat) { |
| this.dateFormat = dateFormat; |
| } |
| |
| public String getDateFormat() { |
| return dateFormat; |
| } |
| |
| public void setDateFormat(String dateFormat) { |
| this.dateFormat = dateFormat; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public void populateObject(Object object, final Map elements) throws IllegalAccessException, |
| InvocationTargetException, NoSuchMethodException, IntrospectionException, |
| IllegalArgumentException, JSONException, InstantiationException { |
| Class clazz = object.getClass(); |
| |
| BeanInfo info = Introspector.getBeanInfo(clazz); |
| PropertyDescriptor[] props = info.getPropertyDescriptors(); |
| |
| // iterate over class fields |
| for (PropertyDescriptor prop : props) { |
| String name = prop.getName(); |
| |
| if (elements.containsKey(name)) { |
| Object value = elements.get(name); |
| Method method = prop.getWriteMethod(); |
| |
| if (method != null) { |
| JSON json = method.getAnnotation(JSON.class); |
| if ((json != null) && !json.deserialize()) { |
| continue; |
| } |
| |
| // use only public setters |
| if (Modifier.isPublic(method.getModifiers())) { |
| Class[] paramTypes = method.getParameterTypes(); |
| Type[] genericTypes = method.getGenericParameterTypes(); |
| |
| if (paramTypes.length == 1) { |
| Object convertedValue = this.convert(paramTypes[0], genericTypes[0], value, method); |
| method.invoke(object, new Object[] { convertedValue }); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| public Object convert(Class clazz, Type type, Object value, Method method) |
| throws IllegalArgumentException, JSONException, IllegalAccessException, |
| InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException { |
| |
| if (value == null) { |
| // if it is a java primitive then get a default value, otherwise |
| // leave it as null |
| return clazz.isPrimitive() ? convertPrimitive(clazz, value, method) : null; |
| } else if (isJSONPrimitive(clazz)) |
| return convertPrimitive(clazz, value, method); |
| else if (Collection.class.isAssignableFrom(clazz)) |
| return convertToCollection(clazz, type, value, method); |
| else if (Map.class.isAssignableFrom(clazz)) |
| return convertToMap(clazz, type, value, method); |
| else if (clazz.isArray()) |
| return convertToArray(clazz, type, value, method); |
| else if (value instanceof Map) { |
| // nested bean |
| Object convertedValue = clazz.newInstance(); |
| this.populateObject(convertedValue, (Map) value); |
| return convertedValue; |
| } else if (BigDecimal.class.equals(clazz)) { |
| return new BigDecimal(value.toString()); |
| } else if (BigInteger.class.equals(clazz)) { |
| return new BigInteger(value.toString()); |
| } else |
| throw new JSONException("Incompatible types for property " + method.getName()); |
| } |
| |
| private static boolean isJSONPrimitive(Class clazz) { |
| return clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Date.class) |
| || clazz.equals(Boolean.class) || clazz.equals(Byte.class) || clazz.equals(Character.class) |
| || clazz.equals(Double.class) || clazz.equals(Float.class) || clazz.equals(Integer.class) |
| || clazz.equals(Long.class) || clazz.equals(Short.class) || clazz.equals(Locale.class) |
| || clazz.isEnum(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Object convertToArray(Class clazz, Type type, Object value, Method accessor) |
| throws JSONException, IllegalArgumentException, IllegalAccessException, |
| InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException { |
| if (value == null) |
| return null; |
| else if (value instanceof List) { |
| Class arrayType = clazz.getComponentType(); |
| List values = (List) value; |
| Object newArray = Array.newInstance(arrayType, values.size()); |
| |
| // create an object for each element |
| for (int j = 0; j < values.size(); j++) { |
| Object listValue = values.get(j); |
| |
| if (arrayType.equals(Object.class)) { |
| // Object[] |
| Array.set(newArray, j, listValue); |
| } else if (isJSONPrimitive(arrayType)) { |
| // primitive array |
| Array.set(newArray, j, this.convertPrimitive(arrayType, listValue, accessor)); |
| } else if (listValue instanceof Map) { |
| // array of other class |
| Object newObject; |
| if (Map.class.isAssignableFrom(arrayType)) { |
| newObject = convertToMap(arrayType, type, listValue, accessor); |
| } else if (List.class.isAssignableFrom(arrayType)) { |
| newObject = convertToCollection(arrayType, type, listValue, accessor); |
| } else { |
| newObject = arrayType.newInstance(); |
| this.populateObject(newObject, (Map) listValue); |
| } |
| |
| Array.set(newArray, j, newObject); |
| } else |
| throw new JSONException("Incompatible types for property " + accessor.getName()); |
| } |
| |
| return newArray; |
| } else |
| throw new JSONException("Incompatible types for property " + accessor.getName()); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Object convertToCollection(Class clazz, Type type, Object value, Method accessor) |
| throws JSONException, IllegalArgumentException, IllegalAccessException, |
| InvocationTargetException, InstantiationException, NoSuchMethodException, IntrospectionException { |
| if (value == null) |
| return null; |
| else if (value instanceof List) { |
| Class itemClass = Object.class; |
| Type itemType = null; |
| if ((type != null) && (type instanceof ParameterizedType)) { |
| ParameterizedType ptype = (ParameterizedType) type; |
| itemType = ptype.getActualTypeArguments()[0]; |
| if (itemType.getClass().equals(Class.class)) { |
| itemClass = (Class) itemType; |
| } else { |
| itemClass = (Class) ((ParameterizedType) itemType).getRawType(); |
| } |
| } |
| List values = (List) value; |
| |
| Collection newCollection = null; |
| try { |
| newCollection = (Collection) clazz.newInstance(); |
| } catch (InstantiationException ex) { |
| // fallback if clazz represents an interface or abstract class |
| if (SortedSet.class.isAssignableFrom(clazz)) { |
| newCollection = new TreeSet(); |
| } else if (Set.class.isAssignableFrom(clazz)) { |
| newCollection = new HashSet(); |
| } else if (Queue.class.isAssignableFrom(clazz)) { |
| newCollection = new ArrayDeque(); |
| } else { |
| newCollection = new ArrayList(); |
| } |
| } |
| |
| // create an object for each element |
| for (Object listValue : values) { |
| |
| if (itemClass.equals(Object.class)) { |
| // Object[] |
| newCollection.add(listValue); |
| } else if (isJSONPrimitive(itemClass)) { |
| // primitive array |
| newCollection.add(this.convertPrimitive(itemClass, listValue, accessor)); |
| } else if (Map.class.isAssignableFrom(itemClass)) { |
| Object newObject = convertToMap(itemClass, itemType, listValue, accessor); |
| newCollection.add(newObject); |
| } else if (List.class.isAssignableFrom(itemClass)) { |
| Object newObject = convertToCollection(itemClass, itemType, listValue, accessor); |
| newCollection.add(newObject); |
| } else if (listValue instanceof Map) { |
| // array of beans |
| Object newObject = itemClass.newInstance(); |
| this.populateObject(newObject, (Map) listValue); |
| newCollection.add(newObject); |
| } else |
| throw new JSONException("Incompatible types for property " + accessor.getName()); |
| } |
| |
| return newCollection; |
| } else |
| throw new JSONException("Incompatible types for property " + accessor.getName()); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Object convertToMap(Class clazz, Type type, Object value, Method accessor) throws JSONException, |
| IllegalArgumentException, IllegalAccessException, InvocationTargetException, |
| InstantiationException, NoSuchMethodException, IntrospectionException { |
| if (value == null) |
| return null; |
| else if (value instanceof Map) { |
| Class itemClass = Object.class; |
| Type itemType = null; |
| if ((type != null) && (type instanceof ParameterizedType)) { |
| ParameterizedType ptype = (ParameterizedType) type; |
| itemType = ptype.getActualTypeArguments()[1]; |
| if (itemType.getClass().equals(Class.class)) { |
| itemClass = (Class) itemType; |
| } else { |
| itemClass = (Class) ((ParameterizedType) itemType).getRawType(); |
| } |
| } |
| Map values = (Map) value; |
| |
| Map newMap; |
| try { |
| newMap = (Map) clazz.newInstance(); |
| } catch (InstantiationException ex) { |
| // fallback if clazz represents an interface or abstract class |
| newMap = new HashMap(); |
| } |
| |
| // create an object for each element |
| for (Object next : values.entrySet()) { |
| Map.Entry entry = (Map.Entry) next; |
| String key = (String) entry.getKey(); |
| Object v = entry.getValue(); |
| |
| if (itemClass.equals(Object.class)) { |
| // String, Object |
| newMap.put(key, v); |
| } else if (isJSONPrimitive(itemClass)) { |
| // primitive map |
| newMap.put(key, this.convertPrimitive(itemClass, v, accessor)); |
| } else if (Map.class.isAssignableFrom(itemClass)) { |
| Object newObject = convertToMap(itemClass, itemType, v, accessor); |
| newMap.put(key, newObject); |
| } else if (List.class.isAssignableFrom(itemClass)) { |
| Object newObject = convertToCollection(itemClass, itemType, v, accessor); |
| newMap.put(key, newObject); |
| } else if (v instanceof Map) { |
| // map of beans |
| Object newObject = itemClass.newInstance(); |
| this.populateObject(newObject, (Map) v); |
| newMap.put(key, newObject); |
| } else |
| throw new JSONException("Incompatible types for property " + accessor.getName()); |
| } |
| |
| return newMap; |
| } else |
| throw new JSONException("Incompatible types for property " + accessor.getName()); |
| } |
| |
| /** |
| * Converts numbers to the desired class, if possible |
| * |
| * @throws JSONException |
| */ |
| @SuppressWarnings("unchecked") |
| private Object convertPrimitive(Class clazz, Object value, Method method) throws JSONException { |
| if (value == null) { |
| if (Short.TYPE.equals(clazz) || Short.class.equals(clazz)) |
| return (short) 0; |
| else if (Byte.TYPE.equals(clazz) || Byte.class.equals(clazz)) |
| return (byte) 0; |
| else if (Integer.TYPE.equals(clazz) || Integer.class.equals(clazz)) |
| return 0; |
| else if (Long.TYPE.equals(clazz) || Long.class.equals(clazz)) |
| return 0L; |
| else if (Float.TYPE.equals(clazz) || Float.class.equals(clazz)) |
| return 0f; |
| else if (Double.TYPE.equals(clazz) || Double.class.equals(clazz)) |
| return 0d; |
| else if (Boolean.TYPE.equals(clazz) || Boolean.class.equals(clazz)) |
| return Boolean.FALSE; |
| else |
| return null; |
| } else if (value instanceof Number) { |
| Number number = (Number) value; |
| |
| if (Short.TYPE.equals(clazz)) |
| return number.shortValue(); |
| else if (Short.class.equals(clazz)) |
| return number.shortValue(); |
| else if (Byte.TYPE.equals(clazz)) |
| return number.byteValue(); |
| else if (Byte.class.equals(clazz)) |
| return number.byteValue(); |
| else if (Integer.TYPE.equals(clazz)) |
| return number.intValue(); |
| else if (Integer.class.equals(clazz)) |
| return number.intValue(); |
| else if (Long.TYPE.equals(clazz)) |
| return number.longValue(); |
| else if (Long.class.equals(clazz)) |
| return number.longValue(); |
| else if (Float.TYPE.equals(clazz)) |
| return number.floatValue(); |
| else if (Float.class.equals(clazz)) |
| return number.floatValue(); |
| else if (Double.TYPE.equals(clazz)) |
| return number.doubleValue(); |
| else if (Double.class.equals(clazz)) |
| return number.doubleValue(); |
| else if (String.class.equals(clazz)) |
| return value.toString(); |
| } else if (clazz.equals(Date.class)) { |
| try { |
| JSON json = method.getAnnotation(JSON.class); |
| |
| DateFormat formatter = new SimpleDateFormat( |
| (json != null) && (json.format().length() > 0) ? json.format() : this.dateFormat); |
| return formatter.parse((String) value); |
| } catch (ParseException e) { |
| LOG.error("Unable to parse date from: {}", value, e); |
| throw new JSONException("Unable to parse date from: " + value); |
| } |
| } else if (clazz.isEnum()) { |
| String sValue = (String) value; |
| return Enum.valueOf(clazz, sValue); |
| } else if (value instanceof String) { |
| String sValue = (String) value; |
| if (Boolean.TYPE.equals(clazz)) |
| return Boolean.parseBoolean(sValue); |
| else if (Boolean.class.equals(clazz)) |
| return Boolean.valueOf(sValue); |
| else if (Short.TYPE.equals(clazz)) |
| return Short.parseShort(sValue); |
| else if (Short.class.equals(clazz)) |
| return Short.valueOf(sValue); |
| else if (Byte.TYPE.equals(clazz)) |
| return Byte.parseByte(sValue); |
| else if (Byte.class.equals(clazz)) |
| return Byte.valueOf(sValue); |
| else if (Integer.TYPE.equals(clazz)) |
| return Integer.parseInt(sValue); |
| else if (Integer.class.equals(clazz)) |
| return Integer.valueOf(sValue); |
| else if (Long.TYPE.equals(clazz)) |
| return Long.parseLong(sValue); |
| else if (Long.class.equals(clazz)) |
| return Long.valueOf(sValue); |
| else if (Float.TYPE.equals(clazz)) |
| return Float.parseFloat(sValue); |
| else if (Float.class.equals(clazz)) |
| return Float.valueOf(sValue); |
| else if (Double.TYPE.equals(clazz)) |
| return Double.parseDouble(sValue); |
| else if (Double.class.equals(clazz)) |
| return Double.valueOf(sValue); |
| else if (Character.TYPE.equals(clazz) || Character.class.equals(clazz)) { |
| char charValue = 0; |
| if (sValue.length() > 0) { |
| charValue = sValue.charAt(0); |
| } |
| if (Character.TYPE.equals(clazz)) |
| return charValue; |
| else |
| return charValue; |
| } else if (clazz.equals(Locale.class)) { |
| String[] components = sValue.split("_", 2); |
| if (components.length == 2) { |
| return new Locale(components[0], components[1]); |
| } else { |
| return new Locale(sValue); |
| } |
| } else if (Enum.class.isAssignableFrom(clazz)) { |
| return Enum.valueOf(clazz, sValue); |
| } |
| } |
| |
| return value; |
| } |
| |
| } |