JavaXT
|
|
JSONObject Classpackage javaxt.json; import javaxt.utils.Value; import java.io.IOException; import java.io.Writer; import java.util.Map.Entry; import java.lang.reflect.Array; //****************************************************************************** //** JSONObject //****************************************************************************** /** * A JSON object consists key/value pairs. The string representation of a * JSON object is a widely-used standard format for exchanging data. The * string begins with a left brace "{" and ends with a right brace "}". * Keys and values are separated by colon ":". Each key/value pair is * separated by comma ",". * * @author Source adapted from json.org (2016-08-15) * ******************************************************************************/ public class JSONObject extends javaxt.utils.Record { //************************************************************************** //** Constructor //************************************************************************** public JSONObject() { super(); } //************************************************************************** //** Constructor //************************************************************************** /** Construct a JSONObject from a source JSON text string. This is the most * commonly used JSONObject constructor. * * @param source A string beginning with <code>{</code> <small>(left * brace)</small> and ending with <code>}</code> <small>(right brace) * </small>. */ public JSONObject(String source) throws JSONException { super(); if (source!=null) init(new JSONTokener(source)); } //************************************************************************** //** Constructor //************************************************************************** /** Construct a JSONObject from a JSONTokener. */ protected JSONObject(JSONTokener source) throws JSONException { super(); if (source!=null) init(source); } private void init(JSONTokener x) throws JSONException{ char c; String key; if (x.nextClean() != '{') { throw x.syntaxError("A JSONObject text must begin with '{'"); } for (;;) { c = x.nextClean(); switch (c) { case 0: throw x.syntaxError("A JSONObject text must end with '}'"); case '}': return; default: x.back(); key = x.nextValue().toString(); } // The key is followed by ':'. c = x.nextClean(); if (c != ':') { throw x.syntaxError("Expected a ':' after a key"); } // Use syntaxError(..) to include error location if (key != null) { // Check if key exists if (get(key).toObject() != null) { // key already exists //throw x.syntaxError("Duplicate key \"" + key + "\""); } // Only add value if non-null Object value = x.nextValue(); if (value!=null) { set(key, value); } } // Pairs are separated by ','. switch (x.nextClean()) { case ';': case ',': if (x.nextClean() == '}') { return; } x.back(); break; case '}': return; default: throw x.syntaxError("Expected a ',' or '}'"); } } } //************************************************************************** //** Constructor //************************************************************************** /** Construct a JSONObject from a javaxt.utils.Record. */ public JSONObject(javaxt.utils.Record record) throws JSONException { for (String key : record.keySet()){ this.set(key, record.get(key)); } } //************************************************************************** //** Constructor //************************************************************************** /** Construct a JSONObject from an XML Document. */ public JSONObject(org.w3c.dom.Document xml) throws JSONException { this(javaxt.xml.DOM.getOuterNode(xml)); } //************************************************************************** //** Constructor //************************************************************************** /** Construct a JSONObject from an XML Node. */ public JSONObject(org.w3c.dom.Node node) throws JSONException { JSONObject json = new JSONObject(); if (javaxt.xml.DOM.hasChildren(node)) { traverse(node, json); } else{ json.set(node.getNodeName(), node.getTextContent()); } for (String key : json.keySet()){ this.set(key, json.get(key)); } } private void traverse(org.w3c.dom.Node node, JSONObject json){ if (node.getNodeType()==1){ if (javaxt.xml.DOM.hasChildren(node)) { JSONObject _json = new JSONObject(); org.w3c.dom.NodeList xmlNodeList = node.getChildNodes(); for (int i=0; i<xmlNodeList.getLength(); i++){ traverse(xmlNodeList.item(i), _json); } json.set(node.getNodeName(), _json); } else{ json.set(node.getNodeName(), node.getTextContent()); } } } //************************************************************************** //** getValue //************************************************************************** /** Returns the value associated with a key. */ public JSONValue get(String key) { return new JSONValue(super.get(key).toObject()); } //************************************************************************** //** getValue //************************************************************************** /** Returns a nested value associated with the given keys */ public JSONValue get(String... path) { JSONValue val = null; if (path!=null){ for (String key : path){ if (val==null) val = get(key); else val = val.get(key); if (val.isNull()){ break; } } } if (val==null) val = new JSONValue(null); return val; } //************************************************************************** //** set //************************************************************************** /** Used to set the value for a given key. */ public void set(String key, Object value) throws JSONException { if (value instanceof Value){ value = ((Value) value).toObject(); } if (value instanceof String){ String str = (String) value; str = str.trim(); if (str.length()==0) str = null; value = str; } if (value!=null) { if (value.getClass().isArray()){ if (!(value instanceof byte[])){ JSONArray arr = new JSONArray(); for (int i=0; i<Array.getLength(value); i++) { Object o = Array.get(value, i); arr.add(o); } value = arr; } } testValidity(value); super.set(key, value); } else { super.remove(key); } } //************************************************************************** //** remove //************************************************************************** /** Remove a name and its value, if present. Returns the value that was * associated with the name, or null if there was no value. */ public JSONValue remove(String key) { return new JSONValue(super.remove(key).toObject()); } //************************************************************************** //** toString //************************************************************************** /** Returns the JSONObject as a String. For compactness, no whitespace is * added. If this would not result in a syntactically correct JSON text, * then null will be returned instead. */ public String toString() { try { return this.toString(0); } catch (Exception e) { return null; } } //************************************************************************** //** toString //************************************************************************** /** Returns a pretty-printed JSON text of this JSONObject. * @param indentFactor The number of spaces to add to each level of * indentation. */ public String toString(int indentFactor) { try{ java.io.StringWriter w = new java.io.StringWriter(); synchronized (w.getBuffer()) { return this.write(w, indentFactor, 0).toString(); } } catch(Exception e){ return null; } } //************************************************************************** //** writeValue //************************************************************************** protected static final Writer writeValue(Writer writer, Object value, int indentFactor, int indent) throws JSONException, IOException { if (value == null || value.equals(null)) { writer.write("null"); } else if (value instanceof Number) { // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary final String numberAsString = numberToString((Number) value); try { // Use the BigDecimal constructor for its parser to validate the format. @SuppressWarnings("unused") java.math.BigDecimal testNum = new java.math.BigDecimal(numberAsString); // Close enough to a JSON number that we will use it unquoted writer.write(numberAsString); } catch (NumberFormatException ex){ // The Number value is not a valid JSON number. // Instead we will quote it as a string quote(numberAsString, writer); } } else if (value instanceof Boolean) { writer.write(value.toString()); } else if (value instanceof javaxt.utils.Date) { writer.write(quote(((javaxt.utils.Date) value).toISOString())); } else if (value instanceof java.util.Date) { writer.write(quote(new javaxt.utils.Date(((java.util.Date) value)).toISOString())); } else if (value instanceof java.util.Calendar) { writer.write(quote(new javaxt.utils.Date(((java.util.Calendar) value)).toISOString())); } else if (value instanceof byte[]){ writer.write(quote(javaxt.utils.Base64.encode((byte[]) value))); } else if (value instanceof java.sql.Clob){ //Special case: Use javaxt.sql.Value to stringify Clobs writer.write(quote(new javaxt.sql.Value(value).toString())); } else if (value instanceof Enum<?>) { writer.write(quote(((Enum<?>)value).name())); } else if (value instanceof javaxt.sql.Model) { JSONObject json = ((javaxt.sql.Model) value).toJson(); json.write(writer, indentFactor, indent); } else if (value instanceof JSONObject) { ((JSONObject) value).write(writer, indentFactor, indent); } else if (value instanceof JSONArray) { ((JSONArray) value).write(writer, indentFactor, indent); // } else if (value instanceof Map) { // Map<?, ?> map = (Map<?, ?>) value; // new JSONObject(map).write(writer, indentFactor, indent); // } else if (value instanceof Collection) { // Collection<?> coll = (Collection<?>) value; // new JSONArray(coll).write(writer, indentFactor, indent); // } else if (value.getClass().isArray()) { // new JSONArray(value).write(writer, indentFactor, indent); } else { if (value.getClass().isArray()){ JSONArray arr = new JSONArray(); for (int i=0; i<Array.getLength(value); i++) { Object o = Array.get(value, i); arr.add(o); } java.io.StringWriter w = new java.io.StringWriter(); synchronized (w.getBuffer()) { writeValue(w, arr, 0, 0); writer.write(w.toString()); } } else{ quote(value.toString(), writer); } } return writer; } //************************************************************************** //** write //************************************************************************** private Writer write(Writer writer, int indentFactor, int indent) throws JSONException { try { boolean commanate = false; final int length = this.length(); writer.write('{'); if (length == 1) { final Entry<String,?> entry = super.entrySet().iterator().next(); final String key = entry.getKey(); writer.write(quote(key)); writer.write(':'); if (indentFactor > 0) { writer.write(' '); } try{ writeValue(writer, entry.getValue(), indentFactor, indent); } catch (Exception e) { throw new JSONException("Unable to write JSONObject value for key: " + key, e); } } else if (length != 0) { final int newindent = indent + indentFactor; for (final Entry<String,?> entry : this.entrySet()) { if (commanate) { writer.write(','); } if (indentFactor > 0) { writer.write('\n'); } indent(writer, newindent); final String key = entry.getKey(); writer.write(quote(key)); writer.write(':'); if (indentFactor > 0) { writer.write(' '); } try { writeValue(writer, entry.getValue(), indentFactor, newindent); } catch (Exception e) { throw new JSONException("Unable to write JSONObject value for key: " + key, e); } commanate = true; } if (indentFactor > 0) { writer.write('\n'); } indent(writer, indent); } writer.write('}'); return writer; } catch (IOException exception) { throw new JSONException(exception); } } //************************************************************************** //** indent //************************************************************************** protected static final void indent(Writer writer, int indent) throws IOException { for (int i = 0; i < indent; i += 1) { writer.write(' '); } } //************************************************************************** //** testValidity //************************************************************************** protected static void testValidity(Object o) throws JSONException { if (o != null) { if (o instanceof Double) { if (((Double) o).isInfinite() || ((Double) o).isNaN()) { throw new JSONException( "JSON does not allow non-finite numbers."); } } else if (o instanceof Float) { if (((Float) o).isInfinite() || ((Float) o).isNaN()) { throw new JSONException( "JSON does not allow non-finite numbers."); } } } } //************************************************************************** //** numberToString //************************************************************************** /** Produce a string from a Number. */ private static String numberToString(Number number) throws JSONException { if (number == null) { throw new JSONException("Null pointer"); } testValidity(number); // Shave off trailing zeros and decimal point, if possible. String string = number.toString(); if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) { while (string.endsWith("0")) { string = string.substring(0, string.length() - 1); } if (string.endsWith(".")) { string = string.substring(0, string.length() - 1); } } return string; } //************************************************************************** //** quote //************************************************************************** /** Returns a String correctly formatted for insertion in a JSON text. */ private static String quote(String string) { java.io.StringWriter sw = new java.io.StringWriter(); synchronized (sw.getBuffer()) { try { return quote(string, sw).toString(); } catch (IOException ignored) { // will never happen - we are writing to a string writer return ""; } } } private static Writer quote(String string, Writer w) throws IOException { if (string == null || string.length() == 0) { w.write("\"\""); return w; } char b; char c = 0; String hhhh; int i; int len = string.length(); w.write('"'); for (i = 0; i < len; i += 1) { b = c; c = string.charAt(i); switch (c) { case '\\': case '"': w.write('\\'); w.write(c); break; case '/': if (b == '<') { w.write('\\'); } w.write(c); break; case '\b': w.write("\\b"); break; case '\t': w.write("\\t"); break; case '\n': w.write("\\n"); break; case '\f': w.write("\\f"); break; case '\r': w.write("\\r"); break; default: if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) { w.write("\\u"); hhhh = Integer.toHexString(c); w.write("0000", 0, 4 - hhhh.length()); w.write(hhhh); } else { w.write(c); } } } w.write('"'); return w; } //****************************************************************************** //** JSONTokener //****************************************************************************** /** * A JSONTokener takes a source string and extracts characters and tokens * from it. It is used by the JSONObject and JSONArray constructors to parse * JSON source strings. * * @author json.org * @version 2014-05-03 * ******************************************************************************/ protected static class JSONTokener { /** current read character position on the current line. */ private long character; /** flag to indicate if the end of the input has been found. */ private boolean eof; /** current read index of the input. */ private long index; /** current line of the input. */ private long line; /** previous character read from the input. */ private char previous; /** Reader for the input. */ private final java.io.Reader reader; /** flag to indicate that a previous character was requested. */ private boolean usePrevious; /** the number of characters read in the previous line. */ private long characterPreviousLine; //************************************************************************** //** Constructor //************************************************************************** protected JSONTokener(String s) { java.io.Reader reader = new java.io.StringReader(s); this.reader = reader.markSupported() ? reader : new java.io.BufferedReader(reader); this.eof = false; this.usePrevious = false; this.previous = 0; this.index = 0; this.character = 1; this.characterPreviousLine = 0; this.line = 1; } /** * Back up one character. This provides a sort of lookahead capability, * so that you can test for a digit or letter before attempting to parse * the next number or identifier. * @throws JSONException Thrown if trying to step back more than 1 step * or if already at the start of the string */ protected void back() throws JSONException { if (this.usePrevious || this.index <= 0) { throw new JSONException("Stepping back two steps is not supported"); } this.decrementIndexes(); this.usePrevious = true; this.eof = false; } /** * Decrements the indexes for the {@link #back()} method based on the previous character read. */ private void decrementIndexes() { this.index--; if(this.previous=='\r' || this.previous == '\n') { this.line--; this.character=this.characterPreviousLine ; } else if(this.character > 0){ this.character--; } } /** * Get the hex value of a character (base16). * @param c A character between '0' and '9' or between 'A' and 'F' or * between 'a' and 'f'. * @return An int between 0 and 15, or -1 if c was not a hex digit. */ private static int dehexchar(char c) { if (c >= '0' && c <= '9') { return c - '0'; } if (c >= 'A' && c <= 'F') { return c - ('A' - 10); } if (c >= 'a' && c <= 'f') { return c - ('a' - 10); } return -1; } /** * Checks if the end of the input has been reached. * * @return true if at the end of the file and we didn't step back */ private boolean end() { return this.eof && !this.usePrevious; } /** * Determine if the source string still contains characters that next() * can consume. * @return true if not yet at the end of the source. * @throws JSONException thrown if there is an error stepping forward * or backward while checking for more data. */ private boolean more() throws JSONException { if(this.usePrevious) { return true; } try { this.reader.mark(1); } catch (IOException e) { throw new JSONException("Unable to preserve stream position", e); } try { // -1 is EOF, but next() can not consume the null character '\0' if(this.reader.read() <= 0) { this.eof = true; return false; } this.reader.reset(); } catch (IOException e) { throw new JSONException("Unable to read the next character from the stream", e); } return true; } /** * Get the next character in the source string. * * @return The next character, or 0 if past the end of the source string. * @throws JSONException Thrown if there is an error reading the source string. */ private char next() throws JSONException { int c; if (this.usePrevious) { this.usePrevious = false; c = this.previous; } else { try { c = this.reader.read(); } catch (IOException exception) { throw new JSONException(exception); } } if (c <= 0) { // End of stream this.eof = true; return 0; } this.incrementIndexes(c); this.previous = (char) c; return this.previous; } /** * Increments the internal indexes according to the previous character * read and the character passed as the current character. * @param c the current character read. */ private void incrementIndexes(int c) { if(c > 0) { this.index++; if(c=='\r') { this.line++; this.characterPreviousLine = this.character; this.character=0; }else if (c=='\n') { if(this.previous != '\r') { this.line++; this.characterPreviousLine = this.character; } this.character=0; } else { this.character++; } } } /** * Consume the next character, and check that it matches a specified * character. * @param c The character to match. * @return The character. * @throws JSONException if the character does not match. */ private char next(char c) throws JSONException { char n = this.next(); if (n != c) { if(n > 0) { throw this.syntaxError("Expected '" + c + "' and instead saw '" + n + "'"); } throw this.syntaxError("Expected '" + c + "' and instead saw ''"); } return n; } /** * Get the next n characters. * * @param n The number of characters to take. * @return A string of n characters. * @throws JSONException * Substring bounds error if there are not * n characters remaining in the source string. */ private String next(int n) throws JSONException { if (n == 0) { return ""; } char[] chars = new char[n]; int pos = 0; while (pos < n) { chars[pos] = this.next(); if (this.end()) { throw this.syntaxError("Substring bounds error"); } pos += 1; } return new String(chars); } /** * Get the next char in the string, skipping whitespace. * @throws JSONException Thrown if there is an error reading the source string. * @return A character, or 0 if there are no more characters. */ protected char nextClean() throws JSONException { for (;;) { char c = this.next(); if (c == 0 || c > ' ') { return c; } } } /** * Return the characters up to the next close quote character. * Backslash processing is done. The formal JSON format does not * allow strings in single quotes, but an implementation is allowed to * accept them. * @param quote The quoting character, either * <code>"</code> <small>(double quote)</small> or * <code>'</code> <small>(single quote)</small>. * @return A String. * @throws JSONException Unterminated string. */ private String nextString(char quote) throws JSONException { char c; StringBuilder sb = new StringBuilder(); for (;;) { c = this.next(); switch (c) { case 0: case '\n': case '\r': throw this.syntaxError("Unterminated string"); case '\\': c = this.next(); switch (c) { 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; case 'u': try { sb.append((char)Integer.parseInt(this.next(4), 16)); } catch (NumberFormatException e) { throw this.syntaxError("Illegal escape.", e); } break; case '"': case '\'': case '\\': case '/': sb.append(c); break; default: throw this.syntaxError("Illegal escape."); } break; default: if (c == quote) { return sb.toString(); } sb.append(c); } } } /** * Get the text up but not including the specified character or the * end of line, whichever comes first. * @param delimiter A delimiter character. * @return A string. * @throws JSONException Thrown if there is an error while searching * for the delimiter */ private String nextTo(char delimiter) throws JSONException { StringBuilder sb = new StringBuilder(); for (;;) { char c = this.next(); if (c == delimiter || c == 0 || c == '\n' || c == '\r') { if (c != 0) { this.back(); } return sb.toString().trim(); } sb.append(c); } } /** * Get the text up but not including one of the specified delimiter * characters or the end of line, whichever comes first. * @param delimiters A set of delimiter characters. * @return A string, trimmed. * @throws JSONException Thrown if there is an error while searching * for the delimiter */ private String nextTo(String delimiters) throws JSONException { char c; StringBuilder sb = new StringBuilder(); for (;;) { c = this.next(); if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') { if (c != 0) { this.back(); } return sb.toString().trim(); } sb.append(c); } } /** * Get the next value. The value can be a Boolean, Double, Integer, * JSONArray, JSONObject, Long, or String. * @throws JSONException If syntax error. */ protected Object nextValue() throws JSONException { char c = this.nextClean(); String string; switch (c) { case '"': case '\'': return this.nextString(c); case '{': this.back(); return new JSONObject(this); case '[': this.back(); return new JSONArray(this); } /* * Handle unquoted text. This could be the values true, false, or * null, or it can be a number. An implementation (such as this one) * is allowed to also accept non-standard forms. * * Accumulate characters until we reach the end of the text or a * formatting character. */ StringBuilder sb = new StringBuilder(); while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { sb.append(c); c = this.next(); } this.back(); string = sb.toString().trim(); if ("".equals(string)) { throw this.syntaxError("Missing value"); } return stringToValue(string); } /** * Skip characters until the next character is the requested character. * If the requested character is not found, no characters are skipped. * @param to A character to skip to. * @return The requested character, or zero if the requested character * is not found. * @throws JSONException Thrown if there is an error while searching * for the to character */ private char skipTo(char to) throws JSONException { char c; try { long startIndex = this.index; long startCharacter = this.character; long startLine = this.line; this.reader.mark(1000000); do { c = this.next(); if (c == 0) { // in some readers, reset() may throw an exception if // the remaining portion of the input is greater than // the mark size (1,000,000 above). this.reader.reset(); this.index = startIndex; this.character = startCharacter; this.line = startLine; return 0; } } while (c != to); this.reader.mark(1); } catch (IOException exception) { throw new JSONException(exception); } this.back(); return c; } /** * Make a JSONException to signal a syntax error. * * @param message The error message. * @return A JSONException object, suitable for throwing */ protected JSONException syntaxError(String message) { return new JSONException(message + this.toString()); } /** * Make a JSONException to signal a syntax error. * * @param message The error message. * @param causedBy The throwable that caused the error. * @return A JSONException object, suitable for throwing */ private JSONException syntaxError(String message, Throwable causedBy) { return new JSONException(message + this.toString(), causedBy); } /** * Make a printable string of this JSONTokener. * * @return " at {index} [character {character} line {line}]" */ @Override public String toString() { return " at " + this.index + " [character " + this.character + " line " + this.line + "]"; } //************************************************************************** //** stringToValue //************************************************************************** /** Try to convert a string into a number, boolean, or null. If the string * can't be converted, return the string. */ private static Object stringToValue(String string) { if (string.equals("")) { return string; } if (string.equalsIgnoreCase("true")) { return Boolean.TRUE; } if (string.equalsIgnoreCase("false")) { return Boolean.FALSE; } if (string.equalsIgnoreCase("null")) { return null; } /* * If it might be a number, try converting it. If a number cannot be * produced, then the value will just be a string. */ char initial = string.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { try { // if we want full Big Number support this block can be replaced with: // return stringToNumber(string); if (isDecimalNotation(string)) { Double d = Double.valueOf(string); if (!d.isInfinite() && !d.isNaN()) { return d; } } else { Long myLong = Long.valueOf(string); if (string.equals(myLong.toString())) { if (myLong.longValue() == myLong.intValue()) { return Integer.valueOf(myLong.intValue()); } return myLong; } } } catch (Exception ignore) { } } return string; } //************************************************************************** //** isDecimalNotation //************************************************************************** /** Tests if the value should be treated as a decimal. It makes no test if * there are actual digits. * * @return true if the string is "-0" or if it contains '.', 'e', or 'E', * false otherwise. */ private static boolean isDecimalNotation(final String val) { return val.indexOf('.') > -1 || val.indexOf('e') > -1 || val.indexOf('E') > -1 || "-0".equals(val); } } } |