Package flexjson

Source Code of flexjson.JSONDeserializer

package flexjson;

import flexjson.factories.ClassLocatorObjectFactory;
import flexjson.factories.ExistingObjectFactory;
import flexjson.locators.StaticClassLocator;

import java.io.Reader;
import java.util.Map;
import java.util.HashMap;

/**
* <p>
* JSONDeserializer takes as input a json string and produces a static typed object graph from that
* json representation.  By default it uses the class property in the json data in order to map the
* untyped generic json data into a specific Java type.  However, you are limited to only json strings
* with class information embedded when resolving it into a Java type.  But, for now let's just look at
* the simplest case of class attributes in your json.  We'll look at how {@link JSONSerializer} and
* JSONDeserializer pair together out of the box.
* </p>
* <p>
* Say we have a simple object like Hero (see the superhero package under the test and mock).
* To create a json represenation of Hero we'd do the following:
* </p>
*
* <pre>
*   Hero harveyBirdman = new Hero("Harvey Birdman", new SecretIdentity("Attorney At Law"), new SecretLair("Sebben & Sebben") );
*   String jsonHarvey = new JSONSerialize().serialize(hero);
* </pre>
* <p>
* Now to reconsitute Harvey to fight for the law we'd use JSONDeserializer like so:
* </p>
* <pre>
*   Hero hero = new JSONDeserializer<Hero>().deserialize( jsonHarvey );
* </pre>
* <p>
* Pretty easy when all the type information is included with the JSON data.  Now let's look at the more difficult
* case of how we might reconstitute something missing type info.
* </p>
* <p>
* Let's exclude the class attribute in our json like so:
* </p>
*
* <pre>
*   String jsonHarvey = new JSONSerialize().exclude("*.class").serialize(hero);
* </pre>
* <p>
* The big trick here is to replace that type information when we instantiate the deserializer.
* To do that we'll use the {@link flexjson.JSONDeserializer#use(String, Class)} method like so:
* </p>
* <pre>
*   Hero hero = new JSONDeserializer<Hero>().use( null, Hero.class ).deserialize( jsonHarvey );
* </pre>
* <p>
* Like riding a horse with no saddle without our type information.  So what is happening here is we've registered
* the Hero class to the root of the json.  The {@link flexjson.JSONDeserializer#use(String, Class)} method  uses
* the object graph path to attach certain classes to those locations.  So, when the deserializer is deserializing
* it knows where it is in the object graph.  It uses that graph path to look up the java class it should use
* when reconstituting the object.
* </p>
* <p>
* Notice that in our json you'd see there is no type information in the stream.  However, all we had to do is point
* the class at the Hero object, and it figured it out.  That's because it uses the target type (in this case Hero)
* to figure out the other types by inspecting that class.  Meaning notice that we didn't have to tell it about
* SecretLair or SecretIdentity.  That's because it can figure that out from the Hero class.
* </p>
* <p>
* Pretty cool.  Where this fails is when we starting working with interfaces, abstract classes, and subclasses.
* Yea our friend polymorphism can be a pain when deserializing.  Why?  Well if you haven't realized by now
* inspecting the type from our target class won't help us because either it's not a concrete class or we
* can't tell the subclass by looking at the super class alone.  Next section we're going to stand up on our
* bare back horse.  Ready?  Let's do it.
* </p>
* <p>
* Before we showed how the {@link flexjson.JSONDeserializer#use(String, Class)} method would allow us to
* plug in a single class for a given path.  That might work when you know exactly which class you want to
* instantiate, but when the class type depends on external factors we really need a way to specify several
* possibilities.  That's where the second version of {@link flexjson.JSONDeserializer#use(String, ClassLocator)}
* comes into play.  {@link flexjson.ClassLocator} allow you to use a stradegy for finding which java Class
* you want to attach at a particular object path.
* </p>
* <p>
* {@link flexjson.JSONDeserializer#use(String, ClassLocator)} have access to the intermediate form of
* the object as a Map.  Given the Map at the object path the ClassLocator figures out which Class
* Flexjson will bind the parameters into that object.
* </p>
* <p>
* Let's take a look at how this can be done using our Hero class.  All Heros have a list of super powers.
* These super powers are things like X Ray Vision, Heat Vision, Flight, etc.  Each super power is represented
* by a subclass of SuperPower.  If we serialize a Hero without class information embedded we'll need a way to
* figure out which instance to instantiate when we deserialize.  In this example I'm going to use a Transformer
* during serialization to embed a special type information into the object.  All this transformer does is strip
* off the package information on the class property.
* </p>
* <pre>
* String json = new JSONSerializer()
*      .include("powers.class")
*      .transform( new SimpleTransformer(), "powers.class")
*      .exclude("*.class")
*      .serialize( superhero );
* Hero hero = new JSONDeserializer<Hero>()
*      .use("powers.class", new PackageClassLocator())
*      .deserialize( json );
* </pre>
* <p>
*
* </p>
* <p>
* All objects that pass through the deserializer must have a no argument constructor.  The no argument
* constructor does not have to be public.  That allows you to maintain some encapsulation.  JSONDeserializer
* will bind parameters using setter methods of the objects instantiated if available.  If a setter method
* is not available it will using reflection to set the value directly into the field.  You can use setter
* methods transform the any data from json into the object structure you want.  That way json structure
* can be different from your Java object structure.  The works very much in the same way getters do for
* the {@link flexjson.JSONSerializer}.
* </p>
* <p>
* Collections and Maps have changed the path structure in order to specify concrete classes for both
* the Collection implementation and the contained values.  Normally you would use generics to specify
* the concrete class to load.  However, if you're contained class is an interface or abstract class
* then you'll need to define those concrete classes using paths.  To specify the concrete class for
* a Collection use the path to the collection.  To specify the contained instance's concrete class
* append "values" onto the path.  For example, if your collection path is "person.friends" you can
* specify the collection type using:
* </p>
* <pre>
* new JSONDeserializer().use("person.friends", ArrayList.class).use("person.friends.values", Frienemies.class)
* </pre>
* <p>
* Notice that append "values" onto the "person.friends" to specify the class to use inside the
* Collection.  Maps have both keys and values within them.  For Maps you can specify those by
* appending "keys" and "values" to the path.
* </p>
* <p>
* Now onto the advanced topics of the deserializer.  {@link flexjson.ObjectFactory} interface is the
* underpinnings of the deserializer.  All object creation is controlled by ObjectFactories.  By default
* there are many ObjectFactories registered to handle all of the default types supported.  However, you
* can add your own implementations to handle specialized formats.  For example, say you've encoded your
* Dates using yyyy.MM.dd.  If you want to read these into java.util.Date objects you can register a
* {@link flexjson.transformer.DateTransformer} to deserialize dates into Date objects.
* </p>
*/
public class JSONDeserializer<T> {

    private Map<Class,ObjectFactory> typeFactories = new HashMap<Class,ObjectFactory>();
    private Map<Path,ObjectFactory> pathFactories = new HashMap<Path,ObjectFactory>();

    public JSONDeserializer() {
    }

    /**
     * Deserialize the given json formatted input into a Java object.
     *
     * @param input a json formatted string.
     * @return an Java instance deserialized from the json input.
     */
    public T deserialize( String input ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bind( new JSONTokener( input ).nextValue() );
    }

    /**
     * Same as {@link #deserialize(String)}, but uses an instance of
     * java.io.Reader as json input.
     *
     * @param input the stream where the json input is coming from.
     * @return an Java instance deserialized from the java.io.Reader's input.
     */
    public T deserialize( Reader input ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bind( new JSONTokener( input ).nextValue() );
    }

    /**
     * Deserialize the given json input, and use the given Class as
     * the type of the initial object to deserialize into.  This object
     * must implement a no-arg constructor.
     *
     * @param input a json formatted string.
     * @param root a Class used to create the initial object.
     * @return the object created from the given json input.
     */
    public T deserialize( String input, Class root ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bind( new JSONTokener( input ).nextValue(), root );
    }

    /**
     * Same as {@link #deserialize(String, Class)}, but uses an instance of
     * java.io.Reader as json input.
     *
     * @param input the stream where the json input is coming from.
     * @param root a Class used to create the initial object.
     * @return an Java instance deserialized from the java.io.Reader's input.
     */
    public T deserialize( Reader input, Class root ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bind( new JSONTokener( input ).nextValue(), root );
    }

    /**
     * Same as {@link #deserialize(String, Class)} but it starts binding into
     * the instance of the given Class at the given path.
     *
     * @param input a json format string.
     * @param path a path to an instance of the given class.
     * @param root the Class used to create the initial object.  Must have a no-arg constructor.
     * @return the object created from the given json input.
     */
    public T deserialize(String input, String path, Class root ) {
        ObjectBinder binder = createObjectBinder();
        Map value = (Map)new JSONTokener( input ).nextValue();
        return (T)binder.bind( value.get(path), root );
    }

    /**
     * Same as {@link #deserialize(java.io.Reader, Class)} but it starts binding into
     * the instance of the given Class at the given path.
     *
     * @param input the stream where the json input is coming from.
     * @param path a path to an instance of the given class.
     * @param root the Class used to create the initial object.  Must have a no-arg constructor.
     * @return an Java instance deserialized from the java.io.Reader's input.
     */
    public T deserialize(Reader input, String path, Class root ) {
        ObjectBinder binder = createObjectBinder();
        Map value = (Map)new JSONTokener( input ).nextValue();
        return (T)binder.bind( value.get(path), root );
    }

    /**
     * Deserialize the given json input, and use the given ObjectFactory to
     * create the initial object to deserialize into.
     *
     * @param input a json formatted string.
     * @param factory an ObjectFactory used to create the initial object.
     * @return the object created from the given json input.
     */
    public T deserialize( String input, ObjectFactory factory ) {
        use( (String)null, factory );
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bind( new JSONTokener( input ).nextValue() );
    }

    /**
     * Same as {@link #deserialize(String, ObjectFactory)}, but uses an instance of
     * java.io.Reader as json input.
     *
     * @param input the stream where the json input is coming from.
     * @param factory an ObjectFactory used to create the initial object.
     * @return an Java instance deserialized from the java.io.Reader's input.
     */
    public T deserialize( Reader input, ObjectFactory factory ) {
        use( (String)null, factory );
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bind( new JSONTokener( input ).nextValue() );
    }

    /**
     * Same as {@link #deserialize(String, ObjectFactory)}, it starts binding into
     * the instance of the given Class at the given path.
     *
     * @param input a json formatted string.
     * @param path the path two which you start binding.
     * @param factory an ObjectFactory used to create the initial object.
     * @return an Java instance deserialized from the given json input.
     */
    public T deserialize( String input, String path, ObjectFactory factory ) {
        use((String)null, factory);
        ObjectBinder binder = createObjectBinder();
        Map value = (Map)new JSONTokener(input).nextValue();
        return (T)binder.bind( value.get(path) );
    }

    /**
     * Same as {@link #deserialize(String, ObjectFactory)}, it starts binding into
     * the instance of the given Class at the given path.
     *
     * @param input the stream where the json input is coming from.
     * @param path the path two which you start binding.
     * @param factory an ObjectFactory used to create the initial object.
     * @return an Java instance deserialized from the java.io.Reader's input.
     */
    public T deserialize(Reader input, String path, ObjectFactory factory ) {
        use( (String)null, factory );
        ObjectBinder binder = createObjectBinder();
        Object value = new JSONTokener(input).nextValue();
        return (T)binder.bind( ((Map)value).get(path) );
    }

    /**
     * Deserialize the given input into the existing object target.
     * Values in the json input will overwrite values in the
     * target object.  This means if a value is included in json
     * a new object will be created and set into the existing object.
     *
     * @param input a json formatted string.
     * @param target an instance to set values into from the json string.
     * @return will return a reference to target.
     */
    public T deserializeInto( String input, T target ) {
        return deserialize( input, new ExistingObjectFactory(target) );
    }

    /**
     * Same as {@link #deserializeInto(String, Object)}, but uses an instance of
     * java.io.Reader as json input.
     *
     * @param input the stream where the json input is coming from.
     * @param target an instance to set values into from the json string.
     * @return will return a reference to target.
     */
    public T deserializeInto( Reader input, T target ) {
        return deserialize( input, new ExistingObjectFactory(target) );
    }

    /**
     * Deserialize the given input into the existing object target.
     * Values in the json input will overwrite values in the
     * target object.  This means if a value is included in json
     * a new object will be created and set into the existing object.
     *
     * @param input a json formatted string.
     * @param path the path two which you start binding.
     * @param target an instance to set values into from the json string.
     * @return will return a reference to target.
     */
    public T deserializeInto( String input, String path, T target ) {
        return deserialize( input, path, new ExistingObjectFactory(target) );
    }

    /**
     * Same as {@link #deserializeInto(String, String, Object)}, but uses an instance of
     * java.io.Reader as json input.
     *
     * @param input the stream where the json input is coming from.
     * @param path the path two which you start binding.
     * @param target an instance to set values into from the json string.
     * @return will return a reference to target.
     */
    public T deserializeInto( Reader input, String path, T target ) {
        return deserialize( input, path, new ExistingObjectFactory(target) );
    }

    public JSONDeserializer<T> use( String path, ClassLocator locator ) {
        pathFactories.put( Path.parse(path), new ClassLocatorObjectFactory( locator ) );
        return this;
    }

    public JSONDeserializer<T> use( String path, Class clazz ) {
        return use( path, new StaticClassLocator(clazz) );
    }

    public JSONDeserializer<T> use( Class clazz, ObjectFactory factory ) {
        typeFactories.put( clazz, factory );
        if( clazz == Boolean.class ) typeFactories.put(Boolean.TYPE, factory );
        else if( clazz == Integer.class ) typeFactories.put(Integer.TYPE, factory );
        else if( clazz == Short.class ) typeFactories.put(Short.TYPE, factory );
        else if( clazz == Long.class ) typeFactories.put(Long.TYPE, factory );
        else if( clazz == Byte.class ) typeFactories.put(Byte.TYPE, factory );
        else if( clazz == Float.class ) typeFactories.put(Float.TYPE, factory );
        else if( clazz == Double.class ) typeFactories.put(Double.TYPE, factory );
        else if( clazz == Character.class ) typeFactories.put(Character.TYPE, factory );
        return this;
    }

    public JSONDeserializer<T> use( String path, ObjectFactory factory ) {
        pathFactories.put( Path.parse( path ), factory );
        return this;
    }

    public JSONDeserializer<T> use(ObjectFactory factory, String... paths) {
        for( String p : paths ) {
            use( p, factory );
        }
        return this;
    }

    private ObjectBinder createObjectBinder() {
        ObjectBinder binder = new ObjectBinder();
        for( Class clazz : typeFactories.keySet() ) {
            binder.use( clazz, typeFactories.get(clazz) );
        }
        for( Path p : pathFactories.keySet() ) {
            binder.use( p, pathFactories.get( p ) );
        }
        return binder;
    }

}
TOP

Related Classes of flexjson.JSONDeserializer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.