/*
* 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;
/**
* <p> 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:</p>
*
* <ul><li>{@link #jsonToJava(String json)}</li>
* <li>{@link #javaToJSON(Object obj, int depth)}</li></ul>
*/
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___";
/**
* <p> 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.</p>
*/
public static Object jsonToJava(String json) {
return replaceSpecial(jsonToJava(new JsonChars(json)));
}
/**
* <p> 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.</p>
*
* <p> 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.</p>
*/
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<String> 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;
}
/**
* <p> This method invokes a getter on the given object.</p>
*
* <p> NOTE: I found a VERY similar method defined in IntegrationPoint...
* at least I'm consistent. ;) These should probably be combined.</p>
*/
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;
}
}
/**
* <p> This method returns the names of the public no-arg getters on the
* given Object.</p>
*/
private static List<String> getGetters(Object obj) {
List<String> result = new ArrayList<String>();
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;
}
/**
* <p> This is the primary switching method which determines the context
* in which the processing should occur.</p>
*/
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;
}
/**
* <p> This method creates a HashMap to represent the JSON Object.</p>
*/
private static Map<String, Object> readObject(JsonChars json) {
// Save the ending char...
json.pushContextEnd('}');
// Create the Map
Map<String, Object> map = new HashMap<String, Object>(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;
}
/**
* <p> This function will process a JSON string and convert it into
* an array.</p>
*/
private static List<Object> readArray(JsonChars json) {
// Save the ending char...
json.pushContextEnd(']');
// Create the List
List<Object> list = new ArrayList<Object>(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;
}
/**
* <p> This function reads a String and returns it.</p>
*/
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();
}
/**
* <p> Returns either a Float or an Long depending on the data.</p>
*/
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());
}
/**
* <p> This method attempts to read a true/false/null value and returns a
* Boolean for true/false values or {@link #NULL} for null values.</p>
*/
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<len; idx++) {
ch = json.next();
if (ch != good[idx]) {
throw new IllegalArgumentException(
"Expected constant (" + constant + ")!");
}
}
// We compared successfully...
return constant.equals("null") ? NULL : Boolean.valueOf(constant);
}
static class JsonChars {
private String string;
private int len;
private int loc = 0;
private Stack<Character> endContext = new Stack<Character>();
/**
* Constructor.
*/
JsonChars(String json) {
string = json;
len = string.length();
}
/**
* <p> Returns the current byte.</p>
*/
char current() {
return string.charAt(loc-1);
}
/**
* <p> Returns the current byte and increments the location by 1.</p>
*/
char next() {
return string.charAt(loc++);
//return (loc<len) ? string.charAt(loc++) : null;
}
/**
* <p> Backs up the iteration 1 character.</p>
*/
void unread() {
loc--;
}
/**
* <p> This function returns a String that represents the content
* around the current position. The <code>width</code> property
* specifies how far before and after the current position that
* should be returned as part of the <code>String</code>.</p>
*/
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);
}
/**
* <p> Returns the length of the JSON String.</p>
*/
int getLength() {
return len;
}
/**
* <p> Returns true if there are more characters to be parsed.</p>
*/
boolean hasNext() {
return loc<len;
}
/**
* <p> 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.</p>
*/
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();
}
}
/**
* <p> 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.</p>
*/
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;
}
}