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

com.cedarsoftware.io.Resolver 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.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import static com.cedarsoftware.io.JsonObject.ITEMS;
import static com.cedarsoftware.io.JsonObject.KEYS;

/**
 * This class is used to convert a source of Java Maps that were created from
 * the JsonParser.  These are in 'raw' form with no 'pointers'.  This code will
 * reconstruct the 'shape' of the graph by connecting @ref's to @ids.
 * 

* The subclasses that override this class can build an object graph using Java * classes or a Map-of-Map representation. In both cases, the @ref value will * be replaced with the Object (or Map) that had the corresponding @id. * * @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"}) public abstract class Resolver { private static final String NO_FACTORY = "_︿_ψ_☼"; final Collection unresolvedRefs = new ArrayList<>(); private final Map visited = new IdentityHashMap<>(); protected final Deque stack = new ArrayDeque<>(); private final Collection prettyMaps = new ArrayList<>(); // store the missing field found during deserialization to notify any client after the complete resolution is done final Collection missingFields = new ArrayList<>(); private ReadOptions readOptions; private ReferenceTracker references; private Converter converter; private SealedSupplier sealedSupplier = new SealedSupplier(); private static final Set convertableValues = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "byte", "java.lang.Byte", "short", "java.lang.Short", "int", "java.lang.Integer", "java.util.concurrent.atomic.AtomicInteger", "long", "java.lang.Long", "java.util.concurrent.atomic.AtomicLong", "float", "java.lang.Float", "double", "java.lang.Double", "boolean", "java.lang.Boolean", "java.util.concurrent.atomic.AtomicBoolean", // "char", "java.lang.Character", "date", "java.util.Date", "BigInt", "java.math.BigInteger", "BigDec", "java.math.BigDecimal", "class", "java.lang.Class", "string", "java.lang.String", "java.lang.StringBuffer", "java.lang.StringBuilder", "java.sql.Date", "java.sql.Timestamp", "java.time.OffsetDateTime", "java.net.URI", "java.net.URL", "java.util.Calendar", "java.util.GregorianCalendar", "java.util.Locale", "java.util.UUID", "java.util.TimeZone", "java.time.Duration", "java.time.Instant", "java.time.MonthDay", "java.time.OffsetDateTime", "java.time.OffsetTime", "java.time.LocalDate", "java.time.LocalDateTime", "java.time.LocalTime", "java.time.Period", "java.time.Year", "java.time.YearMonth", "java.time.ZonedDateTime", "java.time.ZoneId", "java.time.ZoneOffset", "java.time.ZoneRegion", "sun.util.calendar.ZoneInfo" ))); /** * UnresolvedReference is created to hold a logical pointer to a reference that * could not yet be loaded, as the @ref appears ahead of the referenced object's * definition. This can point to a field reference or an array/Collection element reference. */ static final class UnresolvedReference { private final JsonObject referencingObj; private String field; private final long refId; private int index = -1; UnresolvedReference(JsonObject referrer, String fld, long id) { referencingObj = referrer; field = fld; refId = id; } UnresolvedReference(JsonObject referrer, int idx, long id) { referencingObj = referrer; index = idx; refId = id; } } /** * stores missing fields information to notify client after the complete deserialization resolution */ protected static class Missingfields { private final Object target; private final String fieldName; private final Object value; public Missingfields(Object target, String fieldName, Object value) { this.target = target; this.fieldName = fieldName; this.value = value; } } protected Resolver(ReadOptions readOptions, ReferenceTracker references, Converter converter) { this.readOptions = readOptions; this.references = references; this.converter = converter; } public ReadOptions getReadOptions() { return readOptions; } public ReferenceTracker getReferences() { return references; } public Converter getConverter() { return converter; } /** * This method converts a rootObj Map, (which contains nested Maps * and so forth representing a Java Object graph), to a Java * object instance. The rootObj map came from using the JsonReader * to parse a JSON graph (using the API that puts the graph * into Maps, not the typed representation). * * @param rootObj JsonObject instance that was the rootObj object from the * @param root When you know the type you will be returning. Can be null (effectively Map.class) * JSON input that was parsed in an earlier call to JsonReader. * @return a typed Java instance that was serialized into JSON. */ @SuppressWarnings("unchecked") public T toJavaObjects(JsonObject rootObj, Class root) { if (rootObj == null) { return null; } if (rootObj.isReference()) { rootObj = getReferences().get(rootObj); } if (rootObj.isFinished) { // Called on a JsonObject that has already been converted return (T) rootObj.getTarget(); } else { rootObj.setHintType(root); Object instance = rootObj.getTarget() == null ? createInstance(rootObj) : rootObj.getTarget(); if (rootObj.isFinished) { // Factory method instantiated and completely loaded the object. return (T) instance; } else { return traverseJsonObject(rootObj); } } } /** * Walk a JsonObject (Map of String keys to values) and return the * Java object equivalent filled in as good as possible (everything * except unresolved reference fields or unresolved array/collection elements). * * @param root JsonObject reference to a Map-of-Maps representation of the JSON * input after it has been completely read. * @return Properly constructed, typed, Java object graph built from a Map * of Maps representation (JsonObject root). */ public T traverseJsonObject(JsonObject root) { push(root); while (!stack.isEmpty()) { final JsonObject jsonObj = stack.pop(); if (jsonObj.isReference()) { continue; } if (jsonObj.isFinished) { continue; } if (visited.containsKey(jsonObj)) { jsonObj.setFinished(); continue; } visited.put(jsonObj, null); traverseSpecificType(jsonObj); } return (T) root.getTarget(); } public void traverseSpecificType(JsonObject jsonObj) { if (jsonObj.isArray()) { traverseArray(jsonObj); } else if (jsonObj.isCollection()) { traverseCollection(jsonObj); } else if (jsonObj.isMap()) { traverseMap(jsonObj); } else { Object special; if ((special = readWithFactoryIfExists(jsonObj, null)) != null) { jsonObj.setTarget(special); } else { traverseFields(jsonObj); } } } public SealedSupplier getSealedSupplier() { return sealedSupplier; } /** * Push a JsonObject on the work stack that has not yet had it's fields move over to it's Java peer (.target) * @param jsonObject JsonObject that supplies the source values for the Java peer (target) */ public void push(JsonObject jsonObject) { stack.push(jsonObject); } public abstract void traverseFields(final JsonObject jsonObj); protected abstract Object readWithFactoryIfExists(final Object o, final Class compType); protected abstract void traverseCollection(JsonObject jsonObj); protected abstract void traverseArray(JsonObject jsonObj); public abstract void assignField(final JsonObject jsonObj, final Injector injector, final Object rhs); protected void cleanup() { patchUnresolvedReferences(); rehashMaps(); references.clear(); unresolvedRefs.clear(); prettyMaps.clear(); handleMissingFields(); missingFields.clear(); stack.clear(); visited.clear(); references = null; readOptions = null; sealedSupplier.seal(); sealedSupplier = null; } // calls the missing field handler if any for each recorded missing field. private void handleMissingFields() { MissingFieldHandler missingFieldHandler = readOptions.getMissingFieldHandler(); if (missingFieldHandler != null) { for (Missingfields mf : missingFields) { missingFieldHandler.fieldMissing(mf.target, mf.fieldName, mf.value); } }//else no handler so ignore. } /** * Process java.util.Map and it's derivatives. These are written specially * so that the serialization does not expose the class internals * (internal fields of TreeMap for example). * * @param jsonObj a Map-of-Map representation of the JSON input stream. */ protected void traverseMap(JsonObject jsonObj) { Map.Entry pair = jsonObj.asTwoArrays(); final Object[] keys = pair.getKey(); final Object[] items = pair.getValue(); if (keys == null || items == null) { if (keys != items) { throw new JsonIoException("Unbalanced { } in JSON, it has " + KEYS + " or " + ITEMS + " empty. They should be same null, empty, or same length."); } return; } int size = keys.length; if (size != items.length) { throw new JsonIoException("Unbalance { } in JSON, it has " + KEYS + " and " + ITEMS + "s entries of different sizes. They should be same length."); } buildCollection(keys); buildCollection(items); // Save these for later so that unresolved references inside keys or values // get patched first, and then build the Maps. prettyMaps.add(new Object[]{jsonObj, keys, items}); } private void buildCollection(Object[] arrayContent) { final JsonObject collection = new JsonObject(); collection.setJsonArray(arrayContent); collection.setTarget(arrayContent); push(collection); } /** * This method creates a Java Object instance based on the passed in parameters. * If the JsonObject contains a key '@type' then that is used, as the type was explicitly * set in the JSON stream. If the key '@type' does not exist, then the passed in Class * is used to create the instance, handling creating an Array or regular Object * instance. *
* The '@type' is not often specified in the JSON input stream, as in many * cases it can be inferred from a field reference or array component type. * * @param jsonObj Map-of-Map representation of object to create. * @return a new Java object of the appropriate type (clazz) using the jsonObj to provide * enough hints to get the right class instantiated. It is not populated when returned. */ Object createInstance(JsonObject jsonObj) { Object target = jsonObj.getTarget(); if (target != null) { // already created peer Java instance return target; } // Coerce class first Class targetType = jsonObj.getJavaType(); jsonObj.setJavaType(coerceClassIfNeeded(targetType)); targetType = jsonObj.getJavaType(); // Does a 'Converter' conversion exist? if (jsonObj.hasValue() && jsonObj.getValue() != null) { if (converter.isConversionSupportedFor(jsonObj.getValue().getClass(), targetType)) { Object value = converter.convert(jsonObj.getValue(), targetType); return jsonObj.setFinishedTarget(value, true); } } else if (!jsonObj.isEmpty() && converter.isConversionSupportedFor(Map.class, targetType)) { try { Object value = converter.convert(jsonObj, targetType); return jsonObj.setFinishedTarget(value, true); // Calendar, Timestamp, Duration, Instance, LocalDateTime, ... } catch (Exception ignored) { // will be created later, below } } // ClassFactory defined Object mate = createInstanceUsingClassFactory(jsonObj.getJavaType(), jsonObj); if (mate != NO_FACTORY) { return mate; } // TODO: Additional Factory Classes: EnumSet // EnumSet Object mayEnumSpecial = jsonObj.get("@enum"); Class c = jsonObj.getJavaType(); // support deserialization of EnumSet an old serialization of json-io library (second condition) if (mayEnumSpecial instanceof String || EnumSet.class.isAssignableFrom(c)) { // TODO: This should move to EnumSetFactory - Both creating the enum and extracting the enumSet. mate = extractEnumSet(jsonObj); jsonObj.setTarget(mate); jsonObj.isFinished = true; return mate; } // Arrays Object[] items = jsonObj.getJsonArray(); if (c.isArray() || (items != null && c == Object.class && !jsonObj.containsKey(KEYS))) { // Handle [] int size = (items == null) ? 0 : items.length; mate = Array.newInstance(c.isArray() ? c.getComponentType() : Object.class, size); jsonObj.setTarget(mate); return mate; } return createInstanceUsingType(jsonObj); } /** * Create an instance of a Java class using the ".type" field on the jsonObj. The clazz argument is not * used for determining type, just for clarity in an exception message. * TODO: These instances are not all LOADED yet, so that is why they are not in the main createInstance() * TODO: method. As they are loaded, they will move up. Also, pulling primitives, class, and others into * TODO: factories will shrink this to just unknown generic classes, Object[]'s, and Collections of such. */ private Object createInstanceUsingType(JsonObject jsonObj) { Class c = jsonObj.getJavaType(); boolean useMaps = readOptions.isReturningJsonObjects(); Object mate; if (c == Object.class && !useMaps) { // JsonObject Class unknownClass = readOptions.getUnknownTypeClass(); if (unknownClass == null) { JsonObject jsonObject = new JsonObject(); jsonObject.setJavaType(Map.class); mate = jsonObject; } else { mate = MetaUtils.newInstance(converter, unknownClass, null); // can add constructor arg values } } else { // Handle regular field.object reference // ClassFactory already consulted above, likely regular business/data classes. // If the newInstance(c) fails, it throws a JsonIoException. mate = MetaUtils.newInstance(converter, c, null); // can add constructor arg values } jsonObj.setTarget(mate); return mate; } /** * If a ClassFactory is associated to the passed in Class (clazz), then use the ClassFactory * to create an instance. If a ClassFactory create the instance, it may optionall load * the values into the instance, using the values from the passed in JsonObject. If the * ClassFactory instance creates AND loads the object, it is indicated on the ClassFactory * by the isObjectFinal() method returning true. Therefore, the JsonObject instance that is * loaded, is marked with 'isFinished=true' so that no more process is needed for this instance. */ Object createInstanceUsingClassFactory(Class c, JsonObject jsonObj) { // If a ClassFactory exists for a class, use it to instantiate the class. The ClassFactory // may optionally load the newly created instance, in which case, the JsonObject is marked finished, and // return. JsonReader.ClassFactory classFactory = readOptions.getClassFactory(c); if (classFactory == null) { return NO_FACTORY; } Object target = classFactory.newInstance(c, jsonObj, this); // don't pass in classFactory.isObjectFinal, only set it to true if classFactory says its so. // it allows the factory itself to set final on the jsonObj internally where it depends // on how the data comes back, but that value can be a hard true if the factory knows // it's always true. if (classFactory.isObjectFinal()) { return jsonObj.setFinishedTarget(target, true); } jsonObj.setTarget(target); return target; } private Class coerceClassIfNeeded(Class type) { if (type == null) { return null; } Class clazz = readOptions.getCoercedClass(type); return clazz == null ? type : clazz; } private EnumSet extractEnumSet(JsonObject jsonObj) { String enumClassName = (String) jsonObj.get("@enum"); Class enumClass = enumClassName == null ? evaluateEnumSetTypeFromItems(jsonObj) : ClassUtilities.forName(enumClassName, readOptions.getClassLoader()); Object[] items = jsonObj.getJsonArray(); if (items == null || items.length == 0) { if (enumClass != null) { return EnumSet.noneOf(enumClass); } else { return EnumSet.noneOf(MetaUtils.Dumpty.class); } } else if (enumClass == null) { throw new JsonIoException("Could not figure out Enum of the not empty set " + jsonObj); } EnumSet enumSet = null; for (Object item : items) { Enum enumItem; if (item instanceof String) { enumItem = Enum.valueOf(enumClass, (String) item); } else { JsonObject jObj = (JsonObject) item; enumItem = Enum.valueOf(enumClass, (String) jObj.get("name")); } if (enumSet == null) { // Lazy init the EnumSet enumSet = EnumSet.of(enumItem); } else { enumSet.add(enumItem); } } return enumSet; } /** * an old serialized values support a different format of enumset serialization * Example: *

{@code
     *     "@type": "java.util.RegularEnumSet",
     *     "@items": [
     *       {
     *         "@type": "com.cedarsoftware.io.OldSetTest$Enum1",
     *         "name": "E1"
     *       }     *
     *}
*/ private Class evaluateEnumSetTypeFromItems(final JsonObject json) { final Object[] items = json.getJsonArray(); if (items != null && items.length != 0) { if (items[0] instanceof JsonObject) { return ((JsonObject) items[0]).getJavaType(); } } // can't evaluate return null; } /** * For all fields where the value was "@ref":"n" where 'n' was the id of an object * that had not yet been encountered in the stream, make the final substitution. */ private void patchUnresolvedReferences() { for (UnresolvedReference ref : unresolvedRefs) { Object objToFix = ref.referencingObj.getTarget(); JsonObject objReferenced = this.references.get(ref.refId); if (ref.index >= 0) { // Fix []'s and Collections containing a forward reference. if (objToFix instanceof List) { List list = (List) objToFix; list.set(ref.index, objReferenced.getTarget()); } else if (objToFix instanceof Collection) { // Patch up Indexable Collections Collection col = (Collection) objToFix; col.add(objReferenced.getTarget()); } else { Array.set(objToFix, ref.index, objReferenced.getTarget()); // patch array element here } } else { // Fix field forward reference Field field = getReadOptions().getDeepDeclaredFields(objToFix.getClass()).get(ref.field); if (field != null) { try { MetaUtils.setFieldValue(field, objToFix, objReferenced.getTarget()); // patch field here } catch (Exception e) { throw new JsonIoException("Error setting field while resolving references '" + field.getName() + "', @ref = " + ref.refId, e); } } } } unresolvedRefs.clear(); } /** * Process Maps/Sets (fix up their internal indexing structure) * This is required because Maps hash items using hashCode(), which will * change between VMs. Rehashing the map fixes this. *
* If useMaps==true, then move @keys to keys and @items to values * and then drop these two entries from the map. *
* This hashes both Sets and Maps because the JDK sets are implemented * as Maps. If you have a custom-built Set, this would not 'treat' it, * and you would need to provide a custom reader for that set. */ private void rehashMaps() { final boolean useMapsLocal = readOptions.isReturningJsonObjects(); for (Object[] mapPieces : prettyMaps) { JsonObject jsonObj = (JsonObject) mapPieces[0]; jsonObj.rehashMaps(useMapsLocal, (Object[]) mapPieces[1], (Object[]) mapPieces[2]); } } public boolean valueToTarget(JsonObject jsonObject) { if (jsonObject.javaType == null) { if (jsonObject.hintType == null) { return false; } jsonObject.javaType = jsonObject.hintType; } // TODO: Support multiple dimensions // TODO: Support char if (jsonObject.javaType.isArray() && isConvertable(jsonObject.javaType.getComponentType())) { Object[] jsonItems = jsonObject.getJsonArray(); Class componentType = jsonObject.javaType.getComponentType(); if (jsonItems == null) { // empty array jsonObject.setFinishedTarget(null, true); return true; } Object javaArray = Array.newInstance(componentType, jsonItems.length); for (int i = 0; i < jsonItems.length; i++) { try { Class type = componentType; if (jsonItems[i] instanceof JsonObject) { JsonObject jObj = (JsonObject) jsonItems[i]; if (jObj.getJavaType() != null) { type = jObj.getJavaType(); } } Array.set(javaArray, i, converter.convert(jsonItems[i], type)); } catch (Exception e) { JsonIoException jioe = new JsonIoException(e.getMessage()); jioe.setStackTrace(e.getStackTrace()); throw jioe; } } jsonObject.setFinishedTarget(javaArray, true); return true; } if (!isConvertable(jsonObject.javaType)) { return false; } try { Object value = converter.convert(jsonObject, jsonObject.javaType); jsonObject.setFinishedTarget(value, true); return true; } catch (Exception e) { JsonIoException jioe = new JsonIoException(e.getMessage()); jioe.setStackTrace(e.getStackTrace()); throw jioe; } } public boolean isConvertable(Class type) { return convertableValues.contains(type.getName()); } /** * Create peer Java object to the passed in root JsonObject. In the special case that root is an Object[], * then create a JsonObject to wrap it, set the passed in Object[] to be the target of the JsonObject, ensure * that the root Object[] items are copied to the JsonObject, and then return the JsonObject wrapper. If * called with a primitive (anythning else), just return it. */ Object createJavaFromJson(Object root) { if (root instanceof Object[]) { JsonObject array = new JsonObject(); array.setTarget(root); array.setJsonArray((Object[]) root); push(array); // resolver - you do the rest of the mapping return root; } else if (root instanceof JsonObject) { Object ret = createInstance((JsonObject) root); push((JsonObject) root); // thank you, resolver return ret; } else { return root; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy