Open Source Repository

Home /json/flexjson-2.1 | Repository Home


flexjson/JSONDeserializer.java
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 deserializeString input ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bindnew JSONTokenerinput ).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 deserializeReader input ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bindnew JSONTokenerinput ).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 deserializeString input, Class root ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bindnew JSONTokenerinput ).nextValue(), root );
    }

    /**
     * Same as {@link #deserialize(java.io.Reader, 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 deserializeReader input, Class root ) {
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bindnew JSONTokenerinput ).nextValue(), 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 deserializeString input, ObjectFactory factory ) {
        use( (String)null, factory );
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bindnew JSONTokenerinput ).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 deserializeReader input, ObjectFactory factory ) {
        use( (String)null, factory );
        ObjectBinder binder = createObjectBinder();
        return (T)binder.bindnew JSONTokenerinput ).nextValue() );
    }

    /**
     * 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 deserializeIntoString input, T target ) {
        return deserializeinput, 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 deserializeIntoReader input, T target ) {
        return deserializeinput, new ExistingObjectFactory(target) );
    }

    public JSONDeserializer<T> useString path, ClassLocator locator ) {
        pathFactories.putPath.parse(path)new ClassLocatorObjectFactorylocator ) );
        return this;
    }

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

    public JSONDeserializer<T> useClass clazz, ObjectFactory factory ) {
        typeFactories.putclazz, factory );
        return this;
    }

    public JSONDeserializer<T> useString path, ObjectFactory factory ) {
        pathFactories.putPath.parsepath ), factory );
        return this;
    }

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

    private ObjectBinder createObjectBinder() {
        ObjectBinder binder = new ObjectBinder();
        forClass clazz : typeFactories.keySet() ) {
            binder.useclazz, typeFactories.get(clazz) );
        }
        forPath p : pathFactories.keySet() ) {
            binder.usep, pathFactories.get) );
        }
        return binder;
    }

}