/** * $Id: JSONTranscoder.java 110 2013-05-03 13:30:13Z azeckoski $ * $URL: http://reflectutils.googlecode.com/svn/trunk/src/main/java/org/azeckoski/reflectutils/transcoders/JSONTranscoder.java $ * JSONTranscoder.java - entity-broker - Sep 16, 2008 3:19:29 PM - azeckoski ************************************************************************** * Copyright (c) 2008 Aaron Zeckoski * Licensed under the Apache License, Version 2.0 * * A copy of the Apache License has been included in this * distribution and is available at: http://www.apache.org/licenses/LICENSE-2.0.txt * * Aaron Zeckoski (azeckoski @ gmail.com) (aaronz @ vt.edu) (aaron @ caret.cam.ac.uk) */ package org.azeckoski.reflectutils.transcoders; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Timestamp; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.azeckoski.reflectutils.ArrayUtils; import org.azeckoski.reflectutils.ConstructorUtils; import org.azeckoski.reflectutils.ConversionUtils; import org.azeckoski.reflectutils.ReflectUtils; import org.azeckoski.reflectutils.ClassFields.FieldsFilter; import org.azeckoski.reflectutils.map.ArrayOrderedMap; /** * Provides methods for encoding and decoding JSON * * @author Aaron Zeckoski (azeckoski @ gmail.com) */ @SuppressWarnings({ "unchecked", "rawtypes" }) public class JSONTranscoder implements Transcoder { public String getHandledFormat() { return "json"; } public String encode(Object object, String name, Map<String, Object> properties) { return encode(object, name, properties, this.maxLevel); } public String encode(Object object, String name, Map<String, Object> properties, int maxDepth) { // Object data = object; // String encoded = ""; // if (object != null) { // Map<String, Object> mapData = ReflectUtils.getInstance().map(object, 10, null, false, true, Transcoder.DATA_KEY); // // for JSON we can get out the "data" field and convert that only // if (mapData.size() == 1 && mapData.containsKey(Transcoder.DATA_KEY)) { // data = mapData.get(Transcoder.DATA_KEY); // } else { // data = mapData; // } // } // allow the transcoder to deal with the data directly, no need to convert it to a map first String encoded = JSONTranscoder.makeJSON(object, properties, humanOutput, includeNulls, includeClassField, maxDepth, null); return encoded; } public Map<String, Object> decode(String string) { Map<String, Object> decoded = null; Object decode = new JsonReader().read(string); if (decode instanceof Map) { decoded = (Map<String, Object>) decode; } else { // for JSON if the result is not a map then simply put the result into a map decoded = new ArrayOrderedMap<String, Object>(); decoded.put(Transcoder.DATA_KEY, decode); } return decoded; } /** * Default constructor: * See other constructors for options */ public JSONTranscoder() {} private List<ObjectEncoder> encoders = null; public void setEncoders(List<ObjectEncoder> encoders) { this.encoders = encoders; } public List<ObjectEncoder> getEncoders() { return encoders; } public void addEncoder(ObjectEncoder objectEncoder) { if (this.encoders == null) { this.encoders = new ArrayList<ObjectEncoder>(); } this.encoders.add(objectEncoder); } private boolean humanOutput = false; private boolean includeNulls = true; private boolean includeClassField = false; /** * @param humanOutput if true then enable human readable output (includes indentation and line breaks) * @param includeNulls if true then create output tags for null values * @param includeClassField if true then include the value from the "getClass()" method as "class" when encoding beans and maps */ public JSONTranscoder(boolean humanOutput, boolean includeNulls, boolean includeClassField) { this.humanOutput = humanOutput; this.includeNulls = includeNulls; this.includeClassField = includeClassField; } private int maxLevel = 7; /** * @param maxLevel the number of objects to follow when traveling through the object, * 0 means only the fields in the initial object, default is 7 */ public void setMaxLevel(int maxLevel) { this.maxLevel = maxLevel; } // Encoder public static final char OBJ_BEG = '{'; public static final char OBJ_END = '}'; public static final char OBJ_SEP = ':'; public static final char ARRAY_BEG = '['; public static final char ARRAY_END = ']'; public static final char JSON_SEP = ','; // based on code from: http://www.json.org/java/org/json/XML.java public static final char SPACE = ' '; public static final char AMP = '&'; /** * single quote (') */ public static final char APOS = '\''; public static final char BANG = '!'; public static final char BACK = '\\'; public static final char EOL = '\n'; public static final char EQ = '='; public static final char GT = '>'; public static final char LT = '<'; public static final char QUEST = '?'; public static final char QUOT = '"'; public static final char SLASH = '/'; public static final String BOOLEAN_TRUE = "true"; public static final String BOOLEAN_FALSE = "false"; public static final String NULL = "null"; /** * Convert an object into a well-formed, element-normal XML string. * @param object any object * @return the JSON string version of the object */ public static String makeJSON(Object object) { return makeJSON(object, null, false, true, false, 10, null); } /** * Convert an object into a well-formed, element-normal XML string. * @param object any object * @param humanOutput true of human readable output * @param includeNulls true to include null values when generating tags * @param includeClassField if true then include the value from the "getClass()" method as "class" when encoding beans and maps * @param maxLevel maximum level to traverse the objects before stopping * @param encoders the external encoders to allow to process complex objects * @return the JSON string version of the object */ public static String makeJSON(Object object, Map<String, Object> properties, boolean humanOutput, boolean includeNulls, boolean includeClassField, int maxLevel, List<ObjectEncoder> encoders) { return toJSON(object, 0, maxLevel, humanOutput, includeNulls, includeClassField, properties, encoders); } protected static String toJSON(Object object, int level, int maxLevel, boolean humanOutput, boolean includeNulls, boolean includeClassField, Map<String, Object> properties, List<ObjectEncoder> encoders) { StringBuilder sb = new StringBuilder(); if (object == null) { if (includeNulls) { // nulls use the constant sb.append(NULL); } } else { Class<?> type = ConstructorUtils.getWrapper(object.getClass()); if ( ConstructorUtils.isClassSimple(type) ) { // Simple (String, Number, etc.) if (Date.class.isAssignableFrom(type) || Timestamp.class.isAssignableFrom(type)) { // date Date d = (Date) object; sb.append(d.getTime()); } else if (Number.class.isAssignableFrom(type)) { // number sb.append(object.toString()); } else if (Boolean.class.isAssignableFrom(type)) { // boolean if ( ((Boolean)object).booleanValue() ) { sb.append(BOOLEAN_TRUE); } else { sb.append(BOOLEAN_FALSE); } } else { sb.append(QUOT); sb.append( escapeForJSON(object.toString()) ); sb.append(QUOT); } } else if ( ConstructorUtils.isClassArray(type) ) { // ARRAY int length = ArrayUtils.size((Object[])object); sb.append(ARRAY_BEG); if (length > 0) { for (int i = 0; i < length; ++i) { if (i > 0) { sb.append(JSON_SEP); } makeEOL(sb, humanOutput); makeLevelSpaces(sb, level+1, humanOutput); sb.append( toJSON(Array.get(object, i), level+1, maxLevel, humanOutput, includeNulls, includeClassField, properties, encoders) ); } makeEOL(sb, humanOutput); makeLevelSpaces(sb, level, humanOutput); } sb.append(ARRAY_END); } else if ( ConstructorUtils.isClassCollection(type) ) { // COLLECTION Collection<Object> collection = (Collection) object; sb.append(ARRAY_BEG); if (! collection.isEmpty()) { boolean first = true; for (Object element : collection) { if (first) { first = false; } else { sb.append(JSON_SEP); } makeEOL(sb, humanOutput); makeLevelSpaces(sb, level+1, humanOutput); sb.append( toJSON(element, level+1, maxLevel, humanOutput, includeNulls, includeClassField, properties, encoders) ); } makeEOL(sb, humanOutput); makeLevelSpaces(sb, level, humanOutput); } sb.append(ARRAY_END); } else { // must be a bean or map, make sure it is a map // special handling for certain object types String special = TranscoderUtils.handleObjectEncoding(object, encoders); if (special != null) { if ("".equals(special)) { // skip this one entirely sb.append(NULL); } else { // just use the value in special to represent this sb.append(QUOT); sb.append( escapeForJSON(special) ); sb.append(QUOT); } } else { // normal handling if (maxLevel <= level) { // if the max level was reached then stop sb.append(QUOT); sb.append( "MAX level reached (" ); sb.append( level ); sb.append( "):" ); sb.append( escapeForJSON(object.toString()) ); sb.append(QUOT); } else { Map<String, Object> map = null; if (Map.class.isAssignableFrom(type)) { map = (Map<String, Object>) object; } else { // reflect over objects map = ReflectUtils.getInstance().getObjectValues(object, FieldsFilter.SERIALIZABLE, includeClassField); } // add in the optional properties if it makes sense to do so if (level == 0 && properties != null && ! properties.isEmpty()) { map.putAll(properties); } sb.append(OBJ_BEG); boolean first = true; for (Entry<String, Object> entry : map.entrySet()) { if (entry.getKey() != null) { Object value = entry.getValue(); if (value != null || includeNulls) { if (first) { first = false; } else { sb.append(JSON_SEP); } makeEOL(sb, humanOutput); makeLevelSpaces(sb, level+1, humanOutput); sb.append(QUOT); sb.append(entry.getKey()); sb.append(QUOT); sb.append(OBJ_SEP); if (humanOutput) { sb.append(SPACE); } sb.append( toJSON(value, level+1, maxLevel, humanOutput, includeNulls, includeClassField, properties, encoders) ); } } } makeEOL(sb, humanOutput); makeLevelSpaces(sb, level, humanOutput); sb.append(OBJ_END); } } } } return sb.toString(); } protected static void makeEOL(StringBuilder sb, boolean includeEOL) { if (includeEOL) { sb.append(EOL); } } protected static final String SPACES = " "; protected static void makeLevelSpaces(StringBuilder sb, int level, boolean includeEOL) { if (includeEOL) { for (int i = 0; i < level; i++) { sb.append(SPACES); } } } /** * Escape a string for JSON encoding * @param string any string * @return the escaped string */ public static String escapeForJSON(String string) { StringBuilder sb = new StringBuilder(); if (string != null) { for (int i = 0, len = string.length(); i < len; i++) { char c = string.charAt(i); switch (c) { case QUOT: sb.append("\\\""); break; case BACK: sb.append("\\\\"); break; case SLASH: sb.append("\\/"); break; case '\b': sb.append("\\b"); break; case '\f': sb.append("\\f"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; default: if (Character.isISOControl(c)) { sb.append("\\u"); int n = c; for (int j = 0; j < 4; ++j) { int digit = (n & 0xf000) >> 12; sb.append(hex[digit]); n <<= 4; } } else { sb.append(c); } } } } return sb.toString(); } // STATICS private static final Object MARK_OBJECT_END = new Object(); private static final Object MARK_ARRAY_END = new Object(); private static final Object MARK_COLON = new Object(); private static final Object MARK_COMMA = new Object(); private static final Object MARK_END_INPUT = new Object(); public static final int FIRST = 0; public static final int CURRENT = 1; public static final int NEXT = 2; private static Map<Character, Character> escapes = new HashMap<Character, Character>(); static { escapes.put(new Character('"'), new Character('"')); escapes.put(new Character('\\'), new Character('\\')); escapes.put(new Character('/'), new Character('/')); escapes.put(new Character('b'), new Character('\b')); escapes.put(new Character('f'), new Character('\f')); escapes.put(new Character('n'), new Character('\n')); escapes.put(new Character('r'), new Character('\r')); escapes.put(new Character('t'), new Character('\t')); } protected static char[] hex = "0123456789ABCDEF".toCharArray(); /** * Create Java objects from JSON (note that only simple java objects, maps, and arrays will be returned) <br/> * Dates will come back in as UTC timecodes or possibly strings which you will need to parse manually <br/> * Numbers will come in as int if they are small, long if they are big, and BigInteger if they are huge, * floating point is handled similarly: float, double, BigDecimal <br/> * JSON arrays come back as a List always, similarly, any collection or array that was output will come back as a list <br/> * You can use the {@link ConversionUtils} to help with conversion if needed <br/> * * Derived from code at: * https://svn.sourceforge.net/svnroot/stringtree/trunk/src/delivery/java/org/stringtree/json/JSONWriter.java */ public class JsonReader { private CharacterIterator it; // TODO crikey - thread safety private char c; // TODO crikey - thread safety private Object token; // TODO crikey - thread safety private StringBuffer buf = new StringBuffer(); // TODO crikey - thread safety private char next() { c = it.next(); return c; } private void skipWhiteSpace() { while (Character.isWhitespace(c)) { next(); } } public Object read(CharacterIterator it) { return read(it, NEXT); } public Object read(String string) { CharacterIterator ci = new StringCharacterIterator(string); return read(ci, FIRST); } /** * 3rd public input method, should mostly not be used directly * Called ONLY by the other 2 read methods ({@link #read(String)} and {@link #read(CharacterIterator)}) * @param ci * @param start * @return the Object which represents the JSON string, null for invalid */ public Object read(CharacterIterator ci, int start) { it = ci; switch (start) { case FIRST: c = it.first(); break; case CURRENT: c = it.current(); break; case NEXT: c = it.next(); break; } Object o = read(); if (MARK_END_INPUT.equals(o) || MARK_COLON.equals(o)) { o = null; // SPECIAL cases - empty or invalid input } return o; } private Object read() { skipWhiteSpace(); char ch = c; next(); switch (ch) { case '"': token = string(); break; case '[': token = array(); break; case ']': token = MARK_ARRAY_END; break; case ',': token = MARK_COMMA; break; case '{': token = object(); break; case '}': token = MARK_OBJECT_END; break; case ':': token = MARK_COLON; break; case 't': next(); next(); next(); // assumed r-u-e token = Boolean.TRUE; break; case'f': next(); next(); next(); next(); // assumed a-l-s-e token = Boolean.FALSE; break; case 'n': next(); next(); next(); // assumed u-l-l token = null; break; case StringCharacterIterator.DONE: token = MARK_END_INPUT; break; default: if (Character.isDigit(ch) || ch == '-') { // Push this back on so it's part of the number c = it.previous(); token = number(); } } // System.out.println("token: " + token); // enable this line to see the token stream return token; } private Object object() { Map<Object, Object> ret = new ArrayOrderedMap<Object, Object>(); Object key = read(); while (token != MARK_OBJECT_END && token != MARK_END_INPUT) { read(); // should be a colon if (token != MARK_OBJECT_END) { ret.put(key, read()); if (read() == MARK_COMMA) { key = read(); } } } if (MARK_END_INPUT.equals(token)) { ret = null; // did not end the object in a valid way } return ret; } private Object array() { List<Object> ret = new ArrayList<Object>(); Object value = read(); while (token != MARK_ARRAY_END && token != MARK_END_INPUT) { ret.add(value); if (read() == MARK_COMMA) { value = read(); } } if (MARK_END_INPUT.equals(token)) { ret = null; // did not end the array in a valid way } return ret; } private Object number() { int length = 0; boolean isFloatingPoint = false; buf.setLength(0); if (c == '-') { add(); } length += addDigits(); if (c == '.') { add(); length += addDigits(); isFloatingPoint = true; } if (c == 'e' || c == 'E') { add(); if (c == '+' || c == '-') { add(); } addDigits(); isFloatingPoint = true; } String s = buf.toString(); // more friendly handling of numbers Object num = null; if (isFloatingPoint) { if (length < 10) { num = Float.valueOf(s); } else if (length < 17) { num = Double.valueOf(s); } else { num = new BigDecimal(s); } } else { if (length < 10) { num = Integer.valueOf(s); } else if (length < 19) { num = Long.valueOf(s); } else { num = new BigInteger(s); } } return num; } private int addDigits() { int ret; for (ret = 0; Character.isDigit(c); ++ret) { add(); } return ret; } private Object string() { buf.setLength(0); while (c != '"' && c != StringCharacterIterator.DONE) { if (c == '\\') { next(); if (c == 'u') { add(unicode()); } else { Object value = escapes.get(new Character(c)); if (value != null) { add(((Character) value).charValue()); } } } else { add(); } } // unterminated string will terminate automatically next(); return buf.toString(); } private void add(char cc) { buf.append(cc); next(); } private void add() { add(c); } private char unicode() { int value = 0; for (int i = 0; i < 4; ++i) { switch (next()) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': value = (value << 4) + c - '0'; break; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': value = (value << 4) + (c - 'a') + 10; break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': value = (value << 4) + (c - 'A') + 10; break; } } return (char) value; } } }