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

com.cedarsoftware.util.io.JsonReader Maven / Gradle / Ivy

package com.cedarsoftware.util.io;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Read an object graph in JSON format and make it available in Java objects, or
 * in a "Map of Maps." (untyped representation).  This code handles cyclic references
 * and can deserialize any Object graph without requiring a class to be 'Serializeable'
 * or have any specific methods on it.  It will handle classes with non public constructors.
 * 

* Usages: *
  • * Call the static method: {@code JsonReader.jsonToJava(String json)}. This will * return a typed Java object graph.
  • *
  • * Call the static method: {@code JsonReader.jsonToMaps(String json)}. This will * return an untyped object representation of the JSON String as a Map of Maps, where * the fields are the Map keys, and the field values are the associated Map's values. You can * call the JsonWriter.objectToJson() method with the returned Map, and it will serialize * the Graph into the equivalent JSON stream from which it was read. *
  • * Instantiate the JsonReader with an InputStream: {@code JsonReader(InputStream in)} and then call * {@code readObject()}. Cast the return value of readObject() to the Java class that was the root of * the graph. *
  • *
  • * Instantiate the JsonReader with an InputStream: {@code JsonReader(InputStream in, true)} and then call * {@code readObject()}. The return value will be a Map of Maps. *

* * @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. */ public class JsonReader implements Closeable { public static final String CUSTOM_READER_MAP = "CUSTOM_READERS"; // If set, this map specifies Class to CustomReader public static final String NOT_CUSTOM_READER_MAP = "NOT_CUSTOM_READERS"; // If set, this map specifies Class to CustomReader public static final String USE_MAPS = "USE_MAPS"; // If set, the read-in JSON will be turned into a Map of Maps (JsonObject) representation public static final String UNKNOWN_OBJECT = "UNKNOWN_OBJECT"; // What to do when an object is found and 'type' cannot be determined. public static final String JSON_READER = "JSON_READER"; // Pointer to 'this' (automatically placed in the Map) public static final String TYPE_NAME_MAP = "TYPE_NAME_MAP"; // If set, this map will be used when writing @type values - allows short-hand abbreviations type names static final String TYPE_NAME_MAP_REVERSE = "TYPE_NAME_MAP_REVERSE";// This map is the reverse of the TYPE_NAME_MAP (value -> key) protected final ConcurrentMap readers = new ConcurrentHashMap(); protected final Set notCustom = new HashSet(); private static final Map factory = new ConcurrentHashMap(); private final Map objsRead = new HashMap(); private final FastPushbackReader input; // _args is using ThreadLocal so that static inner classes can have access to them private final Map args = new HashMap(); { addReader(String.class, new Readers.StringReader()); addReader(Date.class, new Readers.DateReader()); addReader(BigInteger.class, new Readers.BigIntegerReader()); addReader(BigDecimal.class, new Readers.BigDecimalReader()); addReader(java.sql.Date.class, new Readers.SqlDateReader()); addReader(Timestamp.class, new Readers.TimestampReader()); addReader(Calendar.class, new Readers.CalendarReader()); addReader(TimeZone.class, new Readers.TimeZoneReader()); addReader(Locale.class, new Readers.LocaleReader()); addReader(Class.class, new Readers.ClassReader()); addReader(StringBuilder.class, new Readers.StringBuilderReader()); addReader(StringBuffer.class, new Readers.StringBufferReader()); } static { Factory colFactory = new CollectionFactory(); assignInstantiator(Collection.class, colFactory); assignInstantiator(List.class, colFactory); assignInstantiator(Set.class, colFactory); assignInstantiator(SortedSet.class, colFactory); Factory mapFactory = new MapFactory(); assignInstantiator(Map.class, mapFactory); assignInstantiator(SortedMap.class, mapFactory); } public interface Factory { } /** * Subclass this interface and create a class that will return a new instance of the * passed in Class (c). Your subclass will be called when json-io encounters an * the new to instantiate an instance of (c). * * Make json-io aware that it needs to call your class by calling the public * JsonReader.assignInstantiator() API. */ public interface ClassFactory extends Factory { Object newInstance(Class c); } /** * Subclass this interface and create a class that will return a new instance of the * passed in Class (c). Your subclass will be called when json-io encounters an * the new to instantiate an instance of (c). The 'args' Map passed in will * contain a 'jsonObj' key that holds the JsonObject (Map) representing the * object being converted. If you need values from the fields of this object * in order to instantiate your class, you can grab them from the JsonObject (Map). * * Make json-io aware that it needs to call your class by calling the public * JsonReader.assignInstantiator() API. */ public interface ClassFactoryEx extends Factory { Object newInstance(Class c, Map args); } /** * Implement this interface to add a custom JSON reader. */ public interface JsonClassReaderBase { } /** * Implement this interface to add a custom JSON reader. */ public interface JsonClassReader extends JsonClassReaderBase { Object read(Object jOb, Deque> stack); } /** * Implement this interface to add a custom JSON reader. */ public interface JsonClassReaderEx extends JsonClassReaderBase { Object read(Object jOb, Deque> stack, Map args); class Support { public static JsonReader getReader(Map args) { return (JsonReader) args.get(JSON_READER); } } } /** * Use to create new instances of collection interfaces (needed for empty collections) */ public static class CollectionFactory implements ClassFactory { public Object newInstance(Class c) { if (List.class.isAssignableFrom(c)) { return new ArrayList(); } else if (SortedSet.class.isAssignableFrom(c)) { return new TreeSet(); } else if (Set.class.isAssignableFrom(c)) { return new LinkedHashSet(); } else if (Collection.class.isAssignableFrom(c)) { return new ArrayList(); } throw new JsonIoException("CollectionFactory handed Class for which it was not expecting: " + c.getName()); } } /** * Use to create new instances of Map interfaces (needed for empty Maps) */ public static class MapFactory implements ClassFactory { public Object newInstance(Class c) { if (SortedMap.class.isAssignableFrom(c)) { return new TreeMap(); } else if (Map.class.isAssignableFrom(c)) { return new LinkedHashMap(); } throw new JsonIoException("MapFactory handed Class for which it was not expecting: " + c.getName()); } } /** * For difficult to instantiate classes, you can add your own ClassFactory * or ClassFactoryEx which will be called when the passed in class 'c' is * encountered. Your ClassFactory will be called with newInstance(c) and * your factory is expected to return a new instance of 'c'. * * This API is an 'escape hatch' to allow ANY object to be instantiated by JsonReader * and is useful when you encounter a class that JsonReader cannot instantiate using its * internal exhausting attempts (trying all constructors, varying arguments to them, etc.) * @param c Class to assign an ClassFactory to * @param f ClassFactory that will create 'c' instances */ public static void assignInstantiator(Class c, Factory f) { factory.put(c, f); } /** * Call this method to add your custom JSON reader to json-io. It will * associate the Class 'c' to the reader you pass in. The readers are * found with isAssignableFrom(). If this is too broad, causing too * many classes to be associated to the custom reader, you can indicate * that json-io should not use a custom reader for a particular class, * by calling the addNotCustomReader() method. * @param c Class to assign a custom JSON reader to * @param reader The JsonClassReader which will read the custom JSON format of 'c' */ public void addReader(Class c, JsonClassReaderBase reader) { readers.put(c, reader); } /** * Force json-io to use it's internal generic approach to writing the * passed in class, even if a Custom JSON reader is specified for its * parent class. * @param c Class to which to force no custom JSON reading to occur. * Normally, this is not needed, however, if a reader is assigned to a * parent class of 'c', then calling this method on 'c' will prevent * any custom reader from processing class 'c' */ public void addNotCustomReader(Class c) { notCustom.add(c); } /** * @return The arguments used to configure the JsonReader. These are thread local. */ public Map getArgs() { return args; } /** * Convert the passed in JSON string into a Java object graph. * * @param json String JSON input * @return Java object graph matching JSON input */ public static Object jsonToJava(String json) { return jsonToJava(json, null); } /** * Convert the passed in JSON string into a Java object graph. * * @param json String JSON input * @param optionalArgs Map of optional parameters to control parsing. See readme file for details. * @return Java object graph matching JSON input */ public static Object jsonToJava(String json, Map optionalArgs) { if (optionalArgs == null) { optionalArgs = new HashMap(); optionalArgs.put(USE_MAPS, false); } if (!optionalArgs.containsKey(USE_MAPS)) { optionalArgs.put(USE_MAPS, false); } ByteArrayInputStream ba; try { ba = new ByteArrayInputStream(json.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new JsonIoException("Could not convert JSON to Maps because your JVM does not support UTF-8", e); } JsonReader jr = new JsonReader(ba, optionalArgs); Object obj = jr.readObject(); jr.close(); return obj; } /** * Convert the passed in JSON string into a Java object graph. * * @param inputStream InputStream containing JSON input * @param optionalArgs Map of optional parameters to control parsing. See readme file for details. * @return Java object graph matching JSON input */ public static Object jsonToJava(InputStream inputStream, Map optionalArgs) { if (optionalArgs == null) { optionalArgs = new HashMap(); optionalArgs.put(USE_MAPS, false); } if (!optionalArgs.containsKey(USE_MAPS)) { optionalArgs.put(USE_MAPS, false); } JsonReader jr = new JsonReader(inputStream, optionalArgs); Object obj = jr.readObject(); jr.close(); return obj; } /** * Map args = ["USE_MAPS": true] * Use JsonReader.jsonToJava(String json, args) * Note that the return type will match the JSON type (array, object, string, long, boolean, or null). * No longer recommended: Use jsonToJava with USE_MAPS:true */ public static Map jsonToMaps(String json) { return jsonToMaps(json, null); } /** * Map args = ["USE_MAPS": true] * Use JsonReader.jsonToJava(String json, args) * Note that the return type will match the JSON type (array, object, string, long, boolean, or null). * No longer recommended: Use jsonToJava with USE_MAPS:true */ public static Map jsonToMaps(String json, Map optionalArgs) { try { if (optionalArgs == null) { optionalArgs = new HashMap(); } optionalArgs.put(USE_MAPS, true); ByteArrayInputStream ba = new ByteArrayInputStream(json.getBytes("UTF-8")); JsonReader jr = new JsonReader(ba, optionalArgs); Object ret = jr.readObject(); jr.close(); return adjustOutputMap(ret); } catch (UnsupportedEncodingException e) { throw new JsonIoException("Could not convert JSON to Maps because your JVM does not support UTF-8", e); } } /** * Map args = ["USE_MAPS": true] * Use JsonReader.jsonToJava(inputStream, args) * Note that the return type will match the JSON type (array, object, string, long, boolean, or null). * No longer recommended: Use jsonToJava with USE_MAPS:true */ public static Map jsonToMaps(InputStream inputStream, Map optionalArgs) { if (optionalArgs == null) { optionalArgs = new HashMap(); } optionalArgs.put(USE_MAPS, true); JsonReader jr = new JsonReader(inputStream, optionalArgs); Object ret = jr.readObject(); jr.close(); return adjustOutputMap(ret); } private static Map adjustOutputMap(Object ret) { if (ret instanceof Map) { return (Map) ret; } if (ret != null && ret.getClass().isArray()) { JsonObject retMap = new JsonObject(); retMap.put("@items", ret); return retMap; } JsonObject retMap = new JsonObject(); retMap.put("@items", new Object[]{ret}); return retMap; } public JsonReader() { input = null; getArgs().put(USE_MAPS, false); } public JsonReader(InputStream inp) { this(inp, false); } /** * Use this constructor if you already have a JsonObject graph and want to parse it into * Java objects by calling jsonReader.jsonObjectsToJava(rootJsonObject) after constructing * the JsonReader. * @param optionalArgs Map of optional arguments for the JsonReader. */ public JsonReader(Map optionalArgs) { this(new ByteArrayInputStream(new byte[]{}), optionalArgs); } // This method is needed to get around the fact that 'this()' has to be the first method of a constructor. static Map makeArgMap(Map args, boolean useMaps) { args.put(USE_MAPS, useMaps); return args; } public JsonReader(InputStream inp, boolean useMaps) { this(inp, makeArgMap(new HashMap(), useMaps)); } public JsonReader(InputStream inp, Map optionalArgs) { if (optionalArgs == null) { optionalArgs = new HashMap(); } Map args = getArgs(); args.putAll(optionalArgs); args.put(JSON_READER, this); Map typeNames = (Map) args.get(TYPE_NAME_MAP); if (typeNames != null) { // Reverse the Map (this allows the users to only have a Map from type to short-hand name, // and not keep a 2nd map from short-hand name to type. Map typeNameMap = new HashMap(); for (Map.Entry entry : typeNames.entrySet()) { typeNameMap.put(entry.getValue(), entry.getKey()); } args.put(TYPE_NAME_MAP_REVERSE, typeNameMap); // replace with our reversed Map. } Map customReaders = (Map) args.get(CUSTOM_READER_MAP); if (customReaders != null) { for (Map.Entry entry : customReaders.entrySet()) { addReader(entry.getKey(), entry.getValue()); } } Iterable notCustomReaders = (Iterable) args.get(NOT_CUSTOM_READER_MAP); if (notCustomReaders != null) { for (Class c : notCustomReaders) { addNotCustomReader(c); } } try { input = new FastPushbackReader(new BufferedReader(new InputStreamReader(inp, "UTF-8"))); } catch (UnsupportedEncodingException e) { throw new JsonIoException("Your JVM does not support UTF-8. Get a better JVM.", e); } } public Map getObjectsRead() { return objsRead; } public Object getRefTarget(JsonObject jObj) { if (!jObj.isReference()) { return jObj; } Long id = jObj.getReferenceId(); JsonObject target = objsRead.get(id); if (target == null) { throw new IllegalStateException("The JSON input had an @ref to an object that does not exist."); } return getRefTarget(target); } /** * Read JSON input from the stream that was set up in the constructor, turning it into * Java Maps (JsonObject's). Then, if requested, the JsonObjects can be converted * into Java instances. * * @return Java Object graph constructed from InputStream supplying * JSON serialized content. */ public Object readObject() { JsonParser parser = new JsonParser(input, objsRead, getArgs()); JsonObject root = new JsonObject(); Object o; try { o = parser.readValue(root); if (o == JsonParser.EMPTY_OBJECT) { return new JsonObject(); } } catch (JsonIoException e) { throw e; } catch (Exception e) { throw new JsonIoException("error parsing JSON value", e); } Object graph; if (o instanceof Object[]) { root.setType(Object[].class.getName()); root.setTarget(o); root.put("@items", o); graph = convertParsedMapsToJava(root); } else { graph = o instanceof JsonObject ? convertParsedMapsToJava((JsonObject) o) : o; } // Allow a complete 'Map' return (Javascript style) if (useMaps()) { return o; } return graph; } /** * Convert a root JsonObject that represents parsed JSON, into * an actual Java object. * @param root JsonObject instance that was the root object from the * JSON input that was parsed in an earlier call to JsonReader. * @return a typed Java instance that was serialized into JSON. */ public Object jsonObjectsToJava(JsonObject root) { getArgs().put(USE_MAPS, false); return convertParsedMapsToJava(root); } protected boolean useMaps() { return Boolean.TRUE.equals(getArgs().get(USE_MAPS)); } /** * This method converts a root Map, (which contains nested Maps * and so forth representing a Java Object graph), to a Java * object instance. The root 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 root JsonObject instance that was the root object from the * JSON input that was parsed in an earlier call to JsonReader. * @return a typed Java instance that was serialized into JSON. */ protected Object convertParsedMapsToJava(JsonObject root) { try { Resolver resolver = useMaps() ? new MapResolver(this) : new ObjectResolver(this); resolver.createJavaObjectInstance(Object.class, root); Object graph = resolver.convertMapsToObjects((JsonObject) root); resolver.cleanup(); readers.clear(); return graph; } catch (Exception e) { try { close(); } catch (Exception ignored) { // Exception handled in close() } if (e instanceof JsonIoException) { throw (JsonIoException)e; } throw new JsonIoException(getErrorMessage(e.getMessage()), e); } } public static Object newInstance(Class c) { if (factory.containsKey(c)) { ClassFactory cf = (ClassFactory) factory.get(c); return cf.newInstance(c); } return MetaUtils.newInstance(c); } public static Object newInstance(Class c, JsonObject jsonObject) { if (factory.containsKey(c)) { Factory cf = factory.get(c); if (cf instanceof ClassFactoryEx) { Map args = new HashMap(); args.put("jsonObj", jsonObject); return ((ClassFactoryEx)cf).newInstance(c, args); } if (cf instanceof ClassFactory) { return ((ClassFactory)cf).newInstance(c); } throw new JsonIoException("Unknown instantiator (Factory) class. Must subclass ClassFactoryEx or ClassFactory, found: " + cf.getClass().getName()); } return MetaUtils.newInstance(c); } public void close() { try { if (input != null) { input.close(); } } catch (Exception e) { throw new JsonIoException("Unable to close input", e); } } private String getErrorMessage(String msg) { if (input != null) { return msg + "\nLast read: " + input.getLastSnippet() + "\nline: " + input.line + ", col: " + input.col; } return msg; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy