All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glassfish.admingui.common.util.JSONUtil Maven / Gradle / Ivy

There is a newer version: 4.1.2.181
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2013 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package org.glassfish.admingui.common.util;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 *  

This class provides basic JSON encoding / decoding. It has 2 primary * methods that are of interest. The first allows you to encode a Java * Object into JSON. The other allows you to create a Java data * structure from a JSON String. See:

* *
  • {@link #jsonToJava(String json)}
  • *
  • {@link #javaToJSON(Object obj, int depth)}
*/ public class JSONUtil { private static final String ABORT_PROCESSING = "____EnD___"; private static final String COLON = "____CoLoN___"; private static final String COMMA = "____CoMmA___"; private static final String NULL = "____NuLl___"; /** *

This method returns a Java representation of the given JSON * String. The Java data structure created will be created using * Map's, String's, Long's, Float's, Boolean's, and List's as * specified by the JSON String.

*/ public static Object jsonToJava(String json) { return replaceSpecial(jsonToJava(new JsonChars(json))); } /** *

This method attempts to convert the given Object into a JSON String * to given depth. If -1 (or lower) is supplied for depth, it will * walk upto a default depth of 10 levels of the given Object. If 0 * is supplied, it will simply return "". 1 will encode the current * Object, but no children. 2 will encode the given Object and its * direct children (if any), and so on.

* *

Strings, Longs, Float, and primitives are considered to not have * child Objects. Objects which have a public no-argument getXYZ() * method are considered to be child Objects. Maps and Collections * will be walked.

*/ public static String javaToJSON(Object obj, int depth) { if (depth == 0) { // Make sure we do nothing if told to do nothing... return ""; } else if (depth == -1) { // To prevent recursion... depth = 10; } String value = ""; if (obj == null) { value = "null"; } else if (obj instanceof String) { String chStr; int len; StringCharacterIterator it = new StringCharacterIterator((String) obj); char ch = it.first(); StringBuilder builder = new StringBuilder(((String) obj).length() << 2); builder.append("\""); while (ch != StringCharacterIterator.DONE) { switch (ch) { case '\t': builder.append("\\t"); break; case '\n': builder.append("\\n"); break; case '\r': builder.append("\\r"); break; case '\b': builder.append("\\b"); break; case '\f': builder.append("\\f"); break; case '&': case '<': case '>': case '(': case ')': case '{': case '}': case ':': case '/': case '\\': case '\'': case '"': builder.append("\\"); builder.append(ch); break; default: // Check if we should unicode escape this... if ((ch > 0x7e) || (ch < 0x20)) { builder.append("\\u"); chStr = Integer.toHexString(ch); len = chStr.length(); for (int idx=4; idx > len; idx--) { // Add leading 0's builder.append('0'); } builder.append(chStr); } else { builder.append(ch); } break; } ch = it.next(); } builder.append("\""); value = builder.toString(); } else if ((obj instanceof Boolean) || (obj instanceof Number)) { value = obj.toString(); } else if (obj instanceof Object[]) { StringBuilder builder = new StringBuilder("["); boolean first = true; for (Object element : ((Object []) obj)) { if (first) { first = false; } else { builder.append(','); } if (depth == 1) { // Treat as String, but don't try to go deeper... builder.append(javaToJSON(element.toString(), 1)); } else { // Recurse... builder.append(javaToJSON(element, depth-1)); } } builder.append("]"); value = builder.toString(); } else if (obj instanceof Map) { StringBuilder builder = new StringBuilder("{"); String key; boolean first = true; Map map = ((Map) obj); Iterator it = map.keySet().iterator(); while (it.hasNext()) { if (first) { first = false; } else { builder.append(','); } key = it.next().toString(); builder.append(javaToJSON(key, 1) + ":"); if (depth == 1) { // Treat as String, but don't try to go deeper... builder.append(javaToJSON(map.get(key).toString(), 1)); } else { // Recurse... builder.append(javaToJSON(map.get(key), depth-1)); } } builder.append("}"); value = builder.toString(); } else if (obj instanceof Collection) { StringBuilder builder = new StringBuilder("["); boolean first = true; Iterator it = ((Collection) obj).iterator(); while (it.hasNext()) { if (first) { first = false; } else { builder.append(','); } if (depth == 1) { // Treat as String, but don't try to go deeper... builder.append(javaToJSON(it.next().toString(), 1)); } else { // Recurse... builder.append(javaToJSON(it.next(), depth-1)); } } builder.append("]"); value = builder.toString(); } else { // Object StringBuilder builder = new StringBuilder("{"); String methodName; Object result; boolean first = true; Iterator it = getGetters(obj).iterator(); while (it.hasNext()) { if (first) { first = false; } else { builder.append(','); } methodName = it.next(); // Drop "get"... builder.append(javaToJSON(methodName.substring(3), 1) + ":"); result = invokeGetter(obj, methodName); if ((result != null) && (depth == 1)) { // Treat as String, but don't try to go deeper... builder.append(javaToJSON(result.toString(), 1)); } else { // Recurse... builder.append(javaToJSON(result, depth-1)); } } builder.append("}"); value = builder.toString(); } return value; } /** *

This method invokes a getter on the given object.

* *

NOTE: I found a VERY similar method defined in IntegrationPoint... * at least I'm consistent. ;) These should probably be combined.

*/ private static Object invokeGetter(Object obj, String methodName) { try { return obj.getClass().getMethod(methodName).invoke(obj); } catch (Exception ex) { // Unable to execute it, return null... return null; } } /** *

This method returns the names of the public no-arg getters on the * given Object.

*/ private static List getGetters(Object obj) { List result = new ArrayList(); for (Method method : obj.getClass().getMethods()) { if (method.getName().startsWith("get") && ((method.getModifiers() & Modifier.PUBLIC) != 0) && (method.getParameterTypes().length == 0) && (!method.getName().equals("getClass")) && (!method.getReturnType().getName().equals("void"))) { result.add(method.getName()); } } return result; } /** *

This is the primary switching method which determines the context * in which the processing should occur.

*/ private static Object jsonToJava(JsonChars json) { Object value = null; while (json.hasNext() && (value == null)) { char ch = json.next(); switch (ch) { case '{' : value = readObject(json); break; case '[' : value = readArray(json); break; case '}' : case ']' : if (json.isAtContextEnd()) { // Stop processing value = ABORT_PROCESSING; } else { throw new IllegalArgumentException("Expected '" + json.peekContextEnd() + "' but found '" + json.current() + "' instead!"); } break; case '-' : case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : value = readNumber(json); break; case '\'' : case '"' : value = readString(json); break; case 'T' : case 't' : value = readConstant(json, "true"); break; case 'F' : case 'f' : value = readConstant(json, "false"); break; case 'N' : case 'n' : value = readConstant(json, "null"); break; case ' ' : case '\t' : case '\r' : case '\n' : case '\b' : case '\f' : // Ignore whitespace break; case ':' : value = COLON; break; case ',' : value = COMMA; break; default: throw new IllegalArgumentException( "Unexpected char '" + json.current() + "' near: " + json.getContext(30) + "!"); } } return value; } /** *

This method creates a HashMap to represent the JSON Object.

*/ private static Map readObject(JsonChars json) { // Save the ending char... json.pushContextEnd('}'); // Create the Map Map map = new HashMap(10); Object tmp = null; Object key = null; Object value = null; while (!json.isAtContextEnd()) { // Get the key key = replaceSpecial(jsonToJava(json)); if (json.isAtContextEnd()) { // Abort... break; } if (!(key instanceof String)) { throw new IllegalArgumentException( "Object keys must be a String!"); } // Get the Colon... if (!(jsonToJava(json).equals(COLON))) { throw new IllegalArgumentException( "Object keys must be followed by a colon (:)!"); } // Get the value value = replaceSpecial(jsonToJava(json)); // Get the comma between properties (may also be context end) tmp = jsonToJava(json); if ( (!(tmp.equals(COMMA))) && !json.isAtContextEnd()) { throw new IllegalArgumentException( "Expected comma (,) or end curly brace (}), but found (" + tmp + ") instead! Near: (" + json.getContext(30) + ")"); } // Add the value to the Map... map.put((String) key, value); } // Remove the context end and return json.popContextEnd(); return map; } /** *

This function will process a JSON string and convert it into * an array.

*/ private static List readArray(JsonChars json) { // Save the ending char... json.pushContextEnd(']'); // Create the List List list = new ArrayList(10); Object tmp = null; Object value = null; while (!json.isAtContextEnd()) { // Get the value value = replaceSpecial(jsonToJava(json)); if (!json.isAtContextEnd()) { // Get the comma between properties (may also be context end) tmp = jsonToJava(json); if (( !(tmp.equals(COMMA))) && !json.isAtContextEnd()) { throw new IllegalArgumentException( "Expected comma (,) or end curly brace (}), but found (" + tmp + ") instead!"); } } // Add the value to the List... if ((value == null) || ( !(value.equals(ABORT_PROCESSING)))) { list.add(value); } } // Remove the context end and return json.popContextEnd(); return list; } /** *

This function reads a String and returns it.

*/ private static String readString(JsonChars json) { // Save the ending char... json.pushContextEnd(json.current()); // Build the String... StringBuilder builder = new StringBuilder(); char ch = json.next(); while (!json.isAtContextEnd()) { if (ch == '\\') { ch = json.next(); switch (ch) { case 'b' : ch = '\b'; break; case 'f' : ch = '\f'; break; case 'n' : ch = '\n'; break; case 'r' : ch = '\r'; break; case 't' : ch = '\t'; break; case 'u' : // Handle unicode characters builder.appendCodePoint(Integer.parseInt("" + json.next() + json.next() + json.next() + json.next())); continue; case '"' : case '\'' : case '\\' : case '/' : // Just allow this char to be added break; default: // Ignore illegal escape character. break; } } builder.append(ch); ch = json.next(); } // Return the result json.popContextEnd(); return builder.toString(); } /** *

Returns either a Float or an Long depending on the data.

*/ private static Object readNumber(JsonChars json) { StringBuilder builder = new StringBuilder(); char ch = json.current(); if (ch == '-') { builder.append('-'); ch = json.next(); } boolean hasDecimal = false; boolean hasExp = false; boolean done = false; while (!done) { switch (ch) { case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : builder.append(ch); break; case '.' : if (hasDecimal) { throw new IllegalArgumentException( "Error while parsing number! Found multiple decimal points."); } hasDecimal = true; builder.append(ch); break; case 'e' : case 'E' : // We have an exponent if (hasExp) { throw new IllegalArgumentException( "An attempt was made to parse an Long value, however, it was malformed (had to exponents)."); } hasExp = true; builder.append(ch); ch = json.next(); if ((ch == '-') || (ch == '+')) { builder.append(ch); ch = json.next(); } if ((ch < '0') || (ch > '9')) { throw new IllegalArgumentException( "Required a digit after an exponent, however received: '" + ch + "'."); } builder.append(ch); break; default: done = true; continue; } try { ch = json.next(); } catch (IndexOutOfBoundsException ioobe) { done = true; } } // Numbers don't have an ending delimiter, so we need to push the last // value back onto the queue json.unread(); // Return the number... return (hasDecimal || hasExp) ? (Object) Float.valueOf(builder.toString()) : (Object) Long.valueOf(builder.toString()); } /** *

This method attempts to read a true/false/null value and returns a * Boolean for true/false values or {@link #NULL} for null values.

*/ private static Object readConstant(JsonChars json, String constant) { byte[] good = constant.getBytes(); int len = good.length; char ch; boolean match = true; for (int idx=1; idx endContext = new Stack(); /** * Constructor. */ JsonChars(String json) { string = json; len = string.length(); } /** *

Returns the current byte.

*/ char current() { return string.charAt(loc-1); } /** *

Returns the current byte and increments the location by 1.

*/ char next() { return string.charAt(loc++); //return (loc Backs up the iteration 1 character.

*/ void unread() { loc--; } /** *

This function returns a String that represents the content * around the current position. The width property * specifies how far before and after the current position that * should be returned as part of the String.

*/ String getContext(int width) { int before = loc - width; if (loc < 0) { loc = 0; } if (before < 0) { before = 0; } int after = loc + width; if (after > len) { after = len; } return string.substring(before, after - before); // new String(bytes, before, after - before); } /** *

Returns the length of the JSON String.

*/ int getLength() { return len; } /** *

Returns true if there are more characters to be parsed.

*/ boolean hasNext() { return loc Returns true if the end of the current context is reached. For * example if the current context is an Object, the ending for an * Object is a '}' byte.

*/ boolean isAtContextEnd() { return !hasNext() || (string.charAt(loc-1) == endContext.peek()); } void pushContextEnd(char end) { endContext.push(end); } char popContextEnd() { return endContext.pop(); } char peekContextEnd() { return endContext.peek(); } } /** *

This method substitutes the special Strings to their intended * representations (null, ':', and ','). This method does nothing * except return the given value if the requested value is not a * "special" value.

*/ private static Object replaceSpecial(Object val) { if (val instanceof String) { String strVal = (String) val; if (COLON.equals(strVal)) { val = ':'; } else if (COMMA.equals(strVal)) { val = ','; } else if (NULL.equals(strVal)) { val = null; } } return val; } }