flexjson.JSONSerializer Maven / Gradle / Ivy
/**
* Copyright 2007 Charlie Hubbard and Brandon Goodin
*
* 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.
*/
package flexjson;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import flexjson.transformer.Transformer;
import flexjson.transformer.TransformerWrapper;
import flexjson.transformer.TypeTransformerMap;
/**
*
* JSONSerializer is the main class for performing serialization of Java objects
* to JSON. JSONSerializer by default performs a shallow serialization. While
* this might seem strange there is a method to this madness. Shallow serialization
* allows the developer to control what is serialized out of the object graph.
* This helps with performance, but more importantly makes good OO possible, fixes
* the circular reference problem, and doesn't require boiler plate translation code.
* You don't have to change your object model to make JSON work so it reduces your
* work load, and keeps you
* DRY.
*
*
*
* Let's go through a simple example:
*
*
*
* JSONSerializer serializer = new JSONSerializer();
* return serializer.serialize( person );
*
*
*
*
* What this statement does is output the json from the instance of person. So
* the JSON we might see for this could look like:
*
*
*
* { "class": "com.mysite.Person",
* "firstname": "Charlie",
* "lastname": "Rose",
* "age", 23
* "birthplace": "Big Sky, Montanna"
* }
*
*
*
* In this case it's look like it's pretty standard stuff. But, let's say
* Person had many hobbies (i.e. Person.hobbies is a java.util.List). In
* this case if we executed the code above we'd still getTransformer the same output.
* This is a very important feature of flexjson, and that is any instance
* variable that is a Collection, Map, or Object reference won't be serialized
* by default. This is what gives flexjson the shallow serialization.
*
*
*
* How would we include the hobbies field? Using the {@link JSONSerializer#include}
* method allows us to include these fields in the serialization process. Here is
* how we'd do that:
*
*
*
* return new JSONSerializer().include("hobbies").serialize( person );
*
*
*
* That would produce output like:
*
*
* { "class": "com.mysite.Person",
* "firstname": "Charlie",
* "lastname": "Rose",
* "age", 23
* "birthplace": "Big Sky, Montanna",
* "hobbies", [
* "poker",
* "snowboarding",
* "kite surfing",
* "bull riding"
* ]
* }
*
*
*
*
* If the hobbies field contained objects, say Hobby instances, then a
* shallow copy of those objects would be performed. Let's go further and say
* hobbies had a List of all the people who enjoyed this hobby.
* This would create a circular reference between Person and Hobby. Since the
* shallow copy is being performed on Hobby JSONSerialize won't serialize the people
* field when serializing Hobby instances thus breaking the chain of circular references.
*
*
*
* But, for the sake of argument and illustration let's say we wanted to send the
* people field in Hobby. We can do the following:
*
*
*
* return new JSONSerializer().include("hobbies.people").serialize( person );
*
*
*
*
* JSONSerializer is smart enough to know that you want hobbies field included and
* the people field inside hobbies' instances too. The dot notation allows you
* do traverse the object graph specifying instance fields. But, remember a shallow copy
* will stop the code from getting into an infinte loop.
*
*
*
* You can also use the exclude method to exclude fields that would be included. Say
* we have a User object. It would be a serious security risk if we sent the password
* over the network. We can use the exclude method to prevent the password field from
* being sent.
*
*
*
* return new JSONSerialize().exclude("password").serialize(user);
*
*
*
*
* JSONSerializer will also pay attention to any method or field annotated by
* {@link flexjson.JSON}. You can include and exclude fields permenantly using the
* annotation. This is good like in the case of User.password which should never
* ever be sent through JSON. However, fields like hobbies or
* favoriteMovies depends on the situation so it's best NOT to annotate
* those fields, and use the {@link JSONSerializer#include} method.
*
*
*
* In a shallow copy only these types of instance fields will be sent:
* String, Date, Number,
* Boolean, Character, Enum,
* Object and null. Subclasses of Object will be serialized
* except for Collection or Arrays. Anything that would cause a N objects would not be sent.
* All types will be excluded by default. Fields marked static or transient are not serialized.
*
*
* Includes and excludes can include wildcards. Wildcards allow you to do things like exclude
* all class attributes. For example *.class would remove the class attribute that all objects
* have when serializing. A open ended wildcard like * would cause deep serialization to take
* place. Be careful with that one. Although you can limit it's depth with an exclude like
* *.foo. The order of evaluation of includes and excludes is the order in which you called their
* functions. First call to those functions will cause those expressions to be evaluated first.
* The first expression to match a path that action will be taken thus short circuiting all other
* expressions defined later.
*
*
* Transforers are a new addition that allow you to modify the values that are being serialized.
* This allows you to create different output for certain conditions. This is very important in
* web applications. Say you are saving your text to the DB that could contain < and >. If
* you plan to add that content to your HTML page you'll need to escape those characters. Transformers
* allow you to do this. Flexjson ships with a simple HTML encoder {@link flexjson.transformer.HtmlEncoderTransformer}.
* Transformers are specified in dot notation just like include and exclude methods, but it doesn't
* support wildcards.
*
*
* JSONSerializer is safe to use the serialize() methods from two seperate
* threads. It is NOT safe to use combination of {@link JSONSerializer#include(String...)}
* {@link JSONSerializer#transform(flexjson.transformer.Transformer, String...)}, or {@link JSONSerializer#exclude(String...)}
* from multiple threads at the same time. It is also NOT safe to use
* {@link JSONSerializer#serialize(Object)} and include/exclude/transform from
* multiple threads. The reason for not making them more thread safe is to boost performance.
* Typical use case won't call for two threads to modify the JsonSerializer at the same type it's
* trying to serialize.
*
*/
public class JSONSerializer
{
public final static char[] HEX= "0123456789ABCDEF".toCharArray();
private TypeTransformerMap typeTransformerMap= new TypeTransformerMap(TransformerUtil.getDefaultTypeTransformers());
private Map pathTransformerMap= new HashMap();
private List pathExpressions= new ArrayList();
private boolean prettyPrint;
private String rootName;
// OutputHander Configuration
/**
* format output with indentations
*
* @param prettyPrint - should out put cleanfly formatted Json
* @return JsonSerializer for chaining configuration
*/
public JSONSerializer prettyPrint(boolean prettyPrint)
{
this.prettyPrint= prettyPrint;
return this;
}
/**
* This wraps the resulting JSON in a javascript object that contains a single
* field named rootName. This is great to use in conjunction with other libraries
* like EXTJS whose data models require them to be wrapped in a JSON object.
*
* @param rootName - name to assign to root object
* @return this JsonSerializer for chaining configurations
*/
public JSONSerializer rootName(String rootName)
{
this.rootName= rootName;
return this;
}
// SERIALIZATION
/**
* This performs a shallow serialization of the target instance. It uses a StringBuilder to write output to.
*
* @param target - the instance to serialize to JSON
* @return returns JSON as a String
*/
public String serialize(Object target)
{
return serialize(target, SerializationType.SHALLOW, new StringBuilderOutputHandler(new StringBuilder()));
}
/**
* This performs a shallow serialization of the target instance and
* passes the generated JSON into the provided Writer.
* This can be used to stream JSON back to a browser rather
* than wait for it to all complete and then dump it all at
* once like the StringBufferOutputHandler and StringBuilderOutputHandler
*
* @param target - the instance to serialize to JSON
* @param out - Writer to write output to
*/
public void serialize(Object target, Writer out)
{
serialize(target, SerializationType.SHALLOW, new WriterOutputHandler(out));
}
/**
* This performs a shallow serialization of the target instance and
* passes the generated JSON into the provided StringBuilder.
*
* @param target - the instance to serialize to JSON
* @param out - StringBuilder to write output to
* @return returns JSON as a String
*/
public String serialize(Object target, StringBuilder out)
{
return serialize(target, SerializationType.SHALLOW, new StringBuilderOutputHandler(out));
}
/**
* This performs a shallow serialization of the target instance and
* passes the generated JSON into the provided StringBuffer.
*
* @param target - the instance to serialize to JSON
* @param out - StringBuffer to write output to
* @return returns JSON as a String
*/
public String serialize(Object target, StringBuffer out)
{
return serialize(target, SerializationType.SHALLOW, new StringBufferOutputHandler(out));
}
/**
* This performs a shallow serialization of the target instance and
* passes the generated JSON into the provided OutputHandler.
*
* @param target - the instance to serialize to JSON
* @param out - OutputHandler to write output to
* @return returns JSON as a String
*/
public String serialize(Object target, OutputHandler out)
{
return serialize(target, SerializationType.SHALLOW, out);
}
/**
* This performs a deep serialization of the target instance. It will include
* all collections, maps, and arrays by default so includes are ignored except
* if you want to include something being excluded by an annotation. Excludes
* are honored. However, cycles in the target's graph are NOT followed. This
* means some members won't be included in the JSON if they would create a cycle.
* Rather than throwing an exception the cycle creating members are simply not
* followed. This uses a StringBuilder to output JSON to.
*
* @param target the instance to serialize to JSON.
* @return returns JSON as a String
*/
public String deepSerialize(Object target)
{
return serialize(target, SerializationType.DEEP, new StringBuilderOutputHandler(new StringBuilder()));
}
/**
* This performs a deep serialization of the target instance and
* passes the generated JSON into the provided Writer.
* This can be used to stream JSON back to a browser rather
* than wait for it to all complete and then dump it all at
* once like the StringBufferOutputHandler and StringBuilderOutputHandler
*
* @param target - the instance to serialize to JSON
* @param out - Writer
*/
public void deepSerialize(Object target, Writer out)
{
serialize(target, SerializationType.DEEP, new WriterOutputHandler(out));
}
/**
* This performs a deep serialization of the target instance and
* passes the generated JSON into the provided StringBuilder.
*
* @param target - the instance to serialize to JSON
* @param out - StringBuilder
* @return returns JSON as a String
*/
public String deepSerialize(Object target, StringBuilder out)
{
return serialize(target, SerializationType.DEEP, new StringBuilderOutputHandler(out));
}
/**
* This performs a deep serialization of the target instance and
* passes the generated JSON into the provided StringBuffer.
*
* @param target - the instance to serialize to JSON
* @param out - StringBuffer
* @return returns JSON as a String
*/
public String deepSerialize(Object target, StringBuffer out)
{
return serialize(target, SerializationType.DEEP, new StringBufferOutputHandler(out));
}
/**
* This performs a deep serialization of the target instance and
* passes the generated JSON into the provided OutputHandler.
*
* @param target - the instance to serialize to JSON
* @param out - OutputHandler to write to
* @return returns JSON as a String
*/
public String deepSerialize(Object target, OutputHandler out)
{
return serialize(target, SerializationType.DEEP, out);
}
/**
*
* @param target - the instance to serialize to JSON
* @param serializationType - serialize deep or shallow
* @param out - output handler
* @return returns JSON as a String
*/
protected String serialize(Object target, SerializationType serializationType, OutputHandler out)
{
String output= "";
// initialize context
JSONContext context= JSONContext.get();
context.setRootName(rootName);
context.setPrettyPrint(prettyPrint);
context.setOut(out);
context.serializationType(serializationType);
context.setTypeTransformers(typeTransformerMap);
context.setPathTransformers(pathTransformerMap);
context.setPathExpressions(pathExpressions);
try
{
//initiate serialization of target tree
String rootName= context.getRootName();
if (rootName == null || rootName.trim().equals(""))
{
context.transform(target);
}
else
{
context.writeOpenObject();
context.writeName(rootName);
context.transform(target);
context.writeCloseObject();
}
output= context.getOut().toString();
}
finally
{
// cleanup context
JSONContext.cleanup();
}
return output;
}
// TRANSFORMER CONFIGURATIONS
/**
* This adds a Transformer used to manipulate the value of all the fields you give it.
* Fields can be in dot notation just like {@link JSONSerializer#include} and
* {@link JSONSerializer#exclude } methods. However, transform doesn't support wildcards.
* Specifying more than one field allows you to add a single instance to multiple fields.
* It's there for handiness. :-)
*
* @param transformer the instance used to transform values
* @param fields the paths to the fields you want to transform. They can be in dot notation.
* @return Hit you back with the JSONSerializer for method chain goodness.
*/
public JSONSerializer transform(Transformer transformer, String... fields)
{
transformer= new TransformerWrapper(transformer);
for (String field : fields)
{
if (field.length() == 0)
{
pathTransformerMap.put(new Path(), transformer);
}
else
{
pathTransformerMap.put(new Path(field.split("\\.")), transformer);
}
}
return this;
}
/**
* This adds a Transformer used to manipulate the value of all fields that match the type.
*
* @param transformer the instance used to transform values
* @param types you want to transform.
* @return Hit you back with the JSONSerializer for method chain goodness.
*/
public JSONSerializer transform(Transformer transformer, Class... types)
{
transformer= new TransformerWrapper(transformer);
for (Class type : types)
{
typeTransformerMap.put(type, transformer);
}
return this;
}
// INCLUDE/EXCLUDE CONFIGURATION
protected void addExclude(String field)
{
int index= field.lastIndexOf('.');
if (index > 0)
{
PathExpression expression= new PathExpression(field.substring(0, index), true);
if (!expression.isWildcard())
{
pathExpressions.add(expression);
}
}
pathExpressions.add(new PathExpression(field, false));
}
protected void addInclude(String field)
{
pathExpressions.add(new PathExpression(field, true));
}
/**
* This takes in a dot expression representing fields
* to exclude when serialize method is called. You
* can hand it one or more fields. Example are: "password",
* "bankaccounts.number", "people.socialsecurity", or
* "people.medicalHistory". In exclude method dot notations
* will only exclude the final field (i.e. rightmost field).
* All the fields to the left of the last field will be included.
* In order to exclude the medicalHistory field we have to
* include the people field since people would've been excluded
* anyway since it's a Collection of Person objects. The order of
* evaluation is the order in which you call the exclude method.
* The first call to exclude will be evaluated before other calls to
* include or exclude. The field expressions are evaluated in order
* you pass to this method.
*
* @param fields one or more field expressions to exclude.
* @return this instance for method chaining.
*/
public JSONSerializer exclude(String... fields)
{
for (String field : fields)
{
addExclude(field);
}
return this;
}
/**
* This takes in a dot expression representing fields to
* include when serialize method is called. You can hand
* it one or more fields. Examples are: "hobbies",
* "hobbies.people", "people.emails", or "character.inventory".
* When using dot notation each field between the dots will
* be included in the serialization process. The order of
* evaluation is the order in which you call the include method.
* The first call to include will be evaluated before other calls to
* include or exclude. The field expressions are evaluated in order
* you pass to this method.
*
* @param fields one or more field expressions to include.
* @return this instance for method chaining.
*/
public JSONSerializer include(String... fields)
{
for (String field : fields)
{
addInclude(field);
}
return this;
}
// INCLUDE/EXCLUDE TEST/DEBUG HOOKS
/**
* Return the fields included in serialization. These fields will be in dot notation.
*
* @return A List of dot notation fields included in serialization.
*/
public List getIncludes()
{
List expressions= new ArrayList();
for (PathExpression expression : pathExpressions)
{
if (expression.isIncluded())
{
expressions.add(expression);
}
}
return expressions;
}
/**
* Return the fields excluded from serialization. These fields will be in dot notation.
*
* @return A List of dot notation fields excluded from serialization.
*/
public List getExcludes()
{
List excludes= new ArrayList();
for (PathExpression expression : pathExpressions)
{
if (!expression.isIncluded())
{
excludes.add(expression);
}
}
return excludes;
}
/**
* Sets the fields included in serialization. These fields must be in dot notation.
* This is just here so that JSONSerializer can be treated like a bean so it will
* integrate with Spring or other frameworks. This is not ment to be used
* in code use include method for that.
*
* @param fields the list of fields to be included for serialization. The fields arg should be a
* list of strings in dot notation.
*/
public void setIncludes(List fields)
{
for (String field : fields)
{
pathExpressions.add(new PathExpression(field, true));
}
}
/**
* Sets the fields excluded in serialization. These fields must be in dot notation.
* This is just here so that JSONSerializer can be treated like a bean so it will
* integrate with Spring or other frameworks. This is not ment to be used
* in code use exclude method for that.
*
* @param fields the list of fields to be excluded for serialization. The fields arg should be a
* list of strings in dot notation.
*/
public void setExcludes(List fields)
{
for (String field : fields)
{
addExclude(field);
}
}
public void setSerializingWithUniqueIds(boolean serializingWithUniqueIds)
{
JSONContext.get().setSerializingWithUniqueIds(serializingWithUniqueIds);
}
}