All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoftware.io.ObjectResolver Maven / Gradle / Ivy

There is a newer version: 4.30.0
Show newest version
package com.cedarsoftware.io;

import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.cedarsoftware.io.reflect.Injector;
import com.cedarsoftware.util.ClassUtilities;
import com.cedarsoftware.util.convert.Converter;

/**
 * 

The ObjectResolver converts the raw Maps created from the JsonParser to Java * objects (a graph of Java instances). The Maps have an optional type entry associated * to them to indicate what Java peer instance to create. The reason type is optional * is because it can be inferred in a couple instances. A non-primitive field that * points to an object that is of the same type of the field, does not require the * '@type' because it can be inferred from the field. This is not always the case. * For example, if a Person field points to an Employee object (where Employee is a * subclass of Person), then the resolver cannot create an instance of the field type * (Person) because this is not the proper type. (It had an Employee record with more * fields in this example). In this case, the writer recognizes that the instance type * and field type are not the same and therefore it writes the @type. *

* A similar case as above occurs with specific array types. If there is a Person[] * containing Person and Employee instances, then the Person instances will not have * the '@type' but the employee instances will (because they are more derived than Person). *

* The resolver 'wires' the original object graph. It does this by replacing * '@ref' values in the Maps with pointers (on the field of the associated instance of the * Map) to the object that has the same ID. If the object has not yet been read, then * an UnresolvedReference is created. These are back-patched at the end of the resolution * process. UnresolvedReference keeps track of what field or array element the actual value * should be stored within, and then locates the object (by id), and updates the appropriate * value. *

* @author John DeRegnaucourt ([email protected]) *
* Copyright (c) Cedar Software LLC *

* Licensed 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 *

* License *

* 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. */ @SuppressWarnings({ "rawtypes", "unchecked", "Convert2Diamond" }) public class ObjectResolver extends Resolver { private final ClassLoader classLoader; /** * Constructor * @param readOptions Options to use while reading. */ protected ObjectResolver(ReadOptions readOptions, ReferenceTracker references, Converter converter) { super(readOptions, references, converter); classLoader = readOptions.getClassLoader(); } /** * Walk the Java object fields and copy them from the JSON object to the Java object, performing * any necessary conversions on primitives, or deep traversals for field assignments to other objects, * arrays, Collections, or Maps. * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields). */ public void traverseFields(final JsonObject jsonObj) { final Object javaMate = jsonObj.getTarget(); final Iterator> i = jsonObj.entrySet().iterator(); final Class cls = javaMate.getClass(); ReadOptions readOptions = getReadOptions(); final Map injectorMap = readOptions.getDeepInjectorMap(cls); while (i.hasNext()) { Map.Entry e = i.next(); String key = (String) e.getKey(); final Injector injector = injectorMap.get(key); Object rhs = e.getValue(); if (injector != null) { assignField(jsonObj, injector, rhs); } else if (readOptions.getMissingFieldHandler() != null) { handleMissingField(jsonObj, rhs, key); } //else no handler so ignore. } } /** * Map Json Map object field to Java object field. * * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields). * @param injector instance of injector used for setting values on the object. * @param rhs the JSON value that will be converted and stored in the 'field' on the associated * Java target object. */ public void assignField(final JsonObject jsonObj, final Injector injector, final Object rhs) { final Object target = jsonObj.getTarget(); final Class fieldType = injector.getType(); if (rhs == null) { // Logically clear field if (fieldType.isPrimitive()) { injector.inject(target, getConverter().convert(null, fieldType)); } else { injector.inject(target, null); } return; } // If there is a "tree" of objects (e.g, Map>), the sub-objects may not have a // @type on them, if the source of the JSON is from JSON.stringify(). Deep traverse the args and // mark @type on the items within the Maps and Collections, based on the parameterized type (if it // exists). if (rhs instanceof JsonObject) { if (injector.getGenericType() instanceof ParameterizedType) { // Only JsonObject instances could contain unmarked objects. markUntypedObjects(injector.getGenericType(), rhs, fieldType); } // Ensure 'type' field set on JsonObject final JsonObject jObj = (JsonObject) rhs; if (jObj.getJavaType() == null) { jObj.setJavaType(fieldType); } } Object special; if ((special = readWithFactoryIfExists(rhs, fieldType)) != null) { injector.inject(target, special); } else if (rhs.getClass().isArray()) { // LHS of assignment is an [] field or RHS is an array and LHS is Object final Object[] elements = (Object[]) rhs; JsonObject jsonArray = new JsonObject(); jsonArray.setJsonArray(elements); jsonArray.setHintType(fieldType); createInstance(jsonArray); injector.inject(target, jsonArray.getTarget()); push(jsonArray); } else if (rhs instanceof JsonObject) { final JsonObject jsRhs = (JsonObject) rhs; final Long ref = jsRhs.getReferenceId(); if (ref != null) { // Correct field references final JsonObject refObject = getReferences().get(ref); if (refObject.getTarget() != null) { injector.inject(target, refObject.getTarget()); } else { unresolvedRefs.add(new UnresolvedReference(jsonObj, injector.getName(), ref)); } } else { // Assign ObjectMap's to Object (or derived) fields Object fieldObject = jsRhs.getTarget(); injector.inject(target, fieldObject); boolean isNonRefClass = getReadOptions().isNonReferenceableClass(jsRhs.getJavaType()); if (!isNonRefClass) { // if Object is a reference-able class, it must be processed (pushed on the stack) push(jsRhs); } } } else { if (rhs instanceof String && ((String) rhs).trim().isEmpty() && fieldType != String.class) { // Allow "" to null out a non-String field injector.inject(target, null); } else { injector.inject(target, rhs); } } } /** * Try to create a java object from the missing field. * Mostly primitive types and jsonObject that contains @type attribute will * be candidate for the missing field callback, others will be ignored. * All missing field are stored for later notification * * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields). * @param rhs the JSON value that will be converted and stored in the 'field' on the associated Java target object. * @param missingField name of the missing field in the java object. */ protected void handleMissingField(final JsonObject jsonObj, final Object rhs, final String missingField) { final Object target = jsonObj.getTarget(); try { if (rhs == null) { // Logically clear field (allows null to be set against primitive fields, yielding their zero value. storeMissingField(target, missingField, null); return; } // we have a jsonObject with a type Object special; if ((special = readWithFactoryIfExists(rhs, null)) != null) { storeMissingField(target, missingField, special); } else if (rhs.getClass().isArray()) { // impossible to determine the array type. storeMissingField(target, missingField, null); } else if (rhs instanceof JsonObject) { final JsonObject jObj = (JsonObject) rhs; final Long ref = jObj.getReferenceId(); if (ref != null) { // Correct field references final JsonObject refObject = getReferences().get(ref); storeMissingField(target, missingField, refObject.getTarget()); } else { // Assign ObjectMap's to Object (or derived) fields // check that jObj as a type if (jObj.getJavaType() != null) { Object javaInstance = createInstance(jObj); boolean isNonRefClass = getReadOptions().isNonReferenceableClass(jObj.getJavaType()); // TODO: Check is finished here? if (!isNonRefClass && !jObj.isFinished) { push((JsonObject) rhs); } storeMissingField(target, missingField, javaInstance); } else { //no type found, just notify. storeMissingField(target, missingField, null); } } } else { storeMissingField(target, missingField, rhs); } } catch (Exception e) { if (e instanceof JsonIoException) { throw e; } String message = e.getClass().getSimpleName() + " missing field '" + missingField + "' on target: " + safeToString(target) + " with value: " + rhs; throw new JsonIoException(message, e); } } /** * stores the missing field and their values to call back the handler at the end of the resolution, cause some * reference may need to be resolved later. */ private void storeMissingField(Object target, String missingField, Object value) { missingFields.add(new Missingfields(target, missingField, value)); } /** * @param o Object to turn into a String * @return .toString() version of o or "null" if o is null. */ private static String safeToString(Object o) { if (o == null) { return "null"; } try { return o.toString(); } catch (Exception e) { return o.getClass().toString(); } } /** * Process java.util.Collection and it's derivatives. Collections are written specially * so that the serialization does not expose the Collection's internal structure, for * example, a TreeSet. All entries are processed, except unresolved references, which * are filled in later. For an index-able collection, the unresolved references are set * back into the proper element location. For non-index-able collections (Sets), the * unresolved references are added via .add(). * @param jsonObj a Map-of-Map representation of the JSON input stream. */ protected void traverseCollection(final JsonObject jsonObj) { Object[] items = jsonObj.getJsonArray(); Class mayEnumClass = null; String mayEnumClasName = (String)jsonObj.get("@enum"); if (mayEnumClasName != null) { mayEnumClass = ClassUtilities.forName(mayEnumClasName, classLoader); } final Collection col = (Collection) jsonObj.getTarget(); final boolean isList = col instanceof List; int idx = 0; if (items != null) { for (final Object element : items) { Object special; if (element == null) { col.add(null); } else if ((special = readWithFactoryIfExists(element, null)) != null) { col.add(special); } else if (element instanceof String || element instanceof Boolean || element instanceof Double || element instanceof Long) { // Allow Strings, Booleans, Longs, and Doubles to be "inline" without Java object decoration (@id, @type, etc.) col.add(mayEnumClass == null ? element : Enum.valueOf(mayEnumClass, (String) element)); } else if (element.getClass().isArray()) { final JsonObject jObj = new JsonObject(); jObj.setHintType(Object.class); jObj.setJsonArray((Object[]) element); createInstance(jObj); col.add(jObj.getTarget()); push(jObj); } else { // if (element instanceof JsonObject) final JsonObject jObj = (JsonObject) element; final Long ref = jObj.getReferenceId(); if (ref != null) { JsonObject refObject = getReferences().get(ref); if (refObject.getTarget() != null) { col.add(refObject.getTarget()); } else { unresolvedRefs.add(new UnresolvedReference(jsonObj, idx, ref)); if (isList) { // Index-able collection, so set 'null' as element for now - will be patched in later. col.add(null); } } } else { jObj.setHintType(Object.class); createInstance(jObj); boolean isNonRefClass = getReadOptions().isNonReferenceableClass(jObj.getJavaType()); if (!isNonRefClass) { traverseSpecificType(jObj); } if (!(col instanceof EnumSet)) { // EnumSet has already had it's items added to it. col.add(jObj.getTarget()); } } } idx++; } } jsonObj.clear(); // Reduce memory required during processing } /** * Traverse the JsonObject associated to an array (of any type). Convert and * assign the list of items in the JsonObject (stored in the @items field) * to each array element. All array elements are processed excluding elements * that reference an unresolved object. These are filled in later. * * @param jsonObj a Map-of-Map representation of the JSON input stream. */ protected void traverseArray(final JsonObject jsonObj) { final int len = jsonObj.getLength(); if (len == 0) { return; } final Object array = jsonObj.getTarget(); final Class compType = array.getClass().getComponentType(); final Object[] jsonItems = jsonObj.getJsonArray(); // Primitive arrays never make it here, as the ArrayFactory (ClassFactory) processes them in assignField. for (int i = 0; i < len; i++) { final Object element = jsonItems[i]; Object special; if (element == null) { Array.set(array, i, null); } else if ((special = readWithFactoryIfExists(element, compType)) != null) { if (compType.isEnum() && special instanceof String) { special = Enum.valueOf(compType, (String) special); } Array.set(array, i, special); } else if (element.getClass().isArray()) { // Array of arrays if (char[].class == compType) { // Specially handle char[] because we are writing these // out as UTF-8 strings for compactness and speed. Object[] jsonArray = (Object[]) element; if (jsonArray.length == 0) { Array.set(array, i, new char[]{}); } else { final String value = (String) jsonArray[0]; final int numChars = value.length(); final char[] chars = new char[numChars]; for (int j = 0; j < numChars; j++) { chars[j] = value.charAt(j); } Array.set(array, i, chars); } } else { // Prep a JsonObject for array[i] JsonObject jsonArray = new JsonObject(); jsonArray.setJsonArray((Object[]) element); jsonArray.setHintType(compType); // create and set it into enclosing array Array.set(array, i, createInstance(jsonArray)); // Enclosing [i] assigned to [] // traverse items within this array - if non-primitive push(jsonArray); } } else if (element instanceof JsonObject) { JsonObject jsonElement = (JsonObject) element; Long ref = jsonElement.getReferenceId(); if (ref != null) { // Connect reference JsonObject refObject = getReferences().get(ref); if (refObject.getTarget() != null) { // Array element with reference to existing object Array.set(array, i, refObject.getTarget()); } else { // Array with a forward reference as an element unresolvedRefs.add(new UnresolvedReference(jsonObj, i, ref)); } } else { // Convert JSON HashMap to Java Object instance and assign values jsonElement.setHintType(compType); Object arrayElement = createInstance(jsonElement); Array.set(array, i, arrayElement); boolean isNonRefClass = getReadOptions().isNonReferenceableClass(arrayElement.getClass()); if (!isNonRefClass && !jsonElement.isFinished) { // Skip walking primitives and completed objects. push(jsonElement); } } } else { if (element instanceof String && ((String) element).trim().isEmpty() && compType != String.class && compType != Object.class) { // Allow an entry of "" in the array to set the array element to null, *if* the array type is NOT String[] and NOT Object[] Array.set(array, i, null); } else { Array.set(array, i, element); } } } jsonObj.clear(); } /** * Convert the passed in object (o) to a proper Java object. If the passed in object (o) has a custom reader * associated to it, then have it convert the object. If there is no custom reader, then return null. * @param o Object to read (convert). Will be either a JsonObject or a JSON primitive String, long, boolean, * double, or null. * @param inferredType Class to which 'o' should be converted to. * @return Java object converted from the passed in object o, or if there is no custom reader. */ protected Object readWithFactoryIfExists(final Object o, final Class inferredType) { if (o == null) { throw new JsonIoException("Bug in json-io, null must be checked before calling this method."); } ReadOptions readOptions = getReadOptions(); if (inferredType != null && readOptions.isNotCustomReaderClass(inferredType)) { return null; } final boolean isJsonObject = o instanceof JsonObject; if (!isJsonObject && inferredType == null) { // If not a JsonObject (like a Long that represents a date, then compType must be set) return null; } JsonObject jsonObj; Class c; // Set up class type to check against reader classes (specified as @type, or jObj.target, or compType) if (isJsonObject) { jsonObj = (JsonObject) o; if (jsonObj.isReference()) { // No factory/customer reader for an @ref return null; } if (jsonObj.getTarget() == null) { // '@type' parameter used (not target instance) c = jsonObj.getJavaType(); if (c == null || inferredType == null) { return null; } jsonObj.setHintType(c); Object factoryCreated = createInstance(jsonObj); if (factoryCreated != null && jsonObj.isFinished) { return factoryCreated; } } else { // Type inferred from target object c = jsonObj.getJavaType(); } } else { c = inferredType.equals(Object.class) ? o.getClass() : inferredType; jsonObj = new JsonObject(); jsonObj.setValue(o); jsonObj.setHintType(c); } if (readOptions.isNotCustomReaderClass(c)) { // Explicitly instructed not to use a custom reader for this class. return null; } if (jsonObj.getTarget() == null) { if (jsonObj.hasValue()) { if (getConverter().isConversionSupportedFor(jsonObj.getValue().getClass(), c)) { Object target = getConverter().convert(jsonObj.getValue(), c); return jsonObj.setFinishedTarget(target, true); } } } // from here on out it is assumed you have json object. // Use custom classFactory if one exists and target hasn't already been created. JsonReader.ClassFactory classFactory = readOptions.getClassFactory(c); if (classFactory != null && jsonObj.getTarget() == null) { Object target = createInstanceUsingClassFactory(c, jsonObj); if (jsonObj.isFinished()) { return target; } } // Use custom reader if one exists JsonReader.JsonClassReader closestReader = readOptions.getCustomReader(c); if (closestReader == null) { return null; } Object read = closestReader.read(o, this); if (read == null) { return null; } // Fixes Issue #17 from GitHub. Make sure to place a pointer to the custom read object on the JsonObject. // This way, references to it will be pointed back to the correct instance. return jsonObj.setFinishedTarget(read, true); } private void markUntypedObjects(final Type type, final Object rhs, final Class fieldType) { final Deque stack2 = new ArrayDeque<>(); stack2.addFirst(new Object[]{type, rhs}); Map classFields = getReadOptions().getDeepInjectorMap(fieldType); while (!stack2.isEmpty()) { Object[] item = stack2.removeFirst(); final Type t = (Type) item[0]; final Object instance = item[1]; if (t instanceof ParameterizedType) { Class clazz = getRawType(t); ParameterizedType pType = (ParameterizedType) t; Type[] typeArgs = pType.getActualTypeArguments(); if (typeArgs == null || typeArgs.length < 1 || clazz == null) { continue; } stampTypeOnJsonObject(instance, t); if (Map.class.isAssignableFrom(clazz)) { JsonObject jsonObj = (JsonObject) instance; // Maps are brought in as JsonObjects Map.Entry pair = jsonObj.asTwoArrays(); Object[] keys = pair.getKey(); Object[] items = pair.getValue(); getTemplateTraverseWorkItem(stack2, keys, typeArgs[0]); getTemplateTraverseWorkItem(stack2, items, typeArgs[1]); } else if (Collection.class.isAssignableFrom(clazz)) { if (instance instanceof Object[]) { Object[] array = (Object[]) instance; for (int i = 0; i < array.length; i++) { Object vals = array[i]; stack2.addFirst(new Object[]{t, vals}); if (vals instanceof JsonObject) { stack2.addFirst(new Object[]{t, vals}); } else if (vals instanceof Object[]) { JsonObject coll = new JsonObject(); coll.setJavaType(clazz); coll.setJsonArray((Object[]) vals); List items = Arrays.asList((Object[]) vals); stack2.addFirst(new Object[]{t, items}); array[i] = coll; } else { stack2.addFirst(new Object[]{t, vals}); } } } else if (instance instanceof Collection) { final Collection col = (Collection) instance; for (Object o : col) { stack2.addFirst(new Object[]{typeArgs[0], o}); } } else if (instance instanceof JsonObject) { final JsonObject jObj = (JsonObject) instance; final Object[] array = jObj.getJsonArray(); if (array != null) { for (Object o : array) { stack2.addFirst(new Object[]{typeArgs[0], o}); } } } } else { if (instance instanceof JsonObject) { final JsonObject jObj = (JsonObject) instance; for (Map.Entry entry : jObj.entrySet()) { final String fieldName = (String) entry.getKey(); if (!fieldName.startsWith("this$")) { // TODO: If more than one type, need to associate correct typeArgs entry to value Injector injector = classFields.get(fieldName); if (injector != null && (injector.getType().getTypeParameters().length > 0 || injector.getGenericType() instanceof TypeVariable)) { Object pt = typeArgs[0]; if (entry.getValue() instanceof JsonObject && ((JsonObject) entry.getValue()).get("@enum") != null) { pt = injector.getGenericType(); } stack2.addFirst(new Object[]{pt, entry.getValue()}); } } } } } } else { stampTypeOnJsonObject(instance, t); } } } private static void getTemplateTraverseWorkItem(final Deque stack2, final Object[] items, final Type type) { if (items == null || items.length < 1) { return; } Class rawType = getRawType(type); if (rawType != null && Collection.class.isAssignableFrom(rawType)) { stack2.add(new Object[]{type, items}); } else { for (Object o : items) { stack2.add(new Object[]{type, o}); } } } // Mark 'type' on JsonObject when the type is missing and it is a 'leaf' // node (no further subtypes in it's parameterized type definition) private static void stampTypeOnJsonObject(final Object o, final Type t) { Class clazz = t instanceof Class ? (Class) t : getRawType(t); if (o instanceof JsonObject && clazz != null) { JsonObject jObj = (JsonObject) o; if (jObj.getJavaType() == null && jObj.getTarget() == null) { jObj.setJavaType(clazz); } } } /** * Given the passed in Type t, return the raw type of it, if the passed in value is a ParameterizedType. * @param t Type to attempt to get raw type from. * @return Raw type obtained from the passed in parameterized type or null if T is not a ParameterizedType */ private static Class getRawType(final Type t) { if (t instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) t; if (pType.getRawType() instanceof Class) { return (Class) pType.getRawType(); } } return null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy