Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.cedarsoftware.util.io.Resolver Maven / Gradle / Ivy
package com.cedarsoftware.util.io;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 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
*
* 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.*
*/
abstract class Resolver
{
protected final Collection unresolvedRefs = new ArrayList();
protected final JsonReader reader;
private static final NullClass nullReader = new NullClass();
final ConcurrentMap readerCache = new ConcurrentHashMap();
private final Collection prettyMaps = new ArrayList();
private final boolean useMaps;
private final Object unknownClass;
/**
* 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.
*/
protected static final class UnresolvedReference
{
private final JsonObject referencingObj;
private String field;
private final long refId;
private int index = -1;
protected UnresolvedReference(JsonObject referrer, String fld, long id)
{
referencingObj = referrer;
field = fld;
refId = id;
}
protected UnresolvedReference(JsonObject referrer, int idx, long id)
{
referencingObj = referrer;
index = idx;
refId = id;
}
}
/**
* Dummy place-holder class exists only because ConcurrentHashMap cannot contain a
* null value. Instead, singleton instance of this class is placed where null values
* are needed.
*/
static class NullClass implements JsonReader.JsonClassReaderBase { }
protected Resolver(JsonReader reader)
{
this.reader = reader;
Map optionalArgs = reader.getArgs();
useMaps = Boolean.TRUE.equals(optionalArgs.get(JsonReader.USE_MAPS));
unknownClass = optionalArgs.containsKey(JsonReader.UNKNOWN_OBJECT) ? optionalArgs.get(JsonReader.UNKNOWN_OBJECT) : null;
}
protected JsonReader getReader()
{
return reader;
}
/**
* Walk a JsonObject (Map of String keys to values) and return the
* Java object equivalent filled in as best 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).
*/
protected Object convertMapsToObjects(final JsonObject root)
{
final Deque> stack = new ArrayDeque>();
stack.addFirst(root);
while (!stack.isEmpty())
{
final JsonObject jsonObj = stack.removeFirst();
if (jsonObj.isArray())
{
traverseArray(stack, jsonObj);
}
else if (jsonObj.isCollection())
{
traverseCollection(stack, jsonObj);
}
else if (jsonObj.isMap())
{
traverseMap(stack, jsonObj);
}
else
{
traverseFields(stack, jsonObj);
}
}
return root.target;
}
protected abstract void traverseFields(Deque> stack, JsonObject jsonObj);
protected abstract void traverseCollection(Deque> stack, JsonObject jsonObj);
protected abstract void traverseArray(Deque> stack, JsonObject jsonObj);
protected void cleanup()
{
patchUnresolvedReferences();
rehashMaps();
reader.getObjectsRead().clear();
unresolvedRefs.clear();
prettyMaps.clear();
readerCache.clear();
}
/**
* Process java.util.Map and it's derivatives. These can be written specially
* so that the serialization would not expose the derivative class internals
* (internal fields of TreeMap for example).
*
* @param stack a Stack (Deque) used to support graph traversal.
* @param jsonObj a Map-of-Map representation of the JSON input stream.
*/
protected void traverseMap(Deque> stack, JsonObject jsonObj)
{
// Convert @keys to a Collection of Java objects.
convertMapToKeysItems(jsonObj);
final Object[] keys = (Object[]) jsonObj.get("@keys");
final Object[] items = jsonObj.getArray();
if (keys == null || items == null)
{
if (keys != items)
{
throw new JsonIoException("Map written where one of @keys or @items is empty");
}
return;
}
final int size = keys.length;
if (size != items.length)
{
throw new JsonIoException("Map written with @keys and @items entries of different sizes");
}
Object[] mapKeys = buildCollection(stack, keys, size);
Object[] mapValues = buildCollection(stack, items, size);
// 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, mapKeys, mapValues});
}
private static Object[] buildCollection(Deque> stack, Object[] items, int size)
{
final JsonObject jsonCollection = new JsonObject();
jsonCollection.put("@items", items);
final Object[] javaKeys = new Object[size];
jsonCollection.target = javaKeys;
stack.addFirst(jsonCollection);
return javaKeys;
}
/**
* Convert an input JsonObject map (known to represent a Map.class or derivative) that has regular keys and values
* to have its keys placed into @keys, and its values placed into @items.
*
* @param map Map to convert
*/
protected static void convertMapToKeysItems(final JsonObject map)
{
if (!map.containsKey("@keys") && !map.isReference())
{
final Object[] keys = new Object[map.size()];
final Object[] values = new Object[map.size()];
int i = 0;
for (Object e : map.entrySet())
{
final Map.Entry entry = (Map.Entry) e;
keys[i] = entry.getKey();
values[i] = entry.getValue();
i++;
}
String saveType = map.getType();
map.clear();
map.setType(saveType);
map.put("@keys", keys);
map.put("@items", values);
}
}
/**
* 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 clazz Instance will be create of this class.
* @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.
*/
protected Object createJavaObjectInstance(Class clazz, JsonObject jsonObj)
{
final boolean useMapsLocal = useMaps;
String type = jsonObj.type;
// We can't set values to an Object, so well try to use the contained type instead
if ("java.lang.Object".equals(type))
{
Object value = jsonObj.get("value");
if (jsonObj.keySet().size() == 1 && value != null)
{
type = value.getClass().getName();
}
}
Object mate;
// @type always takes precedence over inferred Java (clazz) type.
if (type != null)
{ // @type is explicitly set, use that as it always takes precedence
Class c;
try
{
c = MetaUtils.classForName(type);
}
catch (Exception e)
{
if (useMapsLocal)
{
jsonObj.type = null;
jsonObj.target = null;
return jsonObj;
}
else
{
String name = clazz == null ? "null" : clazz.getName();
throw new JsonIoException("Unable to create class: " + name, e);
}
}
if (c.isArray())
{ // Handle []
Object[] items = jsonObj.getArray();
int size = (items == null) ? 0 : items.length;
if (c == char[].class)
{
jsonObj.moveCharsToMate();
mate = jsonObj.target;
}
else
{
mate = Array.newInstance(c.getComponentType(), size);
}
}
else
{ // Handle regular field.object reference
if (MetaUtils.isPrimitive(c))
{
mate = MetaUtils.newPrimitiveWrapper(c, jsonObj.get("value"));
}
else if (c == Class.class)
{
mate = MetaUtils.classForName((String) jsonObj.get("value"));
}
else if (c.isEnum())
{
mate = getEnum(c, jsonObj);
}
else if (Enum.class.isAssignableFrom(c)) // anonymous subclass of an enum
{
mate = getEnum(c.getSuperclass(), jsonObj);
}
else if (EnumSet.class.isAssignableFrom(c))
{
mate = getEnumSet(c, jsonObj);
}
else if ("java.util.Arrays$ArrayList".equals(c.getName()))
{ // Special case: Arrays$ArrayList does not allow .add() to be called on it.
mate = new ArrayList();
}
else
{
mate = newInstance(c, jsonObj);
}
}
}
else
{ // @type, not specified, figure out appropriate type
Object[] items = jsonObj.getArray();
// if @items is specified, it must be an [] type.
// if clazz.isArray(), then it must be an [] type.
if (clazz.isArray() || (items != null && clazz == Object.class && !jsonObj.containsKey("@keys")))
{
int size = (items == null) ? 0 : items.length;
mate = Array.newInstance(clazz.isArray() ? clazz.getComponentType() : Object.class, size);
}
else if (clazz.isEnum())
{
mate = getEnum(clazz, jsonObj);
}
else if (Enum.class.isAssignableFrom(clazz)) // anonymous subclass of an enum
{
mate = getEnum(clazz.getSuperclass(), jsonObj);
}
else if (EnumSet.class.isAssignableFrom(clazz)) // anonymous subclass of an enum
{
mate = getEnumSet(clazz, jsonObj);
}
else if ("java.util.Arrays$ArrayList".equals(clazz.getName()))
{ // Special case: Arrays$ArrayList does not allow .add() to be called on it.
mate = new ArrayList();
}
else if (clazz == Object.class && !useMapsLocal)
{
if (unknownClass == null)
{
mate = new JsonObject();
((JsonObject)mate).type = Map.class.getName();
}
else if (unknownClass instanceof String)
{
mate = newInstance(MetaUtils.classForName(((String)unknownClass).trim()), jsonObj);
}
else
{
throw new JsonIoException("Unable to determine object type at column: " + jsonObj.col + ", line: " + jsonObj.line + ", content: " + jsonObj);
}
}
else
{
mate = newInstance(clazz, jsonObj);
}
}
jsonObj.target = mate;
return jsonObj.target;
}
protected JsonObject getReferencedObj(Long ref)
{
JsonObject refObject = reader.getObjectsRead().get(ref);
if (refObject == null)
{
throw new JsonIoException("Forward reference @ref: " + ref + ", but no object defined (@id) with that value");
}
return refObject;
}
protected JsonReader.JsonClassReaderBase getCustomReader(Class c)
{
JsonReader.JsonClassReaderBase reader = readerCache.get(c);
if (reader == null)
{
reader = forceGetCustomReader(c);
JsonReader.JsonClassReaderBase readerRef = readerCache.putIfAbsent(c, reader);
if (readerRef != null)
{
reader = readerRef;
}
}
return reader == nullReader ? null : reader;
}
private JsonReader.JsonClassReaderBase forceGetCustomReader(Class c)
{
JsonReader.JsonClassReaderBase closestReader = nullReader;
int minDistance = Integer.MAX_VALUE;
for (Map.Entry entry : getReaders().entrySet())
{
Class clz = entry.getKey();
if (clz == c)
{
return entry.getValue();
}
int distance = MetaUtils.getDistance(clz, c);
if (distance < minDistance)
{
minDistance = distance;
closestReader = entry.getValue();
}
}
return closestReader;
}
/**
* Fetch enum value (may need to try twice, due to potential 'name' field shadowing by enum subclasses
*/
private Object getEnum(Class c, JsonObject jsonObj)
{
try
{
return Enum.valueOf(c, (String) jsonObj.get("name"));
}
catch (Exception e)
{ // In case the enum class has it's own 'name' member variable (shadowing the 'name' variable on Enum)
return Enum.valueOf(c, (String) jsonObj.get("java.lang.Enum.name"));
}
}
/**
* Create the EnumSet with its values (it must be created this way)
*/
private Object getEnumSet(Class c, JsonObject jsonObj)
{
Object[] items = jsonObj.getArray();
if (items == null || items.length == 0)
{
return newInstance(c, jsonObj);
}
JsonObject item = (JsonObject) items[0];
String type = item.getType();
Class enumClass = MetaUtils.classForName(type);
EnumSet enumSet = null;
for (Object objectItem : items)
{
item = (JsonObject) objectItem;
Enum enumItem = (Enum) getEnum(enumClass, item);
if (enumSet == null)
{ // Lazy init the EnumSet
enumSet = EnumSet.of(enumItem);
}
else
{
enumSet.add(enumItem);
}
}
return enumSet;
}
/**
* 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.
*/
protected void patchUnresolvedReferences()
{
Iterator i = unresolvedRefs.iterator();
while (i.hasNext())
{
UnresolvedReference ref = (UnresolvedReference) i.next();
Object objToFix = ref.referencingObj.target;
JsonObject objReferenced = reader.getObjectsRead().get(ref.refId);
if (ref.index >= 0)
{ // Fix []'s and Collections containing a forward reference.
if (objToFix instanceof List)
{ // Patch up Indexable Collections
List list = (List) objToFix;
list.set(ref.index, objReferenced.target);
}
else if (objToFix instanceof Collection)
{ // Add element (since it was not indexable, add it to collection)
Collection col = (Collection) objToFix;
col.add(objReferenced.target);
}
else
{
Array.set(objToFix, ref.index, objReferenced.target); // patch array element here
}
}
else
{ // Fix field forward reference
Field field = MetaUtils.getField(objToFix.getClass(), ref.field);
if (field != null)
{
try
{
field.set(objToFix, objReferenced.target); // patch field here
}
catch (Exception e)
{
throw new JsonIoException("Error setting field while resolving references '" + field.getName() + "', @ref = " + ref.refId, e);
}
}
}
i.remove();
}
}
/**
* 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 provider a custom reader for that set.
*/
protected void rehashMaps()
{
final boolean useMapsLocal = useMaps;
for (Object[] mapPieces : prettyMaps)
{
JsonObject jObj = (JsonObject) mapPieces[0];
Object[] javaKeys, javaValues;
Map map;
if (useMapsLocal)
{ // Make the @keys be the actual keys of the map.
map = jObj;
javaKeys = (Object[]) jObj.remove("@keys");
javaValues = (Object[]) jObj.remove("@items");
}
else
{
map = (Map) jObj.target;
javaKeys = (Object[]) mapPieces[1];
javaValues = (Object[]) mapPieces[2];
jObj.clear();
}
int j = 0;
while (javaKeys != null && j < javaKeys.length)
{
map.put(javaKeys[j], javaValues[j]);
j++;
}
}
}
// ========== Keep relationship knowledge below the line ==========
public static Object newInstance(Class c, JsonObject jsonObject)
{
return JsonReader.newInstance(c, jsonObject);
}
protected Map getReaders()
{
return reader.readers;
}
protected boolean notCustom(Class cls)
{
return reader.notCustom.contains(cls);
}
}