agilejson.JSON.java Source code

Java tutorial

Introduction

Here is the source code for agilejson.JSON.java

Source

/**
 * JSON.java
 * Copyright 2009 Michael Gottesman
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * The Software shall be used for Good, not Evil.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 **/

package agilejson;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.lang.reflect.Method;
import java.lang.annotation.*;
import java.util.HashSet;
import java.util.Set;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import agilejson.special.SpecialHashSet;

import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONStringer;
import org.json.JSONException;
import org.json.JSONWriter;

/**
 * Important Notes on usage:
 * 2. If you use primitives and do not assign a value to them,
 * they will take a default value i.e. 0 for integer.
 * 3. If on the other hand you use the object version of those primitives,
 * (i.e. Integer vs int, Double vs double), the resulting JSON
 * translation will be null, not 0 (or whatever the default value is
 * @author gottesmm
 */
public class JSON {

    private static Pattern decamelcasePattern = Pattern.compile("([a-z_0-9]+)([A-Z])");

    /**
     * This method takes in a primitive that has been converted to an object
     * and creates a copy of it so that .equals results in different objects.
     * @param v
     * @throws java.lang.Exception
     */
    private static Object getObjectForPrimitive(Object v) throws Exception {
        Class c = v.getClass();
        if (c == Byte.class) {
            return new String(new byte[] { ((Byte) v).byteValue() });
        } else if (c == Boolean.class) {
            return new Boolean((Boolean) v);
        } else if (c == Character.class) {
            return new Character((Character) v);
        } else if (c == Short.class) {
            return new Short((Short) v);
        } else if (c == Integer.class) {
            return new Integer((Integer) v);
        } else if (c == Long.class) {
            return new Long((Long) v);
        } else if (c == Float.class) {
            return new Float((Float) v);
        } else if (c == Double.class) {
            return new Double((Double) v);
        } else {
            throw new Exception("Unknown Primitive");
        }
    }

    public static String deCamelCase(String s) {
        Matcher m = decamelcasePattern.matcher(s.substring(1));
        if (!m.find()) {
            return s.toLowerCase();
        }

        String res = String.valueOf(Character.toLowerCase(s.charAt(0)));
        int lastEnd;
        while (true) {
            res += m.group(1);
            res += "_" + m.group(2).toLowerCase();
            lastEnd = m.end();
            if (!m.find()) {
                return res + s.substring(lastEnd + 1);
            }
        }
    }

    /**
     * This class is a JSONStringer which does not escape any data.
     * Instead it outputs the json unescaped and allows for all of the completed
     * json to be escaped together at the end.
     * 
     * This stops multiple JSON escapes from occuring resulting in nastiness like:
     * Hello, \\\\\\t how are you doing?\\\\\\n.
     * 
     * This makes it much easier to have multiple levels of JSON
     */
    protected static class NoEscapesStringer extends JSONStringer {

        @Override
        public JSONWriter value(Object o) throws JSONException {
            return this.append(o.toString());
        }

        /**
         * Append a key. The key will be associated with the next value. In an
         * object, every value must be preceded by a key.
         * @param s A key string.
         * @return this
         * @throws JSONException If the key is out of place. For example, keys
         *  do not belong in arrays or if the key is null.
         */
        @Override
        public JSONWriter key(String s) throws JSONException {
            if (s == null) {
                throw new JSONException("Null key.");
            }
            if (this.mode == 'k') {
                try {
                    if (this.comma) {
                        this.writer.write(',');
                    }
                    this.writer.write('"' + s + '"');
                    this.writer.write(':');
                    this.comma = false;
                    this.mode = 'o';
                    return this;
                } catch (IOException e) {
                    throw new JSONException(e);
                }
            }
            throw new JSONException("Misplaced key.");
        }
    }

    private static Class[] _primitives = { Object.class, String.class, Short.class, Byte.class, Character.class,
            Boolean.class, Integer.class, Float.class, Double.class, Long.class };
    protected static Set PRIMITIVES = new HashSet(Arrays.asList(_primitives));
    private static Class[] _primitivearrays = { Short[].class, Byte[].class, Character[].class, Boolean[].class,
            Integer[].class, Float[].class, Double[].class, Long[].class, short[].class, byte[].class, char[].class,
            boolean[].class, int[].class, float[].class, double[].class, long[].class };
    protected static Set PRIMITIVEARRAYS = new HashSet(Arrays.asList(_primitivearrays));

    /**
     * Public interface to protected toJSON method.
     * @param o
     * @return valid Json
     * @throws org.json.JSONException
     * @throws java.lang.IllegalAccessException
     */
    public static String toJSON(Object o) throws JSONException, IllegalAccessException {
        Set alreadyVisited = new SpecialHashSet();
        return JSON.toJSON(o, alreadyVisited);
    }

    /**
     * Escapes all of the characters in the string that according
     * to the javascript standard are able to be escaped.
     * WARNING:
     * If you have already js-escaped a string before, 
     * you will have the dreaded \\\\\\ problem where your
     * escapes are themselves escaped. This is needless waste
     * of space so be forewarned.
     * @param string
     * @return escaped string
     */
    protected static String escape(String string) {
        if (string == null || string.length() == 0) {
            return "";
        }

        char b;
        char c = 0;
        int i;
        int len = string.length();
        StringBuffer sb = new StringBuffer(len);
        String t;

        for (i = 0; i < len; i += 1) {
            b = c;
            c = string.charAt(i);
            switch (c) {
            case '\\':
            case '"':
                sb.append('\\');
                sb.append(c);
                break;
            case '/':
                if (b == '<') {
                    sb.append('\\');
                }
                sb.append(c);
                break;
            case '\b':
                sb.append("\\b");
                break;
            case '\t':
                sb.append("\\t");
                break;
            case '\n':
                sb.append("\\n");
                break;
            case '\f':
                sb.append("\\f");
                break;
            case '\r':
                sb.append("\\r");
                break;
            default:
                if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) {
                    t = "000" + Integer.toHexString(c);
                    sb.append("\\u" + t.substring(t.length() - 4));
                } else {
                    sb.append(c);
                }
            }
        }
        return sb.toString();
    }

    public static void jsonifyArray(Object o, JSONStringer s, Set alreadyVisited)
            throws JSONException, IllegalAccessException {
        s.array();
        Object[] array = (Object[]) o;
        Object _o;
        Class _c;
        for (int j = 0; j < array.length; j++) {
            s.value(JSON.toJSON(array[j], alreadyVisited));
        }
        s.endArray();
    }

    /**
     * Note I am assuming your methods are named something like "getObject".
     * With that in my mind I set the name of the json object to the substring
     * from 3 to the end, downcasing it, resulting in get being dropped
     * and the second word capitalization being lowered. So you get
     * object.
     * @param o
     * @param methods 
     * @param c
     * @param s
     * @param alreadyVisited
     * @return boolean on whether any items were written.
     * @throws java.lang.IllegalAccessException
     * @throws org.json.JSONException 
     */
    private static boolean jsonifyGetters(Object o, Method[] methods, JSONStringer s, Set alreadyVisited)
            throws IllegalAccessException, JSONException {
        boolean anyOutput = false;
        for (int i = 0; i < methods.length; i++) {
            TOJSON a;
            if (methods[i].getParameterTypes().length == 0
                    && (a = methods[i].getAnnotation(TOJSON.class)) != null) {
                Object returnValue;
                try {
                    returnValue = methods[i].invoke(o, ((Object[]) null));
                } catch (Exception e) {
                    continue;
                }
                if (returnValue == null) {
                    if (!anyOutput) {
                        anyOutput = true;
                        s.object();
                    }
                    if (a.fieldName().length() != 0) {
                        s.key(JSON.deCamelCase(a.fieldName()));
                    } else if (a.contentLength() == -1) {
                        s.key(JSON.deCamelCase(methods[i].getName().substring(a.prefixLength())));
                    } else {
                        s.key(JSON.deCamelCase(methods[i].getName().substring(a.prefixLength(),
                                a.prefixLength() + a.contentLength())));
                    }
                    s.value(JSON.toJSON(returnValue, alreadyVisited));
                } else {
                    if (!alreadyVisited.contains(returnValue)) {
                        if (!anyOutput) {
                            anyOutput = true;
                            s.object();
                        }
                        if (a.fieldName().length() != 0) {
                            s.key(JSON.deCamelCase(a.fieldName()));
                        } else if (a.contentLength() == -1) {
                            s.key(JSON.deCamelCase(methods[i].getName().substring(a.prefixLength())));
                        } else {
                            s.key(JSON.deCamelCase(
                                    methods[i].getName().substring(a.prefixLength(), a.contentLength())));
                        }

                        if (a.base64()) {
                            // swap the quotes so they are not encoded in the base64 value
                            String json = JSON.toJSON(returnValue, alreadyVisited);
                            s.value("\"" + Base64.encodeBytes(json.substring(1, json.length() - 1)) + "\"");
                        } else {
                            s.value(JSON.toJSON(returnValue, alreadyVisited));
                        }
                    } else {
                        continue;
                    }
                }
            }
        }
        if (anyOutput) {
            s.endObject();
        }
        return anyOutput;
    }

    /**
     * Work Horse of the library
     * @param o
     * @param alreadyVisited
     * @return Proper Json String
     * @throws org.json.JSONException
     * @throws java.lang.IllegalAccessException
     */
    protected static String toJSON(Object o, Set alreadyVisited) throws JSONException, IllegalAccessException {

        // If null return JSON's null value, null
        if (o == null) {
            return "null";
        }

        // Get class for reflection purposes
        Class c = o.getClass();

        // Make sure that given a primitive, it is not added to already visited
        // This is for two reasons:
        // 1. Classes are sealed so can not point to other objects.
        // 2. String, et. al., have overridden equals methods which is true
        //     given equality of value, not equality of reference.
        // This results in the loss of values in the json representation
        if (!PRIMITIVES.contains(c)) {
            alreadyVisited.add(o);
        }

        // Use no excape stringer b/c we want to escape strings only once,
        // to stop things like \\\\\\n, etc.
        JSONStringer s = new NoEscapesStringer();

        // If Array handle elements
        if ((Object[].class).isAssignableFrom(c)) {
            if ((Byte[].class).isAssignableFrom(c)) {
                Byte[] B = (Byte[]) o;
                byte[] b = new byte[B.length];
                for (int i = 0; i < B.length; i++) {
                    if (B[i] != null)
                        b[i] = B[i].byteValue();
                    else
                        b[i] = 48;
                }
                return "\"" + escape(new String(b)) + "\"";
            } else if ((Character[].class).isAssignableFrom(c)) {
                Character[] C = (Character[]) o;
                char[] primitiveC = new char[C.length];
                for (int i = 0; i < C.length; i++) {
                    if (C[i] != null)
                        primitiveC[i] = C[i].charValue();
                    else
                        primitiveC[i] = '0';
                }
                return "\"" + escape(new String(primitiveC)) + "\"";
            } else {
                jsonifyArray(o, s, alreadyVisited);
            }
        } else {
            // Check this part
            if (PRIMITIVEARRAYS.contains(c)) {
                // If byte/char array return as a string. Otherwise returns
                // as a space delimited Hex Representation of the bytes
                if ((byte[].class).isAssignableFrom(c)) {
                    try {
                        return "\"" + escape(new String((byte[]) o, "UTF-8")) + "\"";
                    } catch (UnsupportedEncodingException ex) {
                        return "\"" + escape(new String((byte[]) o)) + "\"";
                    }
                } else if ((char[].class).isAssignableFrom(c)) {
                    return "\"" + escape(new String((char[]) o)) + "\"";
                } else {
                    s.array();
                    if ((short[].class).isAssignableFrom(c)) {
                        short[] array = (short[]) o;
                        for (short b : array) {
                            s.value(String.valueOf(b));
                        }
                    } else if ((int[].class).isAssignableFrom(c)) {
                        int[] array = (int[]) o;
                        for (int b : array) {
                            s.value(String.valueOf(b));
                        }
                    } else if ((long[].class).isAssignableFrom(c)) {
                        long[] array = (long[]) o;
                        for (long b : array) {
                            s.value(String.valueOf(b));
                        }
                    } else if ((float[].class).isAssignableFrom(c)) {
                        float[] array = (float[]) o;
                        for (float b : array) {
                            s.value(String.valueOf(b));
                        }
                    } else if ((double[].class).isAssignableFrom(c)) {
                        double[] array = (double[]) o;
                        for (double b : array) {
                            s.value(String.valueOf(b));
                        }
                    } else {
                        boolean[] array = (boolean[]) o;
                        for (boolean b : array) {
                            s.value(String.valueOf(b));
                        }
                    }
                    s.endArray();
                }
            } else if (String.class.isAssignableFrom(c) || (Character.TYPE).isAssignableFrom(c)
                    || (Character.class).isAssignableFrom(c)) {
                return '"' + escape(o.toString()) + '"';
            } else if (PRIMITIVES.contains(c) || JSONObject.class.isAssignableFrom(c)
                    || JSONArray.class.isAssignableFrom(c)) {
                return o.toString();
            } else {
                // Note json object is created inside jsonify getters
                // this is because we dont know whether or not we have
                // methods which are annotated as @TOJSON until we get in there.
                // This allows us to just tostring the output if no methods
                // have been annotated.
                Method[] m = c.getMethods();
                if (m.length != 0) {
                    if (!jsonifyGetters(o, m, s, alreadyVisited)) {
                        return "\"" + escape(o.toString()) + "\"";
                    }
                } else {
                    return "\"" + escape(o.toString()) + "\"";
                }
            }
        }
        return s.toString();
    }
}