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.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 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 (jdereg@gmail.com)
 *

 *         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
{
    /** If set, this maps class ==> CustomReader */
    public static final String CUSTOM_READER_MAP = "CUSTOM_READERS";
    /** If set, this indicates that no custom reader should be used for the specified class ==> CustomReader */
    public static final String NOT_CUSTOM_READER_MAP = "NOT_CUSTOM_READERS";
    /** If set, the read-in JSON will be turned into a Map of Maps (JsonObject) representation */
    public static final String USE_MAPS = "USE_MAPS";
    /** What to do when an object is found and 'type' cannot be determined. */
    public static final String UNKNOWN_OBJECT = "UNKNOWN_OBJECT";
    /** Will fail JSON parsing if 'type' class defined but is not on classpath. */
    public static final String FAIL_ON_UNKNOWN_TYPE = "FAIL_ON_UNKNOWN_TYPE";
    /** Pointer to 'this' (automatically placed in the Map) */
    public static final String JSON_READER = "JSON_READER";
    /** Pointer to the current ObjectResolver (automatically placed in the Map) */
    public static final String OBJECT_RESOLVER = "OBJECT_RESOLVER";
    /** If set, this map will be used when writing @type values - allows short-hand abbreviations type names */
    public static final String TYPE_NAME_MAP = "TYPE_NAME_MAP";
    /** If set, this object will be called when a field is present in the JSON but missing from the corresponding class */
    public static final String MISSING_FIELD_HANDLER = "MISSING_FIELD_HANDLER";
    /** If set, use the specified ClassLoader */
    public static final String CLASSLOADER = "CLASSLOADER";
    /** This map is the reverse of the TYPE_NAME_MAP (value ==> key) */
    static final String TYPE_NAME_MAP_REVERSE = "TYPE_NAME_MAP_REVERSE";

    private static Map<Class, JsonClassReaderBase> BASE_READERS;
    protected final Map<Class, JsonClassReaderBase> readers = new HashMap<Class, JsonClassReaderBase>(BASE_READERS);
    protected MissingFieldHandler missingFieldHandler;
    protected final Set<Class> notCustom = new HashSet<Class>();
    private static final Map<String, Factory> factory = new ConcurrentHashMap<String, Factory>();
    private final Map<Long, JsonObject> objsRead = new HashMap<Long, JsonObject>();
    private final FastPushbackReader input;
    /** _args is using ThreadLocal so that static inner classes can have access to them */
    private final Map<String, Object> args = new HashMap<String, Object>();

    private static volatile boolean lenient = false;
    /**
     * @return the lenient
     */
    public static boolean isLenient() {
        return lenient;
    }

    /**
     * Set the reader to be out of RFC 4627: it will accept "NaN", "-Infinity" and "Infinity" values.
     * @param lenient the lenient to set
     */
    public static void setLenient(boolean lenient) {
        JsonReader.lenient = lenient;
    }

    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);

        Map<Class, JsonClassReaderBase> temp = new HashMap<Class, JsonClassReaderBase>();
        temp.put(String.class, new Readers.StringReader());
        temp.put(Date.class, new Readers.DateReader());
        temp.put(AtomicBoolean.class, new Readers.AtomicBooleanReader());
        temp.put(AtomicInteger.class, new Readers.AtomicIntegerReader());
        temp.put(AtomicLong.class, new Readers.AtomicLongReader());
        temp.put(BigInteger.class, new Readers.BigIntegerReader());
        temp.put(BigDecimal.class, new Readers.BigDecimalReader());
        temp.put(java.sql.Date.class, new Readers.SqlDateReader());
        temp.put(Timestamp.class, new Readers.TimestampReader());
        temp.put(Calendar.class, new Readers.CalendarReader());
        temp.put(TimeZone.class, new Readers.TimeZoneReader());
        temp.put(Locale.class, new Readers.LocaleReader());
        temp.put(Class.class, new Readers.ClassReader());
        temp.put(StringBuilder.class, new Readers.StringBuilderReader());
        temp.put(StringBuffer.class, new Readers.StringBufferReader());
        BASE_READERS = temp;
    }

    /**
     * Common ancestor for ClassFactory and ClassFactoryEx.
     */
    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);
    }

    /**
     * Used to react to fields missing when reading an object. This method will be called after all deserialization has
     * occured to allow all ref to be resolved.
     *

     * Used in conjunction with {@link JsonReader#MISSING_FIELD_HANDLER}.
     */
    public interface MissingFieldHandler
    {

        /**
         * Notify that a field is missing.

         * Warning : not every type can be deserialized upon missing fields. Arrays and Object type that do not have
         * serialized @type definition will be ignored.
         *
         * @param object the object that contains the missing field
         * @param fieldName name of the field to be replaced
         * @param value current value of the field
         */
        void fieldMissing(Object object, String fieldName, Object value);

    }

    /**
     * Common ancestor for JsonClassReader and JsonClassReaderEx.
     */
    public interface JsonClassReaderBase  { }

    /**
     * Implement this interface to add a custom JSON reader.
     */
    public interface JsonClassReader extends JsonClassReaderBase
    {
        /**
         * @param jOb Object being read.  Could be a fundamental JSON type (String, long, boolean, double, null, or JsonObject)
         * @param stack Deque of objects that have been read (Map of Maps view).
         * @return Object you wish to convert the jOb value into.
         */
        Object read(Object jOb, Deque<JsonObject<String, Object>> stack);
    }

    /**
     * Implement this interface to add a custom JSON reader.
     */
    public interface JsonClassReaderEx extends JsonClassReaderBase
    {
        /**
         * @param jOb Object being read.  Could be a fundamental JSON type (String, long, boolean, double, null, or JsonObject)
         * @param stack Deque of objects that have been read (Map of Maps view).
         * @param args Map of argument settings that were passed to JsonReader when instantiated.
         * @return Java Object you wish to convert the the passed in jOb into.
         */
        Object read(Object jOb, Deque<JsonObject<String, Object>> stack, Map<String, Object> args);

        /**
         * Allow custom readers to have access to the JsonReader
         */
        class Support
        {
            /**
             * Call this method to get an instance of the JsonReader (if needed) inside your custom reader.
             * @param args Map that was passed to your read(jOb, stack, args) method.
             * @return JsonReader instance
             */
            public static JsonReader getReader(Map<String, Object> 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).  Used
     * internally to handle Map, SortedMap when they are within parameterized types.
     */
    public static class MapFactory implements ClassFactory
    {
        /**
         * @param c Map interface that was requested for instantiation.
         * @return a concrete Map type.
         */
        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 n Class name to assign an ClassFactory to
     * @param f ClassFactory that will create 'c' instances
     */
    public static void assignInstantiator(String n, Factory f)
    {
        factory.put(n, f);
    }

    /**
     * Assign instantiated by Class. Falls back to JsonReader.assignInstantiator(String, Factory)
     * @param c Class to assign an ClassFactory to
     * @param f ClassFactory that will create 'c' instances
     */
    public static void assignInstantiator(Class c, Factory f)
    {
        assignInstantiator(c.getName(), f);
    }

    /**
     * Call this method to add a 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);
    }

    /**
     * Call this method to add a 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.  This method will add
     * the customer reader such that it will be there permanently, for the
     * life of the JVM (static).
     * @param c Class to assign a custom JSON reader to
     * @param reader The JsonClassReader which will read the custom JSON format of 'c'
     */
    public static void addReaderPermanent(Class c, JsonClassReaderBase reader)
    {
        BASE_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);
    }

    MissingFieldHandler getMissingFieldHandler()
    {
        return missingFieldHandler;
    }

    public void setMissingFieldHandler(MissingFieldHandler handler)
    {
        missingFieldHandler = handler;
    }

    /**
     * @return The arguments used to configure the JsonReader.  These are thread local.
     */
    public Map<String, Object> 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<String, Object> optionalArgs)
    {
        if (optionalArgs == null)
        {
            optionalArgs = new HashMap<String, Object>();
            optionalArgs.put(USE_MAPS, false);
        }
        if (!optionalArgs.containsKey(USE_MAPS))
        {
            optionalArgs.put(USE_MAPS, false);
        }
        JsonReader jr = new JsonReader(json, 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<String, Object> optionalArgs)
    {
        if (optionalArgs == null)
        {
            optionalArgs = new HashMap<String, Object>();
            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
     * @param json String of JSON content
     * @return Map representing JSON content.  Each object is represented by a Map.
     */
    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
     * @param json String of JSON content
     * @param optionalArgs Map of optional arguments to control customization.  See readme file for
     * details on these options.
     * @return Map where each Map representa an object in the JSON input.
     */
    public static Map jsonToMaps(String json, Map<String, Object> optionalArgs)
    {
        try
        {
            if (optionalArgs == null)
            {
                optionalArgs = new HashMap<String, Object>();
            }
            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
     * @param inputStream containing JSON content
     * @param optionalArgs Map of optional arguments to control customization.  See readme file for
     * details on these options.
     * @return Map containing the content from the JSON input.  Each Map represents an object from the input.
     */
    public static Map jsonToMaps(InputStream inputStream, Map<String, Object> optionalArgs)
    {
        if (optionalArgs == null)
        {
            optionalArgs = new HashMap<String, Object>();
        }
        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<String, Object> retMap = new JsonObject<String, Object>();
            retMap.put("@items", ret);
            return retMap;
        }
        JsonObject<String, Object> retMap = new JsonObject<String, Object>();
        retMap.put("@items", new Object[]{ret});
        return retMap;
    }

    public JsonReader()
    {
        input = null;
        getArgs().put(USE_MAPS, false);
        getArgs().put(CLASSLOADER, JsonReader.class.getClassLoader());
    }

    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<String, Object> 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<String, Object> args, boolean useMaps)
    {
        args.put(USE_MAPS, useMaps);
        return args;
    }

    public JsonReader(InputStream inp, boolean useMaps)
    {
        this(inp, makeArgMap(new HashMap<String, Object>(), useMaps));
    }

    public JsonReader(InputStream inp, Map<String, Object> optionalArgs)
    {
        initializeFromArgs(optionalArgs);

        try
        {
            input = new FastPushbackBufferedReader(new InputStreamReader(inp, "UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            throw new JsonIoException("Your JVM does not support UTF-8.  Get a better JVM.", e);
        }
    }

    public JsonReader(String inp, Map<String, Object> optionalArgs)
    {
        initializeFromArgs(optionalArgs);
        try
        {
            byte[] bytes = inp.getBytes("UTF-8");
            input = new FastPushbackBufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), "UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            throw new JsonIoException("Could not convert JSON to Maps because your JVM does not support UTF-8", e);
        }
    }

    public JsonReader(byte[] inp, Map<String, Object> optionalArgs)
    {
        initializeFromArgs(optionalArgs);
        try
        {
            input = new FastPushbackBufferedReader(new InputStreamReader(new ByteArrayInputStream(inp), "UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            throw new JsonIoException("Could not convert JSON to Maps because your JVM does not support UTF-8", e);
        }
    }

    private void initializeFromArgs(Map<String, Object> optionalArgs)
    {
        if (optionalArgs == null)
        {
            optionalArgs = new HashMap();
        }
        Map<String, Object> args = getArgs();
        args.putAll(optionalArgs);
        args.put(JSON_READER, this);
        if (!args.containsKey(CLASSLOADER))
        {
            args.put(CLASSLOADER, JsonReader.class.getClassLoader());
        }
        Map<String, String> typeNames = (Map<String, String>) 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<String, String> typeNameMap = new HashMap<String, String>();
            for (Map.Entry<String, String> entry : typeNames.entrySet())
            {
                typeNameMap.put(entry.getValue(), entry.getKey());
            }
            args.put(TYPE_NAME_MAP_REVERSE, typeNameMap); // replace with our reversed Map.
        }

        setMissingFieldHandler((MissingFieldHandler) args.get(MISSING_FIELD_HANDLER));

        Map<Class, JsonClassReaderBase> customReaders = (Map<Class, JsonClassReaderBase>) args.get(CUSTOM_READER_MAP);
        if (customReaders != null)
        {
            for (Map.Entry<Class, JsonClassReaderBase> entry : customReaders.entrySet())
            {
                addReader(entry.getKey(), entry.getValue());
            }
        }

        Iterable<Class> notCustomReaders = (Iterable<Class>) args.get(NOT_CUSTOM_READER_MAP);
        if (notCustomReaders != null)
        {
            for (Class c : notCustomReaders)
            {
                addNotCustomReader(c);
            }
        }
    }

    public Map<Long, JsonObject> 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<String, Object> 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));
    }

    /**
     * @return ClassLoader to be used by Custom Writers
     */
    ClassLoader getClassLoader()
    {
        return (ClassLoader) args.get(CLASSLOADER);
    }

    /**
     * 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, (ClassLoader)args.get(CLASSLOADER));
            resolver.createJavaObjectInstance(Object.class, root);
            Object graph = resolver.convertMapsToObjects((JsonObject<String, Object>) 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.getName()))
        {
            ClassFactory cf = (ClassFactory) factory.get(c.getName());
            return cf.newInstance(c);
        }
        return MetaUtils.newInstance(c);
    }

    public static Object newInstance(Class c, JsonObject jsonObject)
    {
        if (factory.containsKey(c.getName()))
        {
            Factory cf = factory.get(c.getName());
            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.getLine() + ", col: " + input.getCol();
        }
        return msg;
    }

}


package com.cedarsoftware.util.io;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
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.Iterator;
import java.util.List;
import java.util.Map;

/**
 *
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 (jdereg@gmail.com)
 *

 *         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 ObjectResolver extends Resolver
{
    private final ClassLoader classLoader;
    protected JsonReader.MissingFieldHandler missingFieldHandler;

    /**
     * Constructor
     * @param reader JsonReader instance being used
     * @param classLoader ClassLoader that was set in the passed in 'options' arguments to JsonReader.
     */
    protected ObjectResolver(JsonReader reader, ClassLoader classLoader)
    {
        super(reader);
        this.classLoader = classLoader;
        missingFieldHandler = reader.getMissingFieldHandler();
    }

    /**
     * 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 stack   Stack (Deque) used for graph traversal.
     * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields).
     */
    public void traverseFields(final Deque<JsonObject<String, Object>> stack, final JsonObject<String, Object> jsonObj)
    {
        final Object javaMate = jsonObj.target;
        final Iterator<Map.Entry<String, Object>> i = jsonObj.entrySet().iterator();
        final Class cls = javaMate.getClass();

        while (i.hasNext())
        {
            Map.Entry<String, Object> e = i.next();
            String key = e.getKey();
            final Field field = MetaUtils.getField(cls, key);
            Object rhs = e.getValue();
            if (field != null)
            {
                assignField(stack, jsonObj, field, rhs);
            }
            else if (missingFieldHandler != null)
            {
                handleMissingField(stack, jsonObj, rhs, key);
            }//else no handler so ignor.
        }
    }

    /**
     * Map Json Map object field to Java object field.
     *
     * @param stack   Stack (Deque) used for graph traversal.
     * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields).
     * @param field   a Java Field object representing where the jsonObj should be converted and stored.
     * @param rhs     the JSON value that will be converted and stored in the 'field' on the associated
     *                Java target object.
     */
    protected void assignField(final Deque<JsonObject<String, Object>> stack, final JsonObject jsonObj,
                               final Field field, final Object rhs)
    {
        final Object target = jsonObj.target;
        try
        {
            final Class fieldType = field.getType();
            if (rhs == null)
            {   // Logically clear field (allows null to be set against primitive fields, yielding their zero value.
                if (fieldType.isPrimitive())
                {
                    field.set(target, MetaUtils.convert(fieldType, "0"));
                }
                else
                {
                    field.set(target, null);
                }
                return;
            }

            // If there is a "tree" of objects (e.g, Map<String, List<Person>>), the subobjects may not have an
            // @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 (field.getGenericType() instanceof ParameterizedType)
                {   // Only JsonObject instances could contain unmarked objects.
                    markUntypedObjects(field.getGenericType(), rhs, MetaUtils.getDeepDeclaredFields(fieldType));
                }

                // Ensure .type field set on JsonObject
                final JsonObject job = (JsonObject) rhs;
                final String type = job.type;
                if (type == null || type.isEmpty())
                {
                    job.setType(fieldType.getName());
                }
            }

            Object special;
            if (rhs == JsonParser.EMPTY_OBJECT)
            {
                final JsonObject jObj = new JsonObject();
                jObj.type = fieldType.getName();
                Object value = createJavaObjectInstance(fieldType, jObj);
                field.set(target, value);
            }
            else if ((special = readIfMatching(rhs, fieldType, stack)) != null)
            {
                field.set(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<String, Object> jsonArray = new JsonObject<String, Object>();
                if (char[].class == fieldType)
                {   // Specially handle char[] because we are writing these
                    // out as UTF8 strings for compactness and speed.
                    if (elements.length == 0)
                    {
                        field.set(target, new char[]{});
                    }
                    else
                    {
                        field.set(target, ((String) elements[0]).toCharArray());
                    }
                }
                else
                {
                    jsonArray.put("@items", elements);
                    createJavaObjectInstance(fieldType, jsonArray);
                    field.set(target, jsonArray.target);
                    stack.addFirst(jsonArray);
                }
            }
            else if (rhs instanceof JsonObject)
            {
                final JsonObject<String, Object> jObj = (JsonObject) rhs;
                final Long ref = jObj.getReferenceId();

                if (ref != null)
                {    // Correct field references
                    final JsonObject refObject = getReferencedObj(ref);

                    if (refObject.target != null)
                    {
                        field.set(target, refObject.target);
                    }
                    else
                    {
                        unresolvedRefs.add(new UnresolvedReference(jsonObj, field.getName(), ref));
                    }
                }
                else
                {    // Assign ObjectMap's to Object (or derived) fields
                    field.set(target, createJavaObjectInstance(fieldType, jObj));
                    if (!MetaUtils.isLogicalPrimitive(jObj.getTargetClass()))
                    {
                        stack.addFirst((JsonObject) rhs);
                    }
                }
            }
            else
            {
                if (MetaUtils.isPrimitive(fieldType))
                {
                    field.set(target, MetaUtils.convert(fieldType, rhs));
                }
                else if (rhs instanceof String && "".equals(((String) rhs).trim()) && fieldType != String.class)
                {   // Allow "" to null out a non-String field
                    field.set(target, null);
                }
                else
                {
                    field.set(target, rhs);
                }
            }
        }
        catch (Exception e)
        {
            String message = e.getClass().getSimpleName() + " setting field '" + field.getName() + "' on target: " + safeToString(target) + " with value: " + rhs;
            if (MetaUtils.loadClassException != null)
            {
                message += " Caused by: " + MetaUtils.loadClassException + " (which created a LinkedHashMap instead of the desired class)";
            }
            throw new JsonIoException(message, e);
        }
    }

    /**
     * Try to create an java object from the missing field.
	 * Mosly 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 stack Stack (Deque) used for graph traversal.
     * @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 Deque<JsonObject<String, Object>> stack, final JsonObject jsonObj, final Object rhs,
                                      final String missingField)
    {
        final Object target = jsonObj.target;
        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 (rhs == JsonParser.EMPTY_OBJECT)
            {
                storeMissingField(target, missingField, null);
            }
            else if ((special = readIfMatching(rhs, null, stack)) != 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<String, Object> jObj = (JsonObject) rhs;
                final Long ref = jObj.getReferenceId();

                if (ref != null)
                { // Correct field references
                    final JsonObject refObject = getReferencedObj(ref);
                    storeMissingField(target, missingField, refObject.target);
                }
                else
                {   // Assign ObjectMap's to Object (or derived) fields
                    // check that jObj as a type
                    if (jObj.getType() != null)
                    {
                        Object createJavaObjectInstance = createJavaObjectInstance(null, jObj);
                        if (!MetaUtils.isLogicalPrimitive(jObj.getTargetClass()))
                        {
                            stack.addFirst((JsonObject) rhs);
                        }
                        storeMissingField(target, missingField, createJavaObjectInstance);
                    }
                    else //no type found, just notify.
                    {
                        storeMissingField(target, missingField, null);
                    }
                }
            }
            else
            {
                storeMissingField(target, missingField, rhs);
            }
        }
        catch (Exception e)
        {
            String message = e.getClass().getSimpleName() + " missing field '" + missingField + "' on target: "
                    + safeToString(target) + " with value: " + rhs;
            if (MetaUtils.loadClassException != null)
            {
                message += " Caused by: " + MetaUtils.loadClassException
                        + " (which created a LinkedHashMap instead of the desired class)";
            }
            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 indexable collection, the unresolved references are set
     * back into the proper element location.  For non-indexable 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 Deque<JsonObject<String, Object>> stack, final JsonObject<String, Object> jsonObj)
    {
        final Object[] items = jsonObj.getArray();
        if (items == null || items.length == 0)
        {
            return;
        }
        final Collection col = (Collection) jsonObj.target;
        final boolean isList = col instanceof List;
        int idx = 0;

        for (final Object element : items)
        {
            Object special;
            if (element == null)
            {
                col.add(null);
            }
            else if (element == JsonParser.EMPTY_OBJECT)
            {   // Handles {}
                col.add(new JsonObject());
            }
            else if ((special = readIfMatching(element, null, stack)) != 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(element);
            }
            else if (element.getClass().isArray())
            {
                final JsonObject jObj = new JsonObject();
                jObj.put("@items", element);
                createJavaObjectInstance(Object.class, jObj);
                col.add(jObj.target);
                convertMapsToObjects(jObj);
            }
            else // if (element instanceof JsonObject)
            {
                final JsonObject jObj = (JsonObject) element;
                final Long ref = jObj.getReferenceId();

                if (ref != null)
                {
                    JsonObject refObject = getReferencedObj(ref);

                    if (refObject.target != null)
                    {
                        col.add(refObject.target);
                    }
                    else
                    {
                        unresolvedRefs.add(new UnresolvedReference(jsonObj, idx, ref));
                        if (isList)
                        {   // Indexable collection, so set 'null' as element for now - will be patched in later.
                            col.add(null);
                        }
                    }
                }
                else
                {
                    createJavaObjectInstance(Object.class, jObj);

                    if (!MetaUtils.isLogicalPrimitive(jObj.getTargetClass()))
                    {
                        convertMapsToObjects(jObj);
                    }
                    col.add(jObj.target);
                }
            }
            idx++;
        }

        jsonObj.remove("@items");   // 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 stack   a Stack (Deque) used to support graph traversal.
     * @param jsonObj a Map-of-Map representation of the JSON input stream.
     */
    protected void traverseArray(final Deque<JsonObject<String, Object>> stack, final JsonObject<String, Object> jsonObj)
    {
        final int len = jsonObj.getLength();
        if (len == 0)
        {
            return;
        }

        final Class compType = jsonObj.getComponentType();

        if (char.class == compType)
        {
            return;
        }

        if (byte.class == compType)
        {   // Handle byte[] special for performance boost.
            jsonObj.moveBytesToMate();
            jsonObj.clearArray();
            return;
        }

        final boolean isPrimitive = MetaUtils.isPrimitive(compType);
        final Object array = jsonObj.target;
        final Object[] items =  jsonObj.getArray();

        for (int i=0; i < len; i++)
        {
            final Object element = items[i];

            Object special;
            if (element == null)
            {
                Array.set(array, i, null);
            }
            else if (element == JsonParser.EMPTY_OBJECT)
            {    // Use either explicitly defined type in ObjectMap associated to JSON, or array component type.
                Object arrayElement = createJavaObjectInstance(compType, new JsonObject());
                Array.set(array, i, arrayElement);
            }
            else if ((special = readIfMatching(element, compType, stack)) != null)
            {
                Array.set(array, i, special);
            }
            else if (isPrimitive)
            {   // Primitive component type array
                Array.set(array, i, MetaUtils.convert(compType, element));
            }
            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
                {
                    JsonObject<String, Object> jsonObject = new JsonObject<String, Object>();
                    jsonObject.put("@items", element);
                    Array.set(array, i, createJavaObjectInstance(compType, jsonObject));
                    stack.addFirst(jsonObject);
                }
            }
            else if (element instanceof JsonObject)
            {
                JsonObject<String, Object> jsonObject = (JsonObject<String, Object>) element;
                Long ref = jsonObject.getReferenceId();

                if (ref != null)
                {    // Connect reference
                    JsonObject refObject = getReferencedObj(ref);
                    if (refObject.target != null)
                    {   // Array element with reference to existing object
                        Array.set(array, i, refObject.target);
                    }
                    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
                    Object arrayElement = createJavaObjectInstance(compType, jsonObject);
                    Array.set(array, i, arrayElement);
                    if (!MetaUtils.isLogicalPrimitive(arrayElement.getClass()))
                    {    // Skip walking primitives, primitive wrapper classes, Strings, and Classes
                        stack.addFirst(jsonObject);
                    }
                }
            }
            else
            {
                if (element instanceof String && "".equals(((String) element).trim()) && 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.clearArray();
    }

    /**
     * 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 compType Class destination type to which the passed in object should be converted to.
     * @param stack   a Stack (Deque) used to support graph traversal.
     * @return Java object converted from the passed in object o, or if there is no custom reader.
     */
    protected Object readIfMatching(final Object o, final Class compType, final Deque<JsonObject<String, Object>> stack)
    {
        if (o == null)
        {
            throw new JsonIoException("Bug in json-io, null must be checked before calling this method.");
        }

        if (compType != null && notCustom(compType))
        {
            return null;
        }

        final boolean isJsonObject = o instanceof JsonObject;
        if (!isJsonObject && compType == null)
        {   // If not a JsonObject (like a Long that represents a date, then compType must be set)
            return null;
        }

        Class c;
        boolean needsType = false;

        // Set up class type to check against reader classes (specified as @type, or jObj.target, or compType)
        if (isJsonObject)
        {
            JsonObject jObj = (JsonObject) o;
            if (jObj.isReference())
            {
                return null;
            }

            if (jObj.target == null)
            {   // '@type' parameter used (not target instance)
                String typeStr = null;
                try
                {
                    Object type =  jObj.type;
                    if (type != null)
                    {
                        typeStr = (String) type;
                        c = MetaUtils.classForName((String) type, classLoader);
                    }
                    else
                    {
                        if (compType != null)
                        {
                            c = compType;
                            needsType = true;
                        }
                        else
                        {
                            return null;
                        }
                    }
                    createJavaObjectInstance(c, jObj);
                }
                catch(Exception e)
                {
                    throw new JsonIoException("Class listed in @type [" + typeStr + "] is not found", e);
                }
            }
            else
            {   // Type inferred from target object
                c = jObj.target.getClass();
            }
        }
        else
        {
            c = compType;
        }

        if (notCustom(c))
        {
            return null;
        }

        JsonReader.JsonClassReaderBase closestReader = getCustomReader(c);

        if (closestReader == null)
        {
            return null;
        }

        if (needsType)
        {
            ((JsonObject)o).setType(c.getName());
        }

        Object read;
        if (closestReader instanceof JsonReader.JsonClassReaderEx)
        {
            read = ((JsonReader.JsonClassReaderEx)closestReader).read(o, stack, getReader().getArgs());
        }
        else
        {
            read = ((JsonReader.JsonClassReader)closestReader).read(o, stack);
        }
		return read;
    }

    private void markUntypedObjects(final Type type, final Object rhs, final Map<String, Field> classFields)
    {
        final Deque<Object[]> stack = new ArrayDeque<Object[]>();
        stack.addFirst(new Object[] {type, rhs});

        while (!stack.isEmpty())
        {
            Object[] item = stack.removeFirst();
            final Type t = (Type) item[0];
            final Object instance = item[1];
            if (t instanceof ParameterizedType)
            {
                final Class clazz = getRawType(t);
                final ParameterizedType pType = (ParameterizedType)t;
                final Type[] typeArgs = pType.getActualTypeArguments();

                if (typeArgs == null || typeArgs.length < 1 || clazz == null)
                {
                    continue;
                }

                stampTypeOnJsonObject(instance, t);

                if (Map.class.isAssignableFrom(clazz))
                {
                    Map map = (Map) instance;
                    if (!map.containsKey("@keys") && !map.containsKey("@items") && map instanceof JsonObject)
                    {   // Maps created in Javascript will come over without @keys / @items.
                        convertMapToKeysItems((JsonObject) map);
                    }

                    Object[] keys = (Object[])map.get("@keys");
                    getTemplateTraverseWorkItem(stack, keys, typeArgs[0]);

                    Object[] items = (Object[])map.get("@items");
                    getTemplateTraverseWorkItem(stack, 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];
                            stack.addFirst(new Object[]{t, vals});

                            if (vals instanceof JsonObject)
                            {
                                stack.addFirst(new Object[]{t, vals});
                            }
                            else if (vals instanceof Object[])
                            {
                                JsonObject coll = new JsonObject();
                                coll.type = clazz.getName();
                                List items = Arrays.asList((Object[]) vals);
                                coll.put("@items", items.toArray());
                                stack.addFirst(new Object[]{t, items});
                                array[i] = coll;
                            }
                            else
                            {
                                stack.addFirst(new Object[]{t, vals});
                            }
                        }
                    }
                    else if (instance instanceof Collection)
                    {
                        final Collection col = (Collection)instance;
                        for (Object o : col)
                        {
                            stack.addFirst(new Object[]{typeArgs[0], o});
                        }
                    }
                    else if (instance instanceof JsonObject)
                    {
                        final JsonObject jObj = (JsonObject) instance;
                        final Object[] array = jObj.getArray();
                        if (array != null)
                        {
                            for (Object o : array)
                            {
                                stack.addFirst(new Object[]{typeArgs[0], o});
                            }
                        }
                    }
                }
                else
                {
                    if (instance instanceof JsonObject)
                    {
                        final JsonObject<String, Object> jObj = (JsonObject) instance;

                        for (Map.Entry<String, Object> entry : jObj.entrySet())
                        {
                            final String fieldName = entry.getKey();
                            if (!fieldName.startsWith("this$"))
                            {
                                // TODO: If more than one type, need to associate correct typeArgs entry to value
                                Field field = classFields.get(fieldName);

                                if (field != null && (field.getType().getTypeParameters().length > 0 || field.getGenericType() instanceof TypeVariable))
                                {
                                    stack.addFirst(new Object[]{typeArgs[0], entry.getValue()});
                                }
                            }
                        }
                    }
                }
            }
            else
            {
                stampTypeOnJsonObject(instance, t);
            }
        }
    }

    private static void getTemplateTraverseWorkItem(final Deque<Object[]> stack, final Object[] items, final Type type)
    {
        if (items == null || items.length < 1)
        {
            return;
        }
        Class rawType = getRawType(type);
        if (rawType != null && Collection.class.isAssignableFrom(rawType))
        {
            stack.add(new Object[]{type, items});
        }
        else
        {
            for (Object o : items)
            {
                stack.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.type == null || jObj.type.isEmpty()) && jObj.target == null)
            {
                jObj.type = clazz.getName();
            }
        }
    }

    /**
     * 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
     */
    public 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;
    }
}


package com.cedarsoftware.util.io;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * This class holds a JSON object in a LinkedHashMap.
 * LinkedHashMap used to keep fields in same order as they are
 * when reflecting them in Java.  Instances of this class hold a
 * Map-of-Map representation of a Java object, read from the JSON
 * input stream.
 *
 * @param <K> field name in Map-of-Map
 * @param <V> Value
 *
 * @author John DeRegnaucourt (jdereg@gmail.com)
 *

 *         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 JsonObject<K, V> extends LinkedHashMap<K, V>
{
    static Set<String> primitives = new HashSet<String>();
    static Set<String> primitiveWrappers = new HashSet<String>();

    Object target;
    boolean isMap = false;
    String type;
    long id = -1;
    int line;
    int col;

    static
    {
        primitives.add("boolean");
        primitives.add("byte");
        primitives.add("char");
        primitives.add("double");
        primitives.add("float");
        primitives.add("int");
        primitives.add("long");
        primitives.add("short");

        primitiveWrappers.add("java.lang.Boolean");
        primitiveWrappers.add("java.lang.Byte");
        primitiveWrappers.add("java.lang.Character");
        primitiveWrappers.add("java.lang.Double");
        primitiveWrappers.add("java.lang.Float");
        primitiveWrappers.add("java.lang.Integer");
        primitiveWrappers.add("java.lang.Long");
        primitiveWrappers.add("java.lang.Short");
    }

    public long getId()
    {
        return id;
    }

    public boolean hasId()
    {
        return id != -1;
    }

    public void setType(String type)
    {
        this.type = type;
    }

    public String getType()
    {
        return type;
    }

    public Object getTarget()
    {
        return target;
    }

    public void setTarget(Object target)
    {
        this.target = target;
    }

    public Class getTargetClass()
    {
        return target.getClass();
    }

    public boolean isLogicalPrimitive()
    {
        return primitiveWrappers.contains(type) || primitives.contains(type) || "date".equals(type) ||
                "java.math.BigInteger".equals(type) || "java.math.BigDecimal".equals(type);
    }

    public Object getPrimitiveValue()
    {
        if ("boolean".equals(type) || "double".equals(type) || "long".equals(type))
        {
            return get("value");
        }
        else if ("byte".equals(type))
        {
            Number b = (Number) get("value");
            return b.byteValue();
        }
        else if ("char".equals(type))
        {
            String c = (String) get("value");
            return c.charAt(0);
        }
        else if ("float".equals(type))
        {
            Number f = (Number) get("value");
            return f.floatValue();
        }
        else if ("int".equals(type))
        {
            Number integer = (Number) get("value");
            return integer.intValue();
        }
        else if ("short".equals(type))
        {
            Number s = (Number) get("value");
            return s.shortValue();
        }
        else if ("date".equals(type))
        {
            Object date = get("value");
            if (date instanceof Long)
            {
                return new Date((Long)(date));
            }
            else if (date instanceof String)
            {
                return Readers.DateReader.parseDate((String) date);
            }
            else
            {
                throw new JsonIoException("Unknown date type: " + type);
            }
        }
        else if ("java.math.BigInteger".equals(type))
        {
            Object value = get("value");
            return Readers.bigIntegerFrom(value);
        }
        else if ("java.math.BigDecimal".equals(type))
        {
            Object value = get("value");
            return Readers.bigDecimalFrom(value);
        }
        else
        {
            throw new JsonIoException("Invalid primitive type, line " + line + ", col " + col);
        }
    }

    /**
     * @return boolean true if this object references another object, false otherwise.
     */
    public boolean isReference()
    {
        return containsKey("@ref");
    }

    public Long getReferenceId()
    {
        return (Long) get("@ref");
    }

    // Map APIs
    public boolean isMap()
    {
        return isMap || target instanceof Map;
    }

    // Collection APIs
    public boolean isCollection()
    {
        if (target instanceof Collection)
        {
            return true;
        }
        if (containsKey("@items") && !containsKey("@keys"))
        {
            return type != null && !type.contains("[");
        }
        return false;
    }

    // Array APIs
    public boolean isArray()
    {
        if (target == null)
        {
            if (type != null)
            {
                return type.contains("[");
            }
            return containsKey("@items") && !containsKey("@keys");
        }
        return target.getClass().isArray();
    }

    // Return the array that this JSON object wraps.  This is used when there is a Collection class (like ArrayList)
    // represented in the JSON.  This also occurs if a specified array type is used (not Object[], but Integer[], for
    // example).
    public Object[] getArray()
    {
        return (Object[]) get("@items");
    }

    public int getLength()
    {
        if (isArray())
        {
            if (target == null)
            {
                Object[] items = (Object[]) get("@items");
                return items == null ? 0 : items.length;
            }
            return Array.getLength(target);
        }
        if (isCollection() || isMap())
        {
            Object[] items = (Object[]) get("@items");
            return items == null ? 0 : items.length;
        }
        throw new JsonIoException("getLength() called on a non-collection, line " + line + ", col " + col);
    }

    public Class getComponentType()
    {
        return target.getClass().getComponentType();
    }

    void moveBytesToMate()
    {
        final byte[] bytes = (byte[]) target;
        final Object[] items = getArray();
        final int len = items.length;

        for (int i = 0; i < len; i++)
        {
            bytes[i] = ((Number) items[i]).byteValue();
        }
    }

    void moveCharsToMate()
    {
        Object[] items = getArray();
        if (items == null)
        {
             target = null;
        }
        else if (items.length == 0)
        {
            target = new char[0];
        }
        else if (items.length == 1)
        {
            String s = (String) items[0];
            target = s.toCharArray();
        }
        else
        {
            throw new JsonIoException("char[] should only have one String in the [], found " + items.length + ", line " + line + ", col " + col);
        }
    }

    public V put(K key, V value)
    {
        if (key == null)
        {
            return super.put(null, value);
        }

        if (key.equals("@type"))
        {
            String oldType = type;
            type = (String) value;
            return (V) oldType;
        }
        else if (key.equals("@id"))
        {
            Long oldId = id;
            id = (Long) value;
            return (V) oldId;
        }
        else if (("@items".equals(key) && containsKey("@keys")) || ("@keys".equals(key) && containsKey("@items")))
        {
            isMap = true;
        }
        return super.put(key, value);
    }

    public void clear()
    {
        super.clear();
        type = null;
    }

    void clearArray()
    {
        remove("@items");
    }

    /**
     * @return int line where this object '{' started in the JSON stream
     */
    public int getLine()
    {
        return line;
    }

    /**
     * @return int column where this object '{' started in the JSON stream
     */
    public int getCol()
    {
        return col;
    }

    public int size()
    {
        if (containsKey("@items"))
        {
            Object value = get("@items");
            if (value instanceof Object[])
            {
                return ((Object[])value).length;
            }
            else if (value == null)
            {
                return 0;
            }
            else
            {
                throw new JsonIoException("JsonObject with @items, but no array [] associated to it, line " + line + ", col " + col);
            }
        }
        else if (containsKey("@ref"))
        {
            return 0;
        }

        return super.size();
    }
}


package com.cedarsoftware.util.io;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Parse the JSON input stream supplied by the FastPushbackReader to the constructor.
 * The entire JSON input stream will be read until it is emptied: an EOF (-1) is read.
 *
 * While reading the content, Java Maps (JsonObjects) are used to hold the contents of
 * JSON objects { }.  Lists are used to hold the contents of JSON arrays.  Each object
 * that has an @id field will be copied into the supplied 'objectsMap' constructor
 * argument.  This allows the user of this class to locate any referenced object
 * directly.
 *
 * When this parser completes, the @ref (references to objects identified with @id)
 * are stored as a JsonObject with an @ref as the key and the ID value of the object.
 * No substitution has yet occurred (substituting the @ref pointers with a Java
 * reference to the actual Map (Map containing the @id)).
 *
 * @author John DeRegnaucourt (jdereg@gmail.com)
 *

 *         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.
 */
class JsonParser
{
    public static final String EMPTY_OBJECT = "~!o~";  // compared with ==
    private static final String EMPTY_ARRAY = "~!a~";  // compared with ==
    private static final int STATE_READ_START_OBJECT = 0;
    private static final int STATE_READ_FIELD = 1;
    private static final int STATE_READ_VALUE = 2;
    private static final int STATE_READ_POST_VALUE = 3;
    private static final Map<String, String> stringCache = new HashMap<String, String>();

    private final FastPushbackReader input;
    private final Map<Long, JsonObject> objsRead;
    private final StringBuilder strBuf = new StringBuilder(256);
    private final StringBuilder hexBuf = new StringBuilder();
    private final StringBuilder numBuf = new StringBuilder();
    private final boolean useMaps;
    private final Map<String, String> typeNameMap;

    static
    {
        // Save heap memory by re-using common strings (String's immutable)
        stringCache.put("", "");
        stringCache.put("true", "true");
        stringCache.put("True", "True");
        stringCache.put("TRUE", "TRUE");
        stringCache.put("false", "false");
        stringCache.put("False", "False");
        stringCache.put("FALSE", "FALSE");
        stringCache.put("null", "null");
        stringCache.put("yes", "yes");
        stringCache.put("Yes", "Yes");
        stringCache.put("YES", "YES");
        stringCache.put("no", "no");
        stringCache.put("No", "No");
        stringCache.put("NO", "NO");
        stringCache.put("on", "on");
        stringCache.put("On", "On");
        stringCache.put("ON", "ON");
        stringCache.put("off", "off");
        stringCache.put("Off", "Off");
        stringCache.put("OFF", "OFF");
        stringCache.put("@id", "@id");
        stringCache.put("@ref", "@ref");
        stringCache.put("@items", "@items");
        stringCache.put("@type", "@type");
        stringCache.put("@keys", "@keys");
        stringCache.put("0", "0");
        stringCache.put("1", "1");
        stringCache.put("2", "2");
        stringCache.put("3", "3");
        stringCache.put("4", "4");
        stringCache.put("5", "5");
        stringCache.put("6", "6");
        stringCache.put("7", "7");
        stringCache.put("8", "8");
        stringCache.put("9", "9");
    }

    JsonParser(FastPushbackReader reader, Map<Long, JsonObject> objectsMap, Map<String, Object> args)
    {
        input = reader;
        useMaps = Boolean.TRUE.equals(args.get(JsonReader.USE_MAPS));
        objsRead = objectsMap;
        typeNameMap = (Map<String, String>) args.get(JsonReader.TYPE_NAME_MAP_REVERSE);
    }

    private Object readJsonObject() throws IOException
    {
        boolean done = false;
        String field = null;
        JsonObject<String, Object> object = new JsonObject<String, Object>();
        int state = STATE_READ_START_OBJECT;
        final FastPushbackReader in = input;

        while (!done)
        {
            int c;
            switch (state)
            {
                case STATE_READ_START_OBJECT:
                    c = skipWhitespaceRead();
                    if (c == '{')
                    {
                        object.line = in.getLine();
                        object.col = in.getCol();
                        c = skipWhitespaceRead();
                        if (c == '}')
                        {    // empty object
                            return EMPTY_OBJECT;
                        }
                        in.unread(c);
                        state = STATE_READ_FIELD;
                    }
                    else
                    {
                        error("Input is invalid JSON; object does not start with '{', c=" + c);
                    }
                    break;

                case STATE_READ_FIELD:
                    c = skipWhitespaceRead();
                    if (c == '"')
                    {
                        field = readString();
                        c = skipWhitespaceRead();
                        if (c != ':')
                        {
                            error("Expected ':' between string field and value");
                        }

                        if (field.startsWith("@"))
                        {   // Expand short-hand meta keys
                            if (field.equals("@t"))
                            {
                                field = stringCache.get("@type");
                            }
                            else if (field.equals("@i"))
                            {
                                field = stringCache.get("@id");
                            }
                            else if (field.equals("@r"))
                            {
                                field = stringCache.get("@ref");
                            }
                            else if (field.equals("@k"))
                            {
                                field = stringCache.get("@keys");
                            }
                            else if (field.equals("@e"))
                            {
                                field = stringCache.get("@items");
                            }
                        }
                        state = STATE_READ_VALUE;
                    }
                    else
                    {
                        error("Expected quote");
                    }
                    break;

                case STATE_READ_VALUE:
                    if (field == null)
                    {	// field is null when you have an untyped Object[], so we place
                        // the JsonArray on the @items field.
                        field = "@items";
                    }

                    Object value = readValue(object);
                    if ("@type".equals(field) && typeNameMap != null)
                    {
                        final String substitute = typeNameMap.get(value);
                        if (substitute != null)
                        {
                            value = substitute;
                        }
                    }
                    object.put(field, value);

                    // If object is referenced (has @id), then put it in the _objsRead table.
                    if ("@id".equals(field))
                    {
                        objsRead.put((Long) value, object);
                    }
                    state = STATE_READ_POST_VALUE;
                    break;

                case STATE_READ_POST_VALUE:
                    c = skipWhitespaceRead();
                    if (c == -1)
                    {
                        error("EOF reached before closing '}'");
                    }
                    if (c == '}')
                    {
                        done = true;
                    }
                    else if (c == ',')
                    {
                        state = STATE_READ_FIELD;
                    }
                    else
                    {
                        error("Object not ended with '}'");
                    }
                    break;
            }
        }

        if (useMaps && object.isLogicalPrimitive())
        {
            return object.getPrimitiveValue();
        }

        return object;
    }

    Object readValue(JsonObject object) throws IOException
    {
        int c = skipWhitespaceRead();
        if (c == '"')
        {
            return readString();
        }
        else if (c >= '0' && c <= '9' || c == '-' || c == 'N' || c == 'I')
        {
            return readNumber(c);
        }
        switch(c)
        {
            case '{':
                input.unread('{');
                return readJsonObject();
            case '[':
                return readArray(object);
            case ']':   // empty array
                input.unread(']');
                return EMPTY_ARRAY;
            case 'f':
            case 'F':
                readToken("false");
                return Boolean.FALSE;
            case 'n':
            case 'N':
                readToken("null");
                return null;
            case 't':
            case 'T':
                readToken("true");
                return Boolean.TRUE;
            case -1:
                error("EOF reached prematurely");
        }

        return error("Unknown JSON value type");
    }

    /**
     * Read a JSON array
     */
    private Object readArray(JsonObject object) throws IOException
    {
        final List<Object> array = new ArrayList();

        while (true)
        {
            final Object o = readValue(object);
            if (o != EMPTY_ARRAY)
            {
                array.add(o);
            }
            final int c = skipWhitespaceRead();

            if (c == ']')
            {
                break;
            }
            else if (c != ',')
            {
                error("Expected ',' or ']' inside array");
            }
        }

        return array.toArray();
    }

    /**
     * Return the specified token from the reader.  If it is not found,
     * throw an IOException indicating that.  Converting to c to
     * (char) c is acceptable because the 'tokens' allowed in a
     * JSON input stream (true, false, null) are all ASCII.
     */
    private void readToken(String token) throws IOException
    {
        final int len = token.length();

        for (int i = 1; i < len; i++)
        {
            int c = input.read();
            if (c == -1)
            {
                error("EOF reached while reading token: " + token);
            }
            c = Character.toLowerCase((char) c);
            int loTokenChar = token.charAt(i);

            if (loTokenChar != c)
            {
                error("Expected token: " + token);
            }
        }
    }

    /**
     * Read a JSON number
     *
     * @param c int a character representing the first digit of the number that
     *          was already read.
     * @return a Number (a Long or a Double) depending on whether the number is
     *         a decimal number or integer.  This choice allows all smaller types (Float, int, short, byte)
     *         to be represented as well.
     * @throws IOException for stream errors or parsing errors.
     */
    private Number readNumber(int c) throws IOException
    {
        final FastPushbackReader in = input;
        final StringBuilder number = numBuf;
        number.setLength(0);
        number.appendCodePoint(c);
        boolean isFloat = false;

        if (JsonReader.isLenient() && (c == '-' || c == 'N' || c == 'I') ) {
            // Handle negativity.
            final boolean isNeg = (c == '-');
            if (isNeg) {
                // Advance to next character.
                c = input.read();
            }

            // Case "-Infinity", "Infinity" or "NaN".
            if (c == 'I') {
                // [Out of RFC 4627] accept NaN/Infinity values
                readToken("infinity");
                return (isNeg) ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
            } else if ('N' == c) {
                // [Out of RFC 4627] accept NaN/Infinity values
                readToken("nan");
                return Double.NaN;
            } else {
                // Case this is a number but not "-Infinity", like "-2". We let the normal code process.
                input.unread('-');
            }
        }

        while (true)
        {
            c = in.read();
            if ((c >= '0' && c <= '9') || c == '-' || c == '+')
            {
                number.appendCodePoint(c);
            }
            else if (c == '.' || c == 'e' || c == 'E')
            {
                number.appendCodePoint(c);
                isFloat = true;
            }
            else if (c == -1)
            {
                break;
            }
            else
            {
                in.unread(c);
                break;
            }
        }

        try
        {
            if (isFloat)
            {   // Floating point number needed
                return Double.parseDouble(number.toString());
            }
            else
            {
                return Long.parseLong(number.toString());
            }
        }
        catch (Exception e)
        {
            return (Number) error("Invalid number: " + number, e);
        }
    }

    private static final int STRING_START = 0;
    private static final int STRING_SLASH = 1;
    private static final int HEX_DIGITS = 2;

    /**
     * Read a JSON string
     * This method assumes the initial quote has already been read.
     *
     * @return String read from JSON input stream.
     * @throws IOException for stream errors or parsing errors.
     */
    private String readString() throws IOException
    {
        final StringBuilder str = strBuf;
        final StringBuilder hex = hexBuf;
        str.setLength(0);
        int state = STRING_START;
        final FastPushbackReader in = input;

        while (true)
        {
            final int c = in.read();
            if (c == -1)
            {
                error("EOF reached while reading JSON string");
            }

            if (state == STRING_START)
            {
                if (c == '"')
                {
                    break;
                }
                else if (c == '\\')
                {
                    state = STRING_SLASH;
                }
                else
                {
                    str.appendCodePoint(c);
                }
            }
            else if (state == STRING_SLASH)
            {
                switch(c)
                {
                    case '\\':
                        str.appendCodePoint('\\');
                        break;
                    case '/':
                        str.appendCodePoint('/');
                        break;
                    case '"':
                        str.appendCodePoint('"');
                        break;
                    case '\'':
                        str.appendCodePoint('\'');
                        break;
                    case 'b':
                        str.appendCodePoint('\b');
                        break;
                    case 'f':
                        str.appendCodePoint('\f');
                        break;
                    case 'n':
                        str.appendCodePoint('\n');
                        break;
                    case 'r':
                        str.appendCodePoint('\r');
                        break;
                    case 't':
                        str.appendCodePoint('\t');
                        break;
                    case 'u':
                        hex.setLength(0);
                        state = HEX_DIGITS;
                        break;
                    default:
                        error("Invalid character escape sequence specified: " + c);
                }

                if (c != 'u')
                {
                    state = STRING_START;
                }
            }
            else
            {
                if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
                {
                    hex.appendCodePoint((char) c);
                    if (hex.length() == 4)
                    {
                        int value = Integer.parseInt(hex.toString(), 16);
                        str.appendCodePoint(value);
                        state = STRING_START;
                    }
                }
                else
                {
                    error("Expected hexadecimal digits");
                }
            }
        }

        final String s = str.toString();
        final String translate =  stringCache.get(s);
        return translate == null ? s : translate;
    }

    /**
     * Read until non-whitespace character and then return it.
     * This saves extra read/pushback.
     *
     * @return int representing the next non-whitespace character in the stream.
     * @throws IOException for stream errors or parsing errors.
     */
    private int skipWhitespaceRead() throws IOException
    {
        FastPushbackReader in = input;
        int c;
        do
        {
            c = in.read();
        } while (c == ' ' || c == '\n' || c == '\r' || c == '\t');
        return c;
    }

    Object error(String msg)
    {
        throw new JsonIoException(getMessage(msg));
    }

    Object error(String msg, Exception e)
    {
        throw new JsonIoException(getMessage(msg), e);
    }

    String getMessage(String msg)
    {
        return msg + "\nline: " + input.getLine()+ ", col: " + input.getCol()+ "\n" + input.getLastSnippet();
    }
}