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.
io.github.lukehutch.fastclasspathscanner.json.JSONSerializer Maven / Gradle / Ivy
Go to download
Uber-fast, ultra-lightweight Java classpath scanner. Scans the classpath by parsing the classfile binary format directly rather than by using reflection.
See https://github.com/lukehutch/fast-classpath-scanner
/*
* This file is part of FastClasspathScanner.
*
* Author: Luke Hutchison
*
* Hosted at: https://github.com/lukehutch/fast-classpath-scanner
*
* --
*
* The MIT License (MIT)
*
* Copyright (c) 2018 Luke Hutchison
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
* EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.lukehutch.fastclasspathscanner.json;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Fast, lightweight Java object to JSON serializer, and JSON to Java object deserializer. Handles cycles in the
* object graph by inserting reference ids.
*/
public class JSONSerializer {
/** Create a unique id for each referenced JSON object. */
private static void assignObjectIds(final Object jsonVal,
final Map, JSONObject> objToJSONVal, final ClassFieldCache classFieldCache,
final Map, CharSequence> jsonReferenceToId,
final AtomicInteger objId, final boolean onlySerializePublicFields) {
if (jsonVal == null) {
return;
} else if (jsonVal instanceof JSONObject) {
for (final Entry item : ((JSONObject) jsonVal).items) {
assignObjectIds(item.getValue(), objToJSONVal, classFieldCache, jsonReferenceToId, objId,
onlySerializePublicFields);
}
} else if (jsonVal instanceof JSONArray) {
for (final Object item : ((JSONArray) jsonVal).items) {
assignObjectIds(item, objToJSONVal, classFieldCache, jsonReferenceToId, objId,
onlySerializePublicFields);
}
} else if (jsonVal instanceof JSONReference) {
// Get the referenced (non-JSON) object
final Object refdObj = ((JSONReference) jsonVal).idObject;
if (refdObj == null) {
// Should not happen
throw new RuntimeException("Internal inconsistency");
}
// Look up the JSON object corresponding to the referenced object
final ReferenceEqualityKey refdObjKey = new ReferenceEqualityKey<>(refdObj);
final JSONObject refdJsonVal = objToJSONVal.get(refdObjKey);
if (refdJsonVal == null) {
// Should not happen
throw new RuntimeException("Internal inconsistency");
}
// See if the JSON object has an @Id field
// (for serialization, typeResolutions can be null)
final Field annotatedField = classFieldCache.get(refdObj.getClass()).idField;
String idStr = null;
if (annotatedField != null) {
// Get id value from field annotated with @Id
try {
final Object idObject = annotatedField.get(refdObj);
if (idObject != null) {
idStr = idObject.toString();
refdJsonVal.objectId = idStr;
}
} catch (IllegalArgumentException | IllegalAccessException e) {
// Should not happen
throw new IllegalArgumentException("Could not access @Id-annotated field " + annotatedField, e);
}
}
if (idStr == null) {
// No @Id field, or field value is null -- check if ref'd JSON Object already has an id
if (refdJsonVal.objectId == null) {
// Ref'd JSON object doesn't have an id yet -- generate unique integer id
idStr = JSONUtils.ID_PREFIX + objId.getAndIncrement() + JSONUtils.ID_SUFFIX;
refdJsonVal.objectId = idStr;
}
}
// Link both the JSON representation ob the object to the id
jsonReferenceToId.put(new ReferenceEqualityKey<>((JSONReference) jsonVal), idStr);
}
}
// -------------------------------------------------------------------------------------------------------------
/** Take an array of object values, and recursively convert them (in place) into JSON values. */
private static void convertVals(final Object[] convertedVals,
final Set> visitedOnPath,
final Set> standardObjectVisited, final ClassFieldCache classFieldCache,
final Map, JSONObject> objToJSONVal,
final boolean onlySerializePublicFields) {
// Pass 1: find standard objects (objects that are not of basic value type or collections/arrays/maps)
// that have not yet been visited, and mark them as visited. Place a JSONReference placeholder in
// convertedVals[i] to signify this. This first pass is non-recursive, so that objects are visited
// as high up the tree as possible, since it is only the first visit of an object that shows in the
// final JSON doc, and the rest are turned into references.
final ReferenceEqualityKey>[] valKeys = new ReferenceEqualityKey>[convertedVals.length];
final boolean[] needToConvert = new boolean[convertedVals.length];
for (int i = 0; i < convertedVals.length; i++) {
final Object val = convertedVals[i];
// By default (for basic value types), don't convert vals
needToConvert[i] = !JSONUtils.isBasicValueType(val);
if (needToConvert[i] && !JSONUtils.isCollectionOrArray(val)) {
// If this object is a standard object or a map, check if it has already been visited
// elsewhere in the tree. If so, use a JSONReference instead, and mark the object
// as visited.
final ReferenceEqualityKey valKey = new ReferenceEqualityKey<>(val);
valKeys[i] = valKey;
final boolean alreadyVisited = !standardObjectVisited.add(valKey);
if (alreadyVisited) {
convertedVals[i] = new JSONReference(val);
needToConvert[i] = false;
}
}
}
// Pass 2: Recursively convert items in standard objects, maps, collections and arrays to JSON objects.
for (int i = 0; i < convertedVals.length; i++) {
if (needToConvert[i]) {
// Recursively convert standard objects (if it is the first time they have been visited)
// and maps to JSON objects, and convert collections and arrays to JSON arrays.
final Object val = convertedVals[i];
convertedVals[i] = toJSONGraph(val, visitedOnPath, standardObjectVisited, classFieldCache,
objToJSONVal, onlySerializePublicFields);
if (!JSONUtils.isCollectionOrArray(val)) {
// If this object is a standard object or map, then it has not been visited before,
// so save the mapping between original object and converted object
@SuppressWarnings("unchecked")
final ReferenceEqualityKey valKey = (ReferenceEqualityKey) valKeys[i];
objToJSONVal.put(valKey, (JSONObject) convertedVals[i]);
}
}
}
}
/**
* Turn an object graph into a graph of JSON objects, arrays, and values.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Object toJSONGraph(final Object obj, final Set> visitedOnPath,
final Set> standardObjectVisited, final ClassFieldCache classFieldCache,
final Map, JSONObject> objToJSONVal,
final boolean onlySerializePublicFields) {
// For null and basic value types, just return value
if (obj == null || obj instanceof String || obj instanceof Integer || obj instanceof Boolean
|| obj instanceof Long || obj instanceof Float || obj instanceof Double || obj instanceof Short
|| obj instanceof Byte || obj instanceof Character || obj.getClass().isEnum()) {
return obj;
}
// Check for cycles
final ReferenceEqualityKey objKey = new ReferenceEqualityKey<>(obj);
if (!visitedOnPath.add(objKey)) {
// Reached cycle in graph
if (JSONUtils.isCollectionOrArray(obj)) {
// If we reached a collection that has already been visited, then there is a cycle
// terminating at this collection. We don't support collection cycles, since collections
// do not have object ids.
throw new IllegalArgumentException(
"Cycles involving collections cannot be serialized, since collections are not "
+ "assigned object ids. Reached cycle at: " + obj);
} else {
// Object is its own ancestor -- output object reference instead of object to break cycle
return new JSONReference(obj);
}
}
Object jsonVal;
final Class extends Object> cls = obj.getClass();
if (Map.class.isAssignableFrom(cls)) {
final Map map = (Map) obj;
// Get map keys, and sort them by value if values are Comparable.
// Assumes all keys have the same type (or at least that if one key is Comparable, they all are).
final ArrayList> keys = new ArrayList<>(map.keySet());
final int n = keys.size();
boolean keysComparable = false;
Object firstNonNullKey = null;
for (int i = 0; i < n && firstNonNullKey == null; i++) {
firstNonNullKey = keys.get(i);
}
if (firstNonNullKey != null) {
if (Comparable.class.isAssignableFrom(firstNonNullKey.getClass())) {
Collections.sort((ArrayList) keys);
keysComparable = true;
}
}
// Serialize keys into string form
final String[] convertedKeys = new String[n];
for (int i = 0; i < n; i++) {
final Object key = keys.get(i);
if (!JSONUtils.isBasicValueType(key)) {
throw new IllegalArgumentException("Map key of type " + key.getClass().getName()
+ " is not a basic type (String, Integer, etc.), so can't be easily "
+ "serialized as a JSON associative array key");
}
convertedKeys[i] = JSONUtils.escapeJSONString(key.toString());
}
// Sort value strings lexicographically if values are not Comparable
if (!keysComparable) {
Arrays.sort(convertedKeys);
}
// Convert map values to JSON values
final Object[] convertedVals = new Object[n];
for (int i = 0; i < n; i++) {
convertedVals[i] = map.get(keys.get(i));
}
convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
onlySerializePublicFields);
// Create new JSON object representing the map
final List> convertedKeyValPairs = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
convertedKeyValPairs.add(new SimpleEntry<>(convertedKeys[i], convertedVals[i]));
}
jsonVal = new JSONObject(convertedKeyValPairs);
} else if (cls.isArray() || List.class.isAssignableFrom(cls)) {
// Serialize an array or list
final boolean isList = List.class.isAssignableFrom(cls);
final List> list = isList ? (List>) obj : null;
final int n = isList ? list.size() : Array.getLength(obj);
// Convert list items to JSON values
final Object[] convertedVals = new Object[n];
for (int i = 0; i < n; i++) {
convertedVals[i] = isList ? list.get(i) : Array.get(obj, i);
}
convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
onlySerializePublicFields);
// Create new JSON array representing the list
jsonVal = new JSONArray(Arrays.asList(convertedVals));
} else if (Collection.class.isAssignableFrom(cls)) {
final Collection> collection = (Collection>) obj;
// Convert items to JSON values
final List convertedValsList = new ArrayList<>();
for (final Object item : collection) {
convertedValsList.add(item);
}
final Object[] convertedVals = convertedValsList.toArray();
convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
onlySerializePublicFields);
// Create new JSON array representing the collection
jsonVal = new JSONArray(Arrays.asList(convertedVals));
} else {
// A standard object -- serialize fields as a JSON associative array.
try {
// Cache class fields to include in serialization (typeResolutions can be null,
// since it's not necessary to resolve type parameters during serialization)
final ClassFields resolvedFields = classFieldCache.get(cls);
final List fieldOrder = resolvedFields.fieldOrder;
final int n = fieldOrder.size();
// Convert field values to JSON values
final String[] fieldNames = new String[n];
final Object[] convertedVals = new Object[n];
for (int i = 0; i < n; i++) {
final FieldTypeInfo fieldInfo = fieldOrder.get(i);
final Field field = fieldInfo.field;
fieldNames[i] = field.getName();
convertedVals[i] = JSONUtils.getFieldValue(obj, field);
}
convertVals(convertedVals, visitedOnPath, standardObjectVisited, classFieldCache, objToJSONVal,
onlySerializePublicFields);
// Create new JSON object representing the standard object
final List> convertedKeyValPairs = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
convertedKeyValPairs.add(new SimpleEntry(fieldNames[i], convertedVals[i]));
}
jsonVal = new JSONObject(convertedKeyValPairs);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException("Could not get value of field in object: " + obj, e);
}
}
// In the case of a DAG, just serialize the same object multiple times, i.e. remove obj
// from visited set when exiting recursion, so that future instances also get serialized.
visitedOnPath.remove(objKey);
return jsonVal;
}
// -------------------------------------------------------------------------------------------------------------
/** Serialize a JSON object, array, or value. */
static void jsonValToJSONString(final Object jsonVal,
final Map, CharSequence> jsonReferenceToId,
final boolean includeNullValuedFields, final int depth, final int indentWidth,
final StringBuilder buf) {
if (jsonVal == null) {
buf.append("null");
} else if (jsonVal instanceof JSONObject) {
// Serialize JSONObject to string
((JSONObject) jsonVal).toJSONString(jsonReferenceToId, includeNullValuedFields, depth, indentWidth,
buf);
} else if (jsonVal instanceof JSONArray) {
// Serialize JSONArray to string
((JSONArray) jsonVal).toJSONString(jsonReferenceToId, includeNullValuedFields, depth, indentWidth, buf);
} else if (jsonVal instanceof JSONReference) {
// Serialize JSONReference to string
final Object referencedObjectId = jsonReferenceToId
.get(new ReferenceEqualityKey<>((JSONReference) jsonVal));
jsonValToJSONString(referencedObjectId, jsonReferenceToId, includeNullValuedFields, depth, indentWidth,
buf);
} else if (jsonVal instanceof CharSequence || jsonVal instanceof Character || jsonVal.getClass().isEnum()) {
// Serialize String, Character or enum val to quoted/escaped string
buf.append('"');
JSONUtils.escapeJSONString(jsonVal.toString(), buf);
buf.append('"');
} else {
// Serialize a numeric or Boolean type (Integer, Long, Short, Float, Double, Boolean, Byte) to string
// (doesn't need quoting or escaping)
buf.append(jsonVal.toString());
}
}
// -------------------------------------------------------------------------------------------------------------
/**
* Recursively serialize an Object (or array, list, map or set of objects) to JSON, skipping transient and final
* fields.
*
* @param obj
* The root object of the object graph to serialize.
* @param indentWidth
* If indentWidth == 0, no prettyprinting indentation is performed, otherwise this specifies the
* number of spaces to indent each level of JSON.
* @param onlySerializePublicFields
* If true, only serialize public fields.
* @param classFieldCache
* The class field cache. Reusing this cache will increase the speed if many JSON documents of the
* same type need to be produced.
* @return The object graph in JSON form.
* @throws IllegalArgumentException
* If anything goes wrong during serialization.
*/
public static String serializeObject(final Object obj, final int indentWidth,
final boolean onlySerializePublicFields, final ClassFieldCache classFieldCache) {
final HashMap, JSONObject> objToJSONVal = new HashMap<>();
final Object rootJsonVal = toJSONGraph(obj, new HashSet>(),
new HashSet>(), classFieldCache, objToJSONVal,
onlySerializePublicFields);
final Map, CharSequence> jsonReferenceToId = new HashMap<>();
final AtomicInteger objId = new AtomicInteger(0);
assignObjectIds(rootJsonVal, objToJSONVal, classFieldCache, jsonReferenceToId, objId,
onlySerializePublicFields);
final StringBuilder buf = new StringBuilder(32768);
jsonValToJSONString(rootJsonVal, jsonReferenceToId, /* includeNullValuedFields = */ false, 0, indentWidth,
buf);
return buf.toString();
}
/**
* Recursively serialize an Object (or array, list, map or set of objects) to JSON, skipping transient and final
* fields.
*
* @param obj
* The root object of the object graph to serialize.
* @param indentWidth
* If indentWidth == 0, no prettyprinting indentation is performed, otherwise this specifies the
* number of spaces to indent each level of JSON.
* @param onlySerializePublicFields
* If true, only serialize public fields.
* @return The object graph in JSON form.
* @throws IllegalArgumentException
* If anything goes wrong during serialization.
*/
public static String serializeObject(final Object obj, final int indentWidth,
final boolean onlySerializePublicFields) {
return serializeObject(obj, indentWidth, onlySerializePublicFields,
new ClassFieldCache(/* resolveTypes = */ false, /* onlySerializePublicFields = */ false));
}
/**
* Recursively serialize an Object (or array, list, map or set of objects) to JSON, skipping transient and final
* fields.
*
* @param obj
* The root object of the object graph to serialize.
* @return The object graph in JSON form.
* @throws IllegalArgumentException
* If anything goes wrong during serialization.
*/
public static String serializeObject(final Object obj) {
return serializeObject(obj, /* indentWidth = */ 0, /* onlySerializePublicFields = */ false);
}
/**
* Recursively serialize the named field of an object, skipping transient and final fields.
*
* @param containingObject
* The object containing the field value to serialize.
* @param fieldName
* The name of the field to serialize.
* @param indentWidth
* If indentWidth == 0, no prettyprinting indentation is performed, otherwise this specifies the
* number of spaces to indent each level of JSON.
* @param onlySerializePublicFields
* If true, only serialize public fields.
* @param classFieldCache
* The class field cache. Reusing this cache will increase the speed if many JSON documents of the
* same type need to be produced.
* @return The object graph in JSON form.
* @throws IllegalArgumentException
* If anything goes wrong during serialization.
*/
public static String serializeFromField(final Object containingObject, final String fieldName,
final int indentWidth, final boolean onlySerializePublicFields, final ClassFieldCache classFieldCache) {
final FieldTypeInfo fieldResolvedTypeInfo = classFieldCache
.get(containingObject.getClass()).fieldNameToFieldTypeInfo.get(fieldName);
if (fieldResolvedTypeInfo == null) {
throw new IllegalArgumentException("Class " + containingObject.getClass().getName()
+ " does not have a field named \"" + fieldName + "\"");
}
final Field field = fieldResolvedTypeInfo.field;
if (!JSONUtils.fieldIsSerializable(field, /* onlySerializePublicFields = */ false)) {
throw new IllegalArgumentException("Field " + containingObject.getClass().getName() + "." + fieldName
+ " needs to be accessible, non-transient, and non-final");
}
Object fieldValue;
try {
fieldValue = JSONUtils.getFieldValue(containingObject, field);
} catch (final Exception e) {
throw new IllegalArgumentException("Could not parse JSON", e);
}
return serializeObject(fieldValue, indentWidth, onlySerializePublicFields, classFieldCache);
}
/**
* Recursively serialize the named field of an object, skipping transient and final fields.
*
* @param containingObject
* The object containing the field value to serialize.
* @param fieldName
* The name of the field to serialize.
* @param indentWidth
* If indentWidth == 0, no prettyprinting indentation is performed, otherwise this specifies the
* number of spaces to indent each level of JSON.
* @param onlySerializePublicFields
* If true, only serialize public fields.
* @return The object graph in JSON form.
* @throws IllegalArgumentException
* If anything goes wrong during serialization.
*/
public static String serializeFromField(final Object containingObject, final String fieldName,
final int indentWidth, final boolean onlySerializePublicFields) {
// Don't need to resolve types during serialization
final ClassFieldCache classFieldCache = new ClassFieldCache(/* resolveTypes = */ false,
onlySerializePublicFields);
return serializeFromField(containingObject, fieldName, indentWidth, onlySerializePublicFields,
classFieldCache);
}
}