/******************************************************************************* * Copyright (c) 2012-2016 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.everrest.core.impl.provider.json; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import org.everrest.core.impl.provider.json.JsonUtils.Types; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Throwables.propagateIfPossible; import static com.google.common.collect.Sets.newHashSet; import static java.util.concurrent.TimeUnit.MINUTES; import static org.everrest.core.impl.provider.json.JsonUtils.Types.ARRAY_OBJECT; import static org.everrest.core.impl.provider.json.JsonUtils.Types.COLLECTION; import static org.everrest.core.impl.provider.json.JsonUtils.Types.ENUM; import static org.everrest.core.impl.provider.json.JsonUtils.Types.MAP; import static org.everrest.core.impl.provider.json.JsonUtils.createProxy; import static org.everrest.core.impl.provider.json.JsonUtils.getFieldName; import static org.everrest.core.impl.provider.json.JsonUtils.getTransientFields; import static org.everrest.core.impl.provider.json.JsonUtils.getType; import static org.everrest.core.impl.provider.json.JsonUtils.isKnownType; /** @author andrew00x */ public class ObjectBuilder { private static final Collection<String> SKIP_METHODS = newHashSet("setMetaClass"); private static LoadingCache<Class<?>, JsonMethod[]> methodsCache = CacheBuilder.newBuilder() .concurrencyLevel(8) .maximumSize(256) .expireAfterAccess(10, MINUTES) .build(new CacheLoader<Class<?>, JsonMethod[]>() { @Override public JsonMethod[] load(Class<?> aClass) throws Exception { return getJsonMethods(aClass); } }); private static Cache<Class<?>, Constructor<?>> constructorsCache = CacheBuilder.newBuilder() .concurrencyLevel(8) .maximumSize(256) .expireAfterAccess(10, MINUTES) .build(); private static JsonMethod[] getJsonMethods(Class<?> clazz) { Set<String> transientFieldNames = getTransientFields(clazz); List<JsonMethod> result = new ArrayList<>(); for (Method method : clazz.getMethods()) { if (shouldBeProcessed(method)) { String field = getFieldName(method); if (!transientFieldNames.contains(field)) { result.add(new JsonMethod(method, field)); } } } return result.toArray(new JsonMethod[result.size()]); } private static boolean shouldBeProcessed(Method method) { return !SKIP_METHODS.contains(method.getName()) && isSetter(method); } private static boolean isSetter(Method method) { String methodName = method.getName(); return methodName.startsWith("set") && methodName.length() > 3 && method.getParameterTypes().length == 1; } /* ------------------------------------------------------------------------------ */ /** * Create array of Java Object from JSON source include multi-dimension * array. * * @param aClass * the Class of target Object. * @param jsonArray * the JSON representation of array * @return result array * @throws JsonException * if any errors occurs */ public static Object createArray(Class<?> aClass, JsonValue jsonArray) throws JsonException { if (jsonArray == null || jsonArray.isNull()) { return null; } Class<?> componentType = aClass.getComponentType(); Object array = Array.newInstance(componentType, jsonArray.size()); Iterator<JsonValue> values = jsonArray.getElements(); int i = 0; if (componentType.isArray()) { if (isKnownType(componentType)) { while (values.hasNext()) { Array.set(array, i++, createObjectKnownTypes(componentType, values.next())); } } else { while (values.hasNext()) { Array.set(array, i++, createArray(componentType, values.next())); } } } else { if (isKnownType(componentType)) { while (values.hasNext()) { Array.set(array, i++, createObjectKnownTypes(componentType, values.next())); } } else { while (values.hasNext()) { Array.set(array, i++, createObject(componentType, values.next())); } } } return array; } /** * Create instance of <code>collectionClass</code> from JSON representation. * If <code>collectionClass</code> is interface then appropriate * implementation of interface will be returned. * * @param collectionClass * collection type * @param genericType * generic type of collection * @param jsonArray * the JSON representation of collection * @return result collection * @throws JsonException * if any errors occurs */ @SuppressWarnings("unchecked") public static <T extends Collection<?>> T createCollection(Class<T> collectionClass, Type genericType, JsonValue jsonArray) throws JsonException { if (jsonArray == null || jsonArray.isNull()) { return null; } Class elementClass; Type elementType; if (genericType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType)genericType; elementType = parameterizedType.getActualTypeArguments()[0]; if (elementType instanceof Class) { elementClass = (Class)elementType; } else if (elementType instanceof ParameterizedType) { elementClass = (Class)((ParameterizedType)elementType).getRawType(); } else { throw new JsonException(String.format( "This type of Collection can't be restored from JSON source.\nCollection is parameterized by wrong Type: %s", parameterizedType)); } } else { throw new JsonException("Collection is not parameterized. Collection<?> is not supported"); } Constructor<? extends T> constructor; if (collectionClass.isInterface() || Modifier.isAbstract(collectionClass.getModifiers())) { constructor = getConstructor(findAcceptableCollectionImplementation(collectionClass), Collection.class); } else { constructor = getConstructor(collectionClass, Collection.class); } ArrayList<Object> sourceCollection = new ArrayList<>(jsonArray.size()); Iterator<JsonValue> values = jsonArray.getElements(); Types jsonElementType = getType(elementClass); while (values.hasNext()) { JsonValue value = values.next(); if (jsonElementType == null) { sourceCollection.add(createObject(elementClass, value)); } else { switch (jsonElementType) { case BYTE: case SHORT: case INT: case LONG: case FLOAT: case DOUBLE: case BOOLEAN: case CHAR: case STRING: case NULL: case ARRAY_BYTE: case ARRAY_SHORT: case ARRAY_INT: case ARRAY_LONG: case ARRAY_FLOAT: case ARRAY_DOUBLE: case ARRAY_BOOLEAN: case ARRAY_CHAR: case ARRAY_STRING: case CLASS: sourceCollection.add(createObjectKnownTypes(elementClass, value)); break; case ARRAY_OBJECT: sourceCollection.add(createArray(elementClass, value)); break; case COLLECTION: sourceCollection.add(createCollection(elementClass, elementType, value)); break; case MAP: sourceCollection.add(createObject(elementClass, elementType, value)); break; case ENUM: sourceCollection.add(createEnum(elementClass, value)); break; } } } try { return constructor.newInstance(sourceCollection); } catch (Exception e) { throw new JsonException(e.getMessage(), e); } } private static <T extends Collection<?>> Class findAcceptableCollectionImplementation(Class<T> collectionClass) throws JsonException { Class impl = null; if (collectionClass.isAssignableFrom(ArrayList.class)) { impl = ArrayList.class.asSubclass(collectionClass); } else if (collectionClass.isAssignableFrom(HashSet.class)) { impl = HashSet.class.asSubclass(collectionClass); } else if (collectionClass.isAssignableFrom(TreeSet.class)) { impl = TreeSet.class.asSubclass(collectionClass); } else if (collectionClass.isAssignableFrom(LinkedList.class)) { impl = LinkedList.class.asSubclass(collectionClass); } if (impl == null) { throw new JsonException(String.format("Can't find proper implementation for collection %s", collectionClass)); } return impl; } /** * Create instance of <code>mapClass</code> from JSON representation. If * <code>mapClass</code> is interface then appropriate implementation of * interface will be returned. * * @param mapClass * map type * @param genericType * actual type of map * @param jsonObject * source JSON object * @return map * @throws JsonException * if any errors occurs */ @SuppressWarnings("unchecked") public static <T extends Map<String, ?>> T createObject(Class<T> mapClass, Type genericType, JsonValue jsonObject) throws JsonException { if (jsonObject == null || jsonObject.isNull()) { return null; } Class mapValueClass; Type mapValueType; if (genericType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType)genericType; if (!String.class.isAssignableFrom((Class)parameterizedType.getActualTypeArguments()[0])) { throw new JsonException("Key of Map must be String. "); } mapValueType = parameterizedType.getActualTypeArguments()[1]; if (mapValueType instanceof Class) { mapValueClass = (Class)mapValueType; } else if (mapValueType instanceof ParameterizedType) { mapValueClass = (Class)((ParameterizedType)mapValueType).getRawType(); } else { throw new JsonException( String.format("This type of Map can't be restored from JSON source.\nMap is parameterized by wrong Type: %s", parameterizedType)); } } else { throw new JsonException("Map is not parameterized. Map<Sting, ?> is not supported."); } Constructor<? extends T> constructor; if (mapClass.isInterface() || Modifier.isAbstract(mapClass.getModifiers())) { constructor = getConstructor(findAcceptableMapImplementation(mapClass), Map.class); } else { constructor = getConstructor(mapClass, Map.class); } Types jsonMapValueType = getType(mapValueClass); HashMap<String, Object> sourceMap = new HashMap<>(jsonObject.size()); Iterator<String> keys = jsonObject.getKeys(); while (keys.hasNext()) { String key = keys.next(); JsonValue childJsonValue = jsonObject.getElement(key); if (jsonMapValueType == null) { sourceMap.put(key, createObject(mapValueClass, childJsonValue)); } else { switch (jsonMapValueType) { case BYTE: case SHORT: case INT: case LONG: case FLOAT: case DOUBLE: case BOOLEAN: case CHAR: case STRING: case NULL: case ARRAY_BYTE: case ARRAY_SHORT: case ARRAY_INT: case ARRAY_LONG: case ARRAY_FLOAT: case ARRAY_DOUBLE: case ARRAY_BOOLEAN: case ARRAY_CHAR: case ARRAY_STRING: case CLASS: sourceMap.put(key, createObjectKnownTypes(mapValueClass, childJsonValue)); break; case ARRAY_OBJECT: sourceMap.put(key, createArray(mapValueClass, childJsonValue)); break; case COLLECTION: sourceMap.put(key, createCollection(mapValueClass, mapValueType, childJsonValue)); break; case MAP: sourceMap.put(key, createObject(mapValueClass, mapValueType, childJsonValue)); break; case ENUM: sourceMap.put(key, createEnum(mapValueClass, childJsonValue)); break; } } } try { return constructor.newInstance(sourceMap); } catch (Exception e) { throw new JsonException(e.getMessage(), e); } } private static <T extends Map<String, ?>> Class findAcceptableMapImplementation(Class<T> mapClass) throws JsonException { Class impl = null; if (mapClass.isAssignableFrom(HashMap.class)) { impl = HashMap.class.asSubclass(mapClass); } else if (mapClass.isAssignableFrom(TreeMap.class)) { impl = TreeMap.class.asSubclass(mapClass); } else if (mapClass.isAssignableFrom(Hashtable.class)) { impl = Hashtable.class.asSubclass(mapClass); } if (impl == null) { throw new JsonException(String.format("Can't find proper implementation for map %s", mapClass)); } return impl; } /** * Create Java Bean from Json Source. * * @param aClass * the Class of target Object. * @param jsonValue * the Json representation. * @return Object. * @throws JsonException * if any errors occurs. */ @SuppressWarnings({"unchecked"}) public static <T> T createObject(Class<T> aClass, JsonValue jsonValue) throws JsonException { if (jsonValue == null || jsonValue.isNull()) { return null; } if (getType(aClass) == ENUM) { return (T)createEnum(aClass, jsonValue); } if (!jsonValue.isObject()) { throw new JsonException("Unsupported type of jsonValue. "); } T object; if (aClass.isInterface()) { object = createProxy(aClass); } else { try { object = getConstructor(aClass).newInstance(); } catch (JsonException e) { throw e; } catch (Exception e) { throw new JsonException(String.format("Unable instantiate object. %s", e.getMessage()), e); } } JsonMethod[] setters; try { setters = methodsCache.get(aClass); } catch (ExecutionException e) { propagateIfPossible(e.getCause()); throw new JsonException(e.getCause()); } for (JsonMethod setter : setters) { JsonValue childJsonValue = jsonValue.getElement(setter.field); if (childJsonValue != null) { try { final Class paramClass = setter.method.getParameterTypes()[0]; if (isKnownType(paramClass)) { setter.method.invoke(object, createObjectKnownTypes(paramClass, childJsonValue)); } else { Types parameterType = getType(paramClass); if (parameterType != null) { if (parameterType == ENUM) { setter.method.invoke(object, createEnum(paramClass, childJsonValue)); } else if (parameterType == ARRAY_OBJECT) { setter.method.invoke(object, createArray(paramClass, childJsonValue)); } else if (parameterType == COLLECTION) { setter.method.invoke(object, createCollection(paramClass, setter.method.getGenericParameterTypes()[0], childJsonValue)); } else if (parameterType == MAP) { setter.method.invoke(object, createObject(paramClass, setter.method.getGenericParameterTypes()[0], childJsonValue)); } else { throw new JsonException(String.format("Can't restore parameter of method : %s#%s from JSON source.", aClass.getName(), setter.method.getName())); } } else { setter.method.invoke(object, createObject(paramClass, childJsonValue)); } } } catch (Exception e) { String msg = String.format("Unable restore parameter via method %s#%s", aClass.getName(), setter.method.getName()); if (e instanceof JsonException) { StringBuilder msgBuilder = new StringBuilder(msg); mergeMessagesFromCausalJsonExceptions(e, msgBuilder); throw new JsonException(msgBuilder.toString(), e); } else { throw new JsonException(msg + e.toString(), e); } } } } return object; } private static void mergeMessagesFromCausalJsonExceptions(Throwable error, StringBuilder msg) { int indent = 4; do { msg.append('\n'); for (int i = 0; i < indent; i++) { msg.append(' '); } indent += 4; msg.append(error.getMessage()); error = error.getCause(); } while (error instanceof JsonException); } @SuppressWarnings("unchecked") private static <T> Constructor<T> getConstructor(Class<T> aClass, Class<?>... parameters) throws JsonException { try { return (Constructor<T>)constructorsCache.get(aClass, (Callable<Constructor<T>>)() -> { try { return aClass.getConstructor(parameters); } catch (NoSuchMethodException e) { throw new JsonException(String.format("Can't find satisfied constructor for : %s", aClass)); } }); } catch (ExecutionException e) { propagateIfPossible(e.getCause(), JsonException.class); throw new JsonException(e.getCause()); } } @SuppressWarnings("unchecked") private static Enum<?> createEnum(Class enumClass, JsonValue jsonValue) { String name = jsonValue.getStringValue(); if (isNullOrEmpty(name)) { return null; } return Enum.valueOf(enumClass, name); } /** * Create Objects of known types. * * @param aClass * class. * @param jsonValue * JsonValue , @see {@link JsonValue} * @return Object. * @throws JsonException * if type is unknown. */ private static Object createObjectKnownTypes(Class<?> aClass, JsonValue jsonValue) throws JsonException { switch (getType(aClass)) { case NULL: return null; case BOOLEAN: return jsonValue.getBooleanValue(); case BYTE: return jsonValue.getByteValue(); case SHORT: return jsonValue.getShortValue(); case INT: return jsonValue.getIntValue(); case LONG: return jsonValue.getLongValue(); case FLOAT: return jsonValue.getFloatValue(); case DOUBLE: return jsonValue.getDoubleValue(); case CHAR: return jsonValue.getStringValue().charAt(0); case STRING: return jsonValue.getStringValue(); case CLASS: try { return Class.forName(jsonValue.getStringValue()); } catch (ClassNotFoundException e) { return null; } case ARRAY_BOOLEAN: { boolean[] params = new boolean[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getBooleanValue(); } return params; } case ARRAY_BYTE: { byte[] params = new byte[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getByteValue(); } return params; } case ARRAY_SHORT: { short[] params = new short[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getShortValue(); } return params; } case ARRAY_INT: { int[] params = new int[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getIntValue(); } return params; } case ARRAY_LONG: { long[] params = new long[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getLongValue(); } return params; } case ARRAY_FLOAT: { float[] params = new float[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getFloatValue(); } return params; } case ARRAY_DOUBLE: { double[] params = new double[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getDoubleValue(); } return params; } case ARRAY_CHAR: { char[] params = new char[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getStringValue().charAt(0); } return params; } case ARRAY_STRING: { String[] params = new String[jsonValue.size()]; Iterator<JsonValue> values = jsonValue.getElements(); int i = 0; while (values.hasNext()) { params[i++] = values.next().getStringValue(); } return params; } } throw new JsonException(String.format("Unknown type %s", aClass.getName())); } private ObjectBuilder() { } }