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(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 deserialize( Reader input, Class root ) {
ObjectBinder binder = createObjectBinder();
return (T)binder.bind( new JSONTokener( input ).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 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() );
}
/**
* 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) );
}
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 );
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;
}
}
|