/** * Copyright 2007 Charlie Hubbard * * 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 message.json; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.*; /** * <p> * 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 * <a href="http://en.wikipedia.org/wiki/Don't_repeat_yourself">DRY</a>. * </p> * * <p> * Let's go through a simple example: * </p> * * <pre> * JSONSerializer serializer = new JSONSerializer(); * return serializer.serialize( person ); * * </pre> * * <p> * What this statement does is output the json from the instance of person. So * the JSON we might see for this could look like: * </p> * * <pre> * { "class": "com.mysite.Person", * "firstname": "Charlie", * "lastname": "Rose", * "age", 23 * "birthplace": "Big Sky, Montanna" * } * * </pre> * <p> * 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 get 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. * </p> * * <p> * How would we include the <em>hobbies</em> field? Using the {@link JSONSerializer#include} * method allows us to include these fields in the serialization process. Here is * how we'd do that: * </p> * * <pre> * return new JSONSerializer().include("hobbies").serialize( person ); * * </pre> * * That would produce output like: * * <pre> * { "class": "com.mysite.Person", * "firstname": "Charlie", * "lastname": "Rose", * "age", 23 * "birthplace": "Big Sky, Montanna", * "hobbies", [ * "poker", * "snowboarding", * "kite surfing", * "bull riding" * ] * } * * </pre> * * <p> * If the <em>hobbies</em> field contained objects, say Hobby instances, then a * shallow copy of those objects would be performed. Let's go further and say * <em>hobbies</em> 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. * </p> * * <p> * But, for the sake of argument and illustration let's say we wanted to send the * <em>people</em> field in Hobby. We can do the following: * </p> * * <pre> * return new JSONSerializer().include("hobbies.people").serialize( person ); * * </pre> * * <p> * JSONSerializer is smart enough to know that you want <em>hobbies</em> field included and * the <em>people</em> 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. * </p> * * <p> * 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. * </p> * * <pre> * return new JSONSerialize().exclude("password").serialize(user); * * </pre> * * <p> * JSONSerializer will also pay attention to any method or field annotated by * . 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 <em>hobbies</em> or * <em>favoriteMovies</em> depends on the situation so it's best NOT to annotate * those fields, and use the {@link JSONSerializer#include} method. * </p> * * <p> * In a shallow copy only these types of instance fields will be sent: * <strong>String</strong>, <strong>Date</strong>, <strong>Number</strong>, * <strong>Boolean</strong>, <strong>Character</strong>, <strong>Enum</strong>, * <strong>Object</strong> and <strong>null</strong>. 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. * </p> * <p> * 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. * </p> * <p> * 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 HTMLEncoder}. * Transformers are specified in dot notation just like include and exclude methods, but it doesn't * support wildcards. * </p> * <p> * 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(message.json.Transformer, String[])}, or {@link JSONSerializer#exclude(String[])} * from multiple threads at the same time. It is also NOT safe to use * {@link JSONSerializer#serialize(String, 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. * </p> */ public class JSONSerializer { public final static char[] HEX = "0123456789ABCDEF".toCharArray(); //List<PathExpression> pathExpressions = new ArrayList<PathExpression>(); //Map<Path, Transformer> transformations = new HashMap<Path,Transformer>(); List pathExpressions = new ArrayList(); Map transformations = new HashMap(); /** * Create a serializer instance. It's unconfigured in terms of fields * it should include or exclude. */ public JSONSerializer() { } /** * This performs a shallow serialization of target instance. It 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 the name of the field to assign the resulting JSON. * @param target the instance to serialize to JSON. * @return the JSON object with one field named rootName and the value being the JSON of target. */ public String serialize( String rootName, Object target ) { return new ShallowVisitor().visit( rootName, target ); } /** * This performs a shallow serialization of the target instance. * * @param target the instance to serialize to JSON * @return the JSON representing the target instance. */ public String serialize( Object target ) { return new ShallowVisitor().visit( target ); } /** * 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. * * @param target the instance to serialize to JSON. * @return the JSON representing the target instance deep serialization. */ public String deepSerialize( Object target ) { return new DeepVisitor().visit( target ); } /** * This performs a deep serialization of target instance. It 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. * See {@link JSONSerializer#deepSerialize(Object)} for more * in depth explaination. * * @param rootName the name of the field to assign the resulting JSON. * @param target the instance to serialize to JSON. * @return the JSON object with one field named rootName and the value being the JSON of target. */ public String deepSerialize( String rootName, Object target ) { return new DeepVisitor().visit( rootName, target ); } /** * 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( int i=0; i<fields.length; i++ ) { addExclude( fields[i] ); } 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( int i=0; i<fields.length; i++ ) { pathExpressions.add( new PathExpression( fields[i], true ) ); } return this; } /** * 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 ) { for( int i=0; i<fields.length; i++ ) { if( fields[i].length() == 0 ) { transformations.put( new Path(), transformer ); } else { transformations.put( new Path( fields[i].split("\\.") ), transformer ); } } return this; } /** * 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(); Iterator it = pathExpressions.iterator(); while(it.hasNext()) { PathExpression expression = (PathExpression)it.next(); 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(); Iterator it = pathExpressions.iterator(); while(it.hasNext()) { PathExpression expression = (PathExpression)it.next(); 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. <strong>This is not ment to be used * in code use include method for that.</strong> * @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 ) { Iterator it = fields.iterator(); while(it.hasNext()) { Object field = it.next(); pathExpressions.add( new PathExpression( field.toString(), 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. <strong>This is not ment to be used * in code use exclude method for that.</strong> * @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 ) { Iterator it = fields.iterator(); while(it.hasNext()) { Object field = it.next(); addExclude( field ); } } private void addExclude(Object field) { String name = field.toString(); int index = name.lastIndexOf('.'); if( index > 0 ) { PathExpression expression = new PathExpression( name.substring( 0, index ), true ); if( !expression.isWildcard() ) { pathExpressions.add( expression ); } } pathExpressions.add( new PathExpression( name, false ) ); } /** * This will do a serialize the target and pretty print the output so it's easier to read. * * @param target of the serialization. * @return the serialized representation of the target in pretty print form. */ public String prettyPrint( Object target ) { return new ShallowVisitor( true ).visit( target ); } /** * This will do a serialize with root name and pretty print the output so it's easier to read. * * @param rootName the name of the field to assign the resulting JSON. * @param target of the serialization. * @return the serialized representation of the target in pretty print form. */ public String prettyPrint( String rootName, Object target ) { return new ShallowVisitor( true ).visit( rootName, target ); } private abstract class ObjectVisitor { protected StringBuffer builder; protected boolean prettyPrint = false; private int amount = 0; private boolean insideArray = false; private Path path; protected ObjectVisitor() { builder = new StringBuffer(); path = new Path(); } public ObjectVisitor(boolean prettyPrint) { this(); this.prettyPrint = prettyPrint; } public String visit( Object target ) { json( target ); return builder.toString(); } public String visit( String rootName, Object target ) { beginObject(); string(rootName); add(':'); json( target ); endObject(); return builder.toString(); } private void json(Object object) { if (object == null) add("null"); else if (object instanceof Class) string( ((Class)object).getName() ); else if (object instanceof Boolean) bool( ((Boolean) object) ); else if (object instanceof Number) add( doTransform( object ) ); else if (object instanceof String) string(object); else if (object instanceof Character) string(object); else if (object instanceof Map) map( (Map)object); else if (object.getClass().isArray()) array( object ); else if (object instanceof Collection) array(((Collection) object).iterator() ); else if( object instanceof Date) date( (Date)object ); //else if( object instanceof Enum ) // enumerate( (Enum)object ); else bean( object ); } // private void enumerate(Enum value) { // string( value.name() ); // } private void map(Map map) { beginObject(); Iterator it = map.keySet().iterator(); boolean firstField = true; while (it.hasNext()) { Object key = it.next(); int len = builder.length(); add( key, map.get(key), firstField ); if( len < builder.length() ) { firstField = false; } } endObject(); } private void array(Iterator it) { beginArray(); while (it.hasNext()) { if( prettyPrint ) { addNewline(); } addArrayElement( it.next(), it.hasNext() ); } endArray(); } private void array(Object object) { beginArray(); int length = Array.getLength(object); for (int i = 0; i < length; ++i) { if( prettyPrint ) { addNewline(); } addArrayElement( Array.get(object, i), i < length - 1 ); } endArray(); } private void addArrayElement(Object object, boolean isLast ) { int len = builder.length(); json( object ); if( len < builder.length() ) { // make sure we at least added an element. if ( isLast ) add(','); } } private void bool(Boolean b) { add( b.booleanValue() ? "true" : "false" ); } private void bool(boolean b) { add( b ? "true" : "false" ); } private void string(Date d) { add( d.toGMTString() ); } private void string(Object obj) { add('"'); CharacterIterator it = new StringCharacterIterator( doTransform( obj ).toString() ); for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) { if (c == '"') add("\\\""); else if (c == '\\') add("\\\\"); // else if (c == '/') add("\\/"); else if (c == '\b') add("\\b"); else if (c == '\f') add("\\f"); else if (c == '\n') add("\\n"); else if (c == '\r') add("\\r"); else if (c == '\t') add("\\t"); else if (Character.isISOControl(c)) { unicode(c); } else { add(c); } } add('"'); } private void date(Date date) { if( transformations.containsKey( path ) ) { string( date ); } else { builder.append( date.getTime() ); } } private ChainedSet visits = new ChainedSet( Collections.EMPTY_SET ); protected void bean(Object object) { if( !visits.contains( object ) ) { visits = new ChainedSet( visits ); visits.add( object ); beginObject(); try { BeanInfo info = Introspector.getBeanInfo( findBeanClass( object ) ); PropertyDescriptor[] props = info.getPropertyDescriptors(); boolean firstField = true; for (int i=0; i<props.length; i++) { PropertyDescriptor prop = props[i]; String name = prop.getName(); path.enqueue( name ); Method accessor = prop.getReadMethod(); if (accessor != null && isIncluded( prop ) ) { Object value = accessor.invoke(object, (Object[]) null); if( !visits.contains( value ) ) { add(name, value, firstField); firstField = false; } } path.pop(); } for( Class current = object.getClass(); current != null; current = current.getSuperclass() ) { Field[] ff = current.getDeclaredFields(); for (int i=0; i<ff.length; i++) { Field field = ff[i]; path.enqueue( field.getName() ); if (isValidField(field)) { if( !visits.contains( field.get(object) ) ) { add(field.getName(), field.get(object), firstField); firstField = false; } } path.pop(); } } } catch( JSONException e ) { throw e; } catch( Exception e ) { throw new JSONException( "Error trying to serialize path: " + path.toString(), e ); } endObject(); visits = (ChainedSet) visits.getParent(); } } private Object doTransform(Object value) { if( transformations.containsKey( path ) ) { value = ((Transformer)transformations.get( path )).transform( value ); } return value; } private Class findBeanClass(Object object) { try { Class[] classes = object.getClass().getInterfaces(); for( int i=0; i<classes.length; i++ ) { Class clazz = classes[i]; /* if( clazz.getName().equals("org.hibernate.proxy.HibernateProxy") ) { Method method = object.getClass().getMethod("getHibernateLazyInitializer"); Object initializer = method.invoke( object ); Method pmethod = initializer.getClass().getMethod("getPersistentClass"); return pmethod.invoke( initializer ).getClass(); } */ } }finally{ } return object.getClass(); } protected abstract boolean isIncluded( PropertyDescriptor prop ); protected boolean isValidField(Field field) { return !Modifier.isStatic( field.getModifiers() ) && Modifier.isPublic( field.getModifiers() ) && !Modifier.isTransient( field.getModifiers() ); } protected boolean addComma(boolean firstField) { if ( !firstField ) { add(','); } else { firstField = false; } return firstField; } protected void beginObject() { if( prettyPrint ) { if( insideArray ) { indent( amount ); } amount += 4; } add( '{' ); } protected void endObject() { if( prettyPrint ) { addNewline(); amount -= 4; indent( amount ); } add( '}' ); } private void beginArray() { if( prettyPrint ) { amount += 4; insideArray = true; } add('['); } private void endArray() { if( prettyPrint ) { addNewline(); amount -= 4; insideArray = false; indent( amount ); } add(']'); } protected void add( char c ) { builder.append( c ); } private void indent(int amount) { for( int i = 0; i < amount; i++ ) { builder.append( " " ); } } private void addNewline() { builder.append("\n"); } protected void add( Object value ) { builder.append( value ); } protected void add(Object key, Object value, boolean prependComma) { int start = builder.length(); addComma( prependComma ); addAttribute( key ); int len = builder.length(); json( value ); if( len == builder.length() ) { builder.delete( start, len ); // erase the attribute key we didn't output anything. } } private void addAttribute(Object key) { if( prettyPrint ) { addNewline(); indent( amount ); } builder.append("\""); builder.append( key ); builder.append( "\"" ); builder.append( ":" ); if( prettyPrint ) { builder.append(" "); } } private void unicode(char c) { add("\\u"); int n = c; for (int i = 0; i < 4; ++i) { int digit = (n & 0xf000) >> 12; add(JSONSerializer.HEX[digit]); n <<= 4; } } protected PathExpression matches(PropertyDescriptor prop, List expressions) { Iterator it = expressions.iterator(); while(it.hasNext()) { PathExpression expr = (PathExpression)it.next(); if( expr.matches( path ) ) { return expr; } } return null; } } private class ShallowVisitor extends ObjectVisitor { public ShallowVisitor() { super(); } public ShallowVisitor(boolean prettyPrint) { super(prettyPrint); } protected boolean isIncluded( PropertyDescriptor prop ) { PathExpression expression = matches( prop, pathExpressions); if( expression != null ) { return expression.isIncluded(); } Method accessor = prop.getReadMethod(); /* if( accessor.isAnnotationPresent( JSON.class ) ) { return accessor.getAnnotation(JSON.class).include(); } */ Class propType = prop.getPropertyType(); return !(propType.isArray() || Collection.class.isAssignableFrom(propType) || Map.class.isAssignableFrom(propType)); } } private class DeepVisitor extends ObjectVisitor { public DeepVisitor() { super(); } public DeepVisitor(boolean prettyPrint) { super(prettyPrint); } protected boolean isIncluded( PropertyDescriptor prop ) { PathExpression expression = matches( prop, pathExpressions); if( expression != null ) { return expression.isIncluded(); } Method accessor = prop.getReadMethod(); /* if( accessor.isAnnotationPresent( JSON.class ) ) { return accessor.getAnnotation(JSON.class).include(); } */ return true; } } public static void main(String[] ss) { // JSONSerializer s = new JSONSerializer(); // List list = new ArrayList(); // for(int i=0; i<10; i++) // { // User u = new User(); // u.setId(new Long(100)); // u.setEmail("email@mail.com"); // // list.add(u); // //System.out.println(s.deepSerialize(u)); // } // s.exclude(new String[]{"class"}); // System.out.println(s.deepSerialize(list)); } }