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

com.republicate.json.Json Maven / Gradle / Ivy

There is a newer version: 2.7
Show newest version
package com.republicate.json;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 

JSON container interface.

*

The two inner classes JsonArray and JsonObject represent the two flavors of this container.

*/ public interface Json extends Serializable { /** * Logger */ Logger logger = LoggerFactory.getLogger("json"); /** * Indentation */ String INDENTATION = " "; /** * Parse a JSON string into a JSON container * @param content JSON content * @return parsed json */ static Json parse(String content) throws IOException { return new Parser(content).parse(); } /** * Parse a JSON stream into a JSON container * @param reader * @return parsed json */ static Json parse(Reader reader) throws IOException { return new Parser(reader).parse(); } /** * Creates a new JSON array * @return empty array */ static JsonArray newJsonArray() { return new JsonArray(); } /** * Creates a new JSON object * @return empty array */ static JsonObject newJsonObject() { return new JsonObject(); } /** * Parse a JSON stream into a JSON container or simple value * @param content JSON content * @return parsed json */ static Serializable parseValue(String content) throws IOException { return new Parser(content).parseValue(); } /** * Parse a JSON stream into a JSON container * @param reader * @return parsed json */ static Serializable parseValue(Reader reader) throws IOException { return new Parser(reader).parseValue(true); } /** * Check if the underlying container is a JSON array. * @return true if underlying container is an array, false otherwise */ boolean isJsonArray(); /** * Check if the underlying container is an object. * @return true if underlying container is an object, false otherwise */ boolean isJsonObject(); /** * Ensure that the underlying container is an array. * @throws IllegalStateException otherwise */ void ensureIsJsonArray(); /** * Ensure that the underlying container is an object. * @throws IllegalStateException otherwise */ void ensureIsJsonObject(); /** * Get self as a JsonArray * @throws IllegalStateException if container is not a JsonArray */ default JsonArray asJsonArray() { ensureIsJsonArray(); return (JsonArray) this; } /** * Get self as a JsonArray * @throws IllegalStateException if container is not a JsonObject */ default JsonObject asJsonObject() { ensureIsJsonObject(); return (JsonObject) this; } /** * Returns the number of elements in this collection. * @return the number of elements in this collection */ int size(); /** * Returns true if this collection contains no elements. * @return true if this collection contains no elements */ boolean isEmpty(); /** * Writes a representation of this container to the specified writer. * @param writer target writer * @return input writer */ Writer toString(Writer writer) throws IOException; /** * Writes a pretty representation of this container to the specified writer. * @param writer target writer * @return input writer */ Writer toPrettyString(Writer writer, String indent) throws IOException; /** * Gets a pretty representation of this container. * @return input writer */ default String toPrettyString() { try { return toPrettyString(new StringWriter(), "").toString(); } catch (IOException ioe) { logger.error("could not render Json container string", ioe); return null; } } /** * Implements a JSON array */ class JsonArray extends ArrayList implements Json { private static final long serialVersionUID = 1272604422260086506L; /** * Check if the underlying container is an array. * * @return true if underlying container is an array, false otherwise */ @Override public boolean isJsonArray() { return true; } /** * Check if the underlying container is an object. * * @return true if underlying container is an object, false otherwise */ @Override public boolean isJsonObject() { return false; } /** * Check that the underlying container is an array. * @throws IllegalStateException otherwise */ @Override public void ensureIsJsonArray() { } /** * Check that the underlying container is an object. * @throws IllegalStateException otherwise */ @Override public void ensureIsJsonObject() { throw new IllegalStateException("container must be a JSON object"); } /** * Writes a representation of this container to the specified writer. * @param writer target writer */ public Writer toString(Writer writer) throws IOException { writer.write('['); boolean first = true; for (Serializable value : this) { if (first) { first = false; } else { writer.write(','); } if (value instanceof Json) { ((Json)value).toString(writer); } else { Serializer.writeSerializable(value, writer); } } writer.write(']'); return writer; } /** * Writes a pretty representation of this container to the specified writer. * @param writer target writer * @return input writer */ @Override public Writer toPrettyString(Writer writer, String indent) throws IOException { writer.write(indent); String nextIndent = indent + INDENTATION; writer.write("[\n"); boolean first = true; for (Serializable value : this) { if (first) { first = false; } else { writer.write(",\n"); } if (value instanceof Json) { ((Json)value).toPrettyString(writer, nextIndent); } else { writer.write(nextIndent); Serializer.writeSerializable(value, writer); } } writer.write('\n'); writer.write(indent); writer.write(']'); return writer; } /** * Returns a string representation of this container * @return container string representation */ @Override public String toString() { try { return toString(new StringWriter()).toString(); } catch (IOException ioe) { logger.error("could not render JsonArray string", ioe); return null; } } } /** * Implements a JSON object */ class JsonObject extends LinkedHashMap implements Json { private static final long serialVersionUID = -8433114857911795160L; /** * Check if the underlying container is an array. * * @return true if underlying container is an array, false otherwise */ @Override public boolean isJsonArray() { return false; } /** * Check if the underlying container is an object. * * @return true if underlying container is an object, false otherwise */ @Override public boolean isJsonObject() { return true; } /** * Check that the underlying container is an array. * @throws IllegalStateException otherwise */ @Override public void ensureIsJsonArray() { throw new IllegalStateException("container must be a JSON array"); } /** * Check that the underlying container is an object. * @throws IllegalStateException otherwise */ @Override public void ensureIsJsonObject() { } /** * Writes a representation of this container to the specified writer. * @param writer target writer */ public Writer toString(Writer writer) throws IOException { writer.write('{'); boolean first = true; for (Map.Entry entry : entrySet()) { if (first) { first = false; } else { writer.write(','); } writer.write('"'); writer.write(entry.getKey()); writer.write("\":"); Serializable value = entry.getValue(); if (value instanceof Json) { ((Json)value).toString(writer); } else { Serializer.writeSerializable(value, writer); } } writer.write('}'); return writer; } /** * Writes a pretty representation of this container to the specified writer. * @param writer target writer * @return input writer */ @Override public Writer toPrettyString(Writer writer, String indent) throws IOException { writer.write("{\n"); String nextIndent = indent + INDENTATION; boolean first = true; for (Map.Entry entry : entrySet()) { if (first) { first = false; } else { writer.write(",\n"); } writer.write(nextIndent); writer.write('"'); writer.write(entry.getKey()); writer.write("\" : "); Serializable value = entry.getValue(); if (value instanceof Json) { ((Json)value).toPrettyString(writer, nextIndent); } else { writer.write(nextIndent); Serializer.writeSerializable(value, writer); } } writer.write('\n'); writer.write(indent); writer.write('}'); return writer; } /** * Returns a string representation of this container * @return container string representation */ @Override public String toString() { try { return toString(new StringWriter()).toString(); } catch (IOException ioe) { logger.error("could not render JsonArray string", ioe); return null; } } } class Serializer { private static final String[] ESCAPED_CHARS; static { ESCAPED_CHARS = new String[128]; for (int i = 0; i <= 0x1f; i++) { ESCAPED_CHARS[i] = String.format("\\u%04x", (int) i); } ESCAPED_CHARS['"'] = "\\\""; ESCAPED_CHARS['\\'] = "\\\\"; ESCAPED_CHARS['\t'] = "\\t"; ESCAPED_CHARS['\b'] = "\\b"; ESCAPED_CHARS['\n'] = "\\n"; ESCAPED_CHARS['\r'] = "\\r"; ESCAPED_CHARS['\f'] = "\\f"; } /** * Escape a string for Json special characters towards * the provided writer * @param str input string * @param writer target writer * @return input writer */ static public Writer escapeJson(String str, Writer writer) throws IOException { // use com.google.gson.stream.JsonWriter method to minimize write() calls // in case the output writer is not buffered int last = 0; int len = str.length(); for (int i = 0; i < len; ++i) { char c = str.charAt(i); String escaped; if (c < 128) { escaped = ESCAPED_CHARS[c]; if (escaped == null) { continue; } } else if (c == '\u2028') { escaped = "\\u2028"; } else if (c == '\u2029') { escaped = "\\u2029"; } else { continue; } if (last < i) { writer.write(str, last, i - last); } writer.write(escaped); last = i + 1; } if (last < len) { writer.write(str, last, len - last); } return writer; } /** * Write a serializable element to an output writer * @param serializable input element * @param writer output writer */ static protected void writeSerializable(Serializable serializable, Writer writer) throws IOException { if (serializable == null) { writer.write("null"); } else if (serializable instanceof Boolean) { writer.write(serializable.toString()); } else if(serializable instanceof Number) { String number = ((Number)serializable).toString(); if (number.equals("-Infinity") || number.equals("Infinity") || number.equals("NaN")) { throw new IOException("invalid number: " + number); } writer.write(serializable.toString()); } else { writer.write('\"'); escapeJson(serializable.toString(), writer); writer.write('\"'); } } } class Parser { private Reader reader; private int row = 1; private int col = 0; private int ch = 0; private boolean prefetch = false; private int prefetched = 0; private char buffer[] = new char[1024]; private int pos = 0; private Parser(String content) { this(new FastStringReader(content)); } private Parser(Reader reader) { if (1==1/*reader.markSupported()*/) { this.reader = reader; } else { this.reader = new BufferedReader(reader); } } private int next() throws IOException { if (prefetch) { ch = prefetched; prefetch = false; } else { ch = reader.read(); if (ch == '\n') { ++row; col = 0; } else { ++col; } } return ch; } private void back() throws IOException { if (prefetch) { throw error("internal error: cannot go back twice"); } prefetch = true; prefetched = ch; } private Json parse() throws IOException { Json ret = null; skipWhiteSpace(); switch (ch) { case -1: break; case '{': ret = parseObject(); break; case '[': ret = parseArray(); break; default: throw error("expecting '[' or '{', got: '" + display(ch) + "'"); } if (ret != null) { skipWhiteSpace(); if (ch != -1) { throw error("expecting end of stream"); } } return ret; } private void skipWhiteSpace() throws IOException { for(; Character.isWhitespace(next()); ); } private IOException error(String msg) { msg = "JSON parsing error at line " + row + ", column " + col + ": " + msg; logger.error(msg); return new IOException(msg); } private String display(int c) { if (c == -1) { return "end of stream"; } else if (Character.isISOControl(c)) { return "0x" + Integer.toHexString(c); } else { return String.valueOf((char)c); } } private JsonArray parseArray() throws IOException { JsonArray ret = new JsonArray(); skipWhiteSpace(); if (ch != ']') { back(); main: while (true) { ret.add(parseValue()); skipWhiteSpace(); switch (ch) { case ']': break main; case ',': break; default: throw error("expecting ',' or ']', got: '" + display(ch) + "'"); } } } return ret; } private JsonObject parseObject() throws IOException { JsonObject ret = new JsonObject(); skipWhiteSpace(); if (ch != '}') { main: while (true) { if (ch != '"') { throw error("expecting key string, got: '" + display(ch) + "'"); } String key = parseString(); skipWhiteSpace(); if (ch != ':') { throw error("expecting ':', got: '" + display(ch) + "'"); } Serializable value = parseValue(); Serializable previous = ret.put(key, value); if (previous != null) { logger.warn("key '{}' is not unique at line {}, column {}", key, row, col); } skipWhiteSpace(); switch (ch) { case '}': break main; case ',': break; default: throw error("expecting ',' or '}', got: '" + display(ch) + "'"); } skipWhiteSpace(); } } return ret; } private Serializable parseValue() throws IOException { return parseValue(false); } private Serializable parseValue(boolean complete) throws IOException { Serializable ret = null; skipWhiteSpace(); if (ch == -1) { throw error("unexpecting end of stream"); } switch (ch) { case '"': ret = parseString(); break; case '[': ret = parseArray(); break; case '{': ret = parseObject(); break; case 't': ret = parseKeyword("true", true); break; case 'f': ret = parseKeyword("false", false); break; case 'n': ret = parseKeyword("null", null); break; case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ret = parseNumber(); break; case -1: break; default: throw error("unexpected chararcter: '" + display(ch) + "'"); } if (complete) { skipWhiteSpace(); if (ch != -1) { throw error("expecting end of stream"); } } return ret; } private Serializable parseKeyword(String keyword, Serializable value) throws IOException { for (int i = 0; i < keyword.length(); ++i) { if (i > 0) { next(); } if (ch != keyword.charAt(i)) { if (ch == -1) { throw new IOException("encountered end of stream while parsing keyword '" + keyword + "'"); } else { throw new IOException("invalid character '" + display(ch) + "' while parsing keyword '" + keyword + "'"); } } } return value; } private String parseString() throws IOException { // borrow some optimization ideas from com.google.gson.stream.JsonReader pos = 0; StringBuilder builder = null; while (true) { while (pos < buffer.length) { buffer[pos++] = (char)next(); if (ch == '"') { if (builder == null) { return new String(buffer, 0, pos - 1); } else { builder.append(buffer, 0, pos - 1); return builder.toString(); } } else if (ch == '\\') { if (builder == null) { builder = new StringBuilder(Math.max(2 * pos, 16)); } builder.append(buffer, 0, pos - 1); pos = 0; char c = parseEscapeSequence(); builder.append(c); if (Character.isHighSurrogate(c)) { ch = next(); if (ch != '\\') { throw error("low surrogate escape sequence expected"); } c = parseEscapeSequence(); builder.append(c); if (!Character.isLowSurrogate(c)) { throw error("low surrogate escape sequence expected"); } } else if (Character.isLowSurrogate(c)) { throw error("lone low surrogate escape sequence unexpected"); } } else if (ch < 0x20) { throw error("unescaped control character"); } } if (builder == null) { builder = new StringBuilder(Math.max(2 * pos, 16)); } builder.append(buffer, 0, pos); pos = 0; if (next() == -1) { throw error("unterminated string"); } } } private char parseEscapeSequence() throws IOException { switch (next()) { case -1: throw error("unterminated escape sequence"); case 'u': char result = 0; for (int i = 0; i < 4; ++i) { if (next() == -1) { throw error("unterminated escape sequence"); } char c = (char)ch; result <<= 4; if (c >= '0' && c <= '9') { result += c - '0'; } else if (c >= 'a' && c <= 'f') { result += c - 'a' + 10; } else if (c >= 'A' && c <= 'F') { result += c - 'A' + 10; } else { throw error("malformed escape sequence"); } } return result; case 't': return '\t'; case 'b': return '\b'; case 'n': return '\n'; case 'f': return '\f'; case 'r': return '\r'; case '"': return '"'; case '\\': return '\\'; case '/': return '/'; default: throw error("unknown escape sequence"); } } private static long MIN_LONG_DECILE = Long.MIN_VALUE / 10; private Number parseNumber() throws IOException { // inspired from com.google.gson.stream.JsonReader, but much more readable // and handle Double/BigDecimal alternatives Number number; pos = 0; int digits = 0; boolean negative = false; boolean decimal = false; boolean fitsInLong = true; boolean fitsInDouble = true; long negValue = 0; // sign if (ch == '-') { negative = true; buffer[pos++] = (char)ch; if (next() == -1) { throw error("malformed number"); } } // mantissa digits += readDigits(false); // fractional part if (ch == '.') { decimal = true; buffer[pos++] = (char)ch; if (next() == -1) { throw error("malformed number"); } digits += readDigits(true); } else if (ch != 'e' && ch != 'E') { // check if number fits in long int i = negative ? 1 : 0; negValue = -(buffer[i++] - '0'); for (; i < pos; ++i) { long newNegValue = negValue * 10 - (buffer[i] - '0'); fitsInLong &= negValue > MIN_LONG_DECILE || (negValue == MIN_LONG_DECILE && newNegValue < negValue); if (!fitsInLong) { break; } negValue = newNegValue; } } if (digits > 15) { fitsInDouble = false; } // exponent if (ch == 'e' || ch == 'E') { decimal = true; buffer[pos++] = (char)ch; if (next() == -1) { throw error("malformed number"); } if (pos == buffer.length) { throw error("number is too long at my taste"); } if (ch == '+' || ch == '-') { buffer[pos++] = (char)ch; if (next() == -1) { throw error("malformed number"); } } int expPos = pos; int expDigits = readDigits(true); // or false ? if (fitsInDouble && expDigits >= 3 && (expDigits > 3 || buffer[expPos] > '3' || buffer[expPos + 1] > '0' || buffer[expPos + 2] > '7')) { fitsInDouble = false; } } if (!decimal && fitsInLong && (negative || negValue != Long.MIN_VALUE) && (!negative || negValue != 0)) { number = Long.valueOf(-negValue); } else { String strBuff = new String(buffer, 0, pos); if (!decimal) { number = new BigInteger(strBuff); } else if (fitsInDouble) { number = Double.valueOf(strBuff); } else { number = new BigDecimal(strBuff); } } // we always end up reading one more character back(); return number; } private int readDigits(boolean zeroFirstAllowed) throws IOException { int len = 0; while (pos < buffer.length) { if (!Character.isDigit(ch)) { break; } buffer[pos++] = (char)ch; ++len; next(); } if (pos == buffer.length) { throw error("number is too long at my taste"); } if (len == 0 || !zeroFirstAllowed && len > 1 && buffer[pos - len] == '0') { throw error("malformed number"); } return len; } } class FastStringReader extends Reader { String str; int len; int pos = 0; protected FastStringReader(String str) { this.str = str; len = str.length(); } @Override public int read(char[] cbuf, int off, int len) throws IOException { throw new NotImplementedException(); } @Override public void close() throws IOException { } @Override public int read() { return pos == len ? -1 : str.charAt(pos++); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy