key(String key) {
+ if (key == null)
+ throw new NullPointerException("key");
+ if (!(json.peek() instanceof JsonObject))
+ throw new JsonWriterException("Invalid call to emit a key value while not writing an object");
+ if (pendingKey != null)
+ throw new JsonWriterException("Invalid call to emit a key value immediately after emitting a key");
+ pendingKey = key;
+ return this;
+ }
+
private JsonObject obj() {
try {
return (JsonObject)json.peek();
diff --git a/src/main/java/com/grack/nanojson/JsonConvertible.java b/src/main/java/com/grack/nanojson/JsonConvertible.java
new file mode 100644
index 0000000..b3f3c1e
--- /dev/null
+++ b/src/main/java/com/grack/nanojson/JsonConvertible.java
@@ -0,0 +1,13 @@
+package com.grack.nanojson;
+
+/**
+ * An interface for classes that can be converted into valid JSON values.
+ */
+public interface JsonConvertible {
+ /**
+ * Creates a view of this object as a valid JSON Type.
+ *
+ * @return an instance of Map, Collection, String, Number or Boolean or {@code null}
+ */
+ Object toJsonValue();
+}
diff --git a/src/main/java/com/grack/nanojson/JsonLazyNumber.java b/src/main/java/com/grack/nanojson/JsonLazyNumber.java
index 080c6ef..01becd6 100644
--- a/src/main/java/com/grack/nanojson/JsonLazyNumber.java
+++ b/src/main/java/com/grack/nanojson/JsonLazyNumber.java
@@ -15,45 +15,52 @@
*/
package com.grack.nanojson;
-import java.math.BigDecimal;
+import ch.randelshofer.fastdoubleparser.JavaBigDecimalParser;
+import ch.randelshofer.fastdoubleparser.JavaDoubleParser;
+import ch.randelshofer.fastdoubleparser.JavaFloatParser;
/**
* Lazily-parsed number for performance.
*/
@SuppressWarnings("serial")
class JsonLazyNumber extends Number {
- private String value;
+ private char[] value;
private boolean isDouble;
- JsonLazyNumber(String number, boolean isDoubleValue) {
+ JsonLazyNumber(char[] number, boolean isDoubleValue) {
this.value = number;
this.isDouble = isDoubleValue;
}
@Override
public double doubleValue() {
- return Double.parseDouble(value);
+ return JavaDoubleParser.parseDouble(value);
}
@Override
public float floatValue() {
- return Float.parseFloat(value);
+ return JavaFloatParser.parseFloat(value);
}
@Override
public int intValue() {
- return isDouble ? (int)Double.parseDouble(value) : Integer.parseInt(value);
+ return isDouble ? (int)JavaDoubleParser.parseDouble(value) : Integer.parseInt(new String(value));
}
@Override
public long longValue() {
- return isDouble ? (long)Double.parseDouble(value) : Long.parseLong(value);
+ return isDouble ? (long) JavaDoubleParser.parseDouble(value) : Long.parseLong(new String(value));
+ }
+
+ @Override
+ public String toString() {
+ return new String(value);
}
/**
* Avoid serializing {@link JsonLazyNumber}.
*/
private Object writeReplace() {
- return new BigDecimal(value);
+ return JavaBigDecimalParser.parseBigDecimal(value);
}
}
diff --git a/src/main/java/com/grack/nanojson/JsonObject.java b/src/main/java/com/grack/nanojson/JsonObject.java
index 863728b..5703c30 100644
--- a/src/main/java/com/grack/nanojson/JsonObject.java
+++ b/src/main/java/com/grack/nanojson/JsonObject.java
@@ -206,6 +206,8 @@ public String getString(String key) {
*/
public String getString(String key, String default_) {
Object o = get(key);
+ if (o instanceof LazyString)
+ return o.toString();
if (o instanceof String)
return (String) o;
return default_;
@@ -243,6 +245,7 @@ public boolean isNumber(String key) {
* Returns true if the object has a string element at that key.
*/
public boolean isString(String key) {
- return get(key) instanceof String;
+ Object o = get(key);
+ return o instanceof LazyString || o instanceof String;
}
}
diff --git a/src/main/java/com/grack/nanojson/JsonParser.java b/src/main/java/com/grack/nanojson/JsonParser.java
index 3cbbfc0..60f04b7 100644
--- a/src/main/java/com/grack/nanojson/JsonParser.java
+++ b/src/main/java/com/grack/nanojson/JsonParser.java
@@ -15,16 +15,19 @@
*/
package com.grack.nanojson;
+import ch.randelshofer.fastdoubleparser.JavaBigIntegerParser;
+import ch.randelshofer.fastdoubleparser.JavaDoubleParser;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
-import java.math.BigInteger;
import java.net.URL;
+import java.util.Arrays;
/**
* Simple JSON parser.
- *
+ *
*
* Object json = {@link JsonParser}.any().from("{\"a\":[true,false], \"b\":1}");
* Number json = ({@link Number}){@link JsonParser}.any().from("123.456e7");
@@ -36,18 +39,21 @@ public final class JsonParser {
private Object value;
private int token;
- private JsonTokener tokener;
- private boolean lazyNumbers;
+ private final JsonTokener tokener;
+ private final boolean lazyNumbers;
+ private final boolean lazyStrings;
/**
- * Returns a type-safe parser context for a {@link JsonObject}, {@link JsonArray} or "any" type from which you can
+ * Returns a type-safe parser context for a {@link JsonObject},
+ * {@link JsonArray} or "any" type from which you can
* parse a {@link String} or a {@link Reader}.
*
* @param The parsed type.
*/
public static final class JsonParserContext {
private final Class clazz;
- private boolean lazyNumbers;
+ private boolean lazyNumbers = true;
+ private boolean lazyStrings = true;
JsonParserContext(Class clazz) {
this.clazz = clazz;
@@ -62,18 +68,27 @@ public JsonParserContext withLazyNumbers() {
return this;
}
+ /**
+ * Parses Strings lazily, allowing us to defer some of the cost of String
+ * construction until later.
+ */
+ public JsonParserContext withLazyStrings() {
+ lazyStrings = true;
+ return this;
+ }
+
/**
* Parses the current JSON type from a {@link String}.
*/
public T from(String s) throws JsonParserException {
- return new JsonParser(new JsonTokener(new StringReader(s)), lazyNumbers).parse(clazz);
+ return new JsonParser(new JsonTokener(new StringReader(s)), lazyNumbers, lazyStrings).parse(clazz);
}
/**
* Parses the current` JSON type from a {@link Reader}.
*/
public T from(Reader r) throws JsonParserException {
- return new JsonParser(new JsonTokener(r), lazyNumbers).parse(clazz);
+ return new JsonParser(new JsonTokener(r), lazyNumbers, lazyStrings).parse(clazz);
}
/**
@@ -81,11 +96,8 @@ public T from(Reader r) throws JsonParserException {
*/
public T from(URL url) throws JsonParserException {
try {
- InputStream stm = url.openStream();
- try {
+ try (InputStream stm = url.openStream()) {
return from(stm);
- } finally {
- stm.close();
}
} catch (IOException e) {
throw new JsonParserException(e, "IOException opening URL", 1, 1, 0);
@@ -93,21 +105,23 @@ public T from(URL url) throws JsonParserException {
}
/**
- * Parses the current JSON type from a {@link InputStream}. Detects the encoding from the input stream.
+ * Parses the current JSON type from a {@link InputStream}. Detects the encoding
+ * from the input stream.
*/
public T from(InputStream stm) throws JsonParserException {
- return new JsonParser(new JsonTokener(stm), lazyNumbers).parse(clazz);
+ return new JsonParser(new JsonTokener(stm), lazyNumbers, lazyStrings).parse(clazz);
}
}
- JsonParser(JsonTokener tokener, boolean lazyNumbers) throws JsonParserException {
+ JsonParser(JsonTokener tokener, boolean lazyNumbers, boolean lazyStrings) throws JsonParserException {
this.tokener = tokener;
this.lazyNumbers = lazyNumbers;
+ this.lazyStrings = lazyStrings;
}
/**
* Parses a {@link JsonObject} from a source.
- *
+ *
*
* JsonObject json = {@link JsonParser}.object().from("{\"a\":[true,false], \"b\":1}");
*
@@ -118,7 +132,7 @@ public static JsonParserContext object() {
/**
* Parses a {@link JsonArray} from a source.
- *
+ *
*
* JsonArray json = {@link JsonParser}.array().from("[1, {\"a\":[true,false], \"b\":1}]");
*
@@ -128,9 +142,11 @@ public static JsonParserContext array() {
}
/**
- * Parses any object from a source. For any valid JSON, returns either a null (for the JSON string 'null'), a
- * {@link String}, a {@link Number}, a {@link Boolean}, a {@link JsonObject} or a {@link JsonArray}.
- *
+ * Parses any object from a source. For any valid JSON, returns either a null
+ * (for the JSON string 'null'), a
+ * {@link String}, a {@link Number}, a {@link Boolean}, a {@link JsonObject} or
+ * a {@link JsonArray}.
+ *
*
* Object json = {@link JsonParser}.any().from("{\"a\":[true,false], \"b\":1}");
* Number json = ({@link Number}){@link JsonParser}.any().from("123.456e7");
@@ -144,138 +160,138 @@ public static JsonParserContext any() {
* Parse a single JSON value from the string, expecting an EOF at the end.
*/
T parse(Class clazz) throws JsonParserException {
- advanceToken(false, false);
- Object parsed = currentValue();
- if (advanceToken(false, false) != JsonTokener.TOKEN_EOF)
- throw tokener.createParseException(null, "Expected end of input, got " + token, true);
- if (clazz != Object.class && (parsed == null || !clazz.isAssignableFrom(parsed.getClass())))
- throw tokener.createParseException(null,
- "JSON did not contain the correct type, expected " + clazz.getSimpleName() + ".",
- true);
- return clazz.cast(parsed);
+ try {
+ advanceToken();
+ Object parsed = currentValue();
+ if (advanceToken() != JsonTokener.TOKEN_EOF)
+ throw tokener.createParseException(null, "Expected end of input, got " + token, true);
+ if (clazz != Object.class && (parsed == null || !clazz.isAssignableFrom(parsed.getClass())))
+ throw tokener.createParseException(null,
+ "JSON did not contain the correct type, expected " + clazz.getSimpleName() + ".",
+ true);
+ return clazz.cast(parsed);
+ } finally {
+ // Automatically close the tokener to release resources back to the pool
+ try {
+ tokener.close();
+ } catch (IOException e) {
+ // Log or ignore IOException during cleanup - don't mask the original exception
+ }
+ }
}
/**
* Starts parsing a JSON value at the current token position.
*/
private Object currentValue() throws JsonParserException {
- // Only a value start token should appear when we're in the context of parsing a JSON value
+ // Only a value start token should appear when we're in the context of parsing a
+ // JSON value
if (token >= JsonTokener.TOKEN_VALUE_MIN)
return value;
throw tokener.createParseException(null, "Expected JSON value, got " + token, true);
}
/**
- * Consumes a token, first eating up any whitespace ahead of it. Note that number tokens are not necessarily valid
+ * Consumes a token, first eating up any whitespace ahead of it. Note that
+ * number tokens are not necessarily valid
* numbers.
*/
- private int advanceToken(boolean allowSemiString, boolean old) throws JsonParserException {
- if (old) tokener.index--;
- token = tokener.advanceToToken(allowSemiString);
+ private int advanceToken() throws JsonParserException {
+ token = tokener.advanceToToken();
switch (token) {
- case JsonTokener.TOKEN_ARRAY_START: // Inlined function to avoid additional stack
- JsonArray list = new JsonArray();
- if (advanceToken(false, false) != JsonTokener.TOKEN_ARRAY_END)
- while (true) {
- list.add(currentValue());
- if (token == JsonTokener.TOKEN_SEMI_STRING)
- throw tokener.createParseException(null, "Semi-string is not allowed in array", true);
- if (advanceToken(false, false) == JsonTokener.TOKEN_ARRAY_END)
- break;
- if (token != JsonTokener.TOKEN_COMMA)
- throw tokener.createParseException(null,
- "Expected a comma or end of the array instead of " + token, true);
- if (advanceToken(false, false) == JsonTokener.TOKEN_ARRAY_END)
- throw tokener.createParseException(null, "Trailing comma found in array", true);
- }
- value = list;
- return token = JsonTokener.TOKEN_ARRAY_START;
- case JsonTokener.TOKEN_OBJECT_START: // Inlined function to avoid additional stack
- JsonObject map = new JsonObject();
- if (advanceToken(true, false) != JsonTokener.TOKEN_OBJECT_END)
- while (true) {
- switch (token) {
- case JsonTokener.TOKEN_NULL:
- case JsonTokener.TOKEN_TRUE:
- case JsonTokener.TOKEN_FALSE:
- value = value.toString();
- break;
- case JsonTokener.TOKEN_STRING:
- case JsonTokener.TOKEN_SEMI_STRING:
- break;
- default:
- throw tokener.createParseException(null, "Expected STRING, got " + token, true);
+ case JsonTokener.TOKEN_ARRAY_START: // Inlined function to avoid additional stack
+ JsonArray list = new JsonArray();
+ if (advanceToken() != JsonTokener.TOKEN_ARRAY_END)
+ while (true) {
+ list.add(currentValue());
+ if (advanceToken() == JsonTokener.TOKEN_ARRAY_END)
+ break;
+ if (token != JsonTokener.TOKEN_COMMA)
+ throw tokener.createParseException(null,
+ "Expected a comma or end of the array instead of " + token, true);
+ if (advanceToken() == JsonTokener.TOKEN_ARRAY_END)
+ throw tokener.createParseException(null, "Trailing comma found in array", true);
}
- String key = (String)value;
- if (token == JsonTokener.TOKEN_SEMI_STRING) {
- if (advanceToken(false, true) != JsonTokener.TOKEN_COLON)
+ value = list;
+ return token = JsonTokener.TOKEN_ARRAY_START;
+ case JsonTokener.TOKEN_OBJECT_START: // Inlined function to avoid additional stack
+ JsonObject map = new JsonObject();
+ if (advanceToken() != JsonTokener.TOKEN_OBJECT_END)
+ while (true) {
+ if (token != JsonTokener.TOKEN_STRING)
+ throw tokener.createParseException(null, "Expected STRING, got " + token, true);
+ String key = lazyStrings ? value.toString() : (String) value;
+ if (advanceToken() != JsonTokener.TOKEN_COLON)
throw tokener.createParseException(null, "Expected COLON, got " + token, true);
- } else if (advanceToken(false, false) != JsonTokener.TOKEN_COLON)
- throw tokener.createParseException(null, "Expected COLON, got " + token, true);
- advanceToken(false, false);
- map.put(key, currentValue());
- if (advanceToken(false, false) == JsonTokener.TOKEN_OBJECT_END)
- break;
- if (token != JsonTokener.TOKEN_COMMA)
- throw tokener.createParseException(null,
- "Expected a comma or end of the object instead of " + token, true);
- if (advanceToken(true, false) == JsonTokener.TOKEN_OBJECT_END)
- throw tokener.createParseException(null, "Trailing object found in array", true);
+ advanceToken();
+ map.put(key, currentValue());
+ if (advanceToken() == JsonTokener.TOKEN_OBJECT_END)
+ break;
+ if (token != JsonTokener.TOKEN_COMMA)
+ throw tokener.createParseException(null,
+ "Expected a comma or end of the object instead of " + token, true);
+ if (advanceToken() == JsonTokener.TOKEN_OBJECT_END)
+ throw tokener.createParseException(null, "Trailing object found in array", true);
+ }
+ value = map;
+ return token = JsonTokener.TOKEN_OBJECT_START;
+ case JsonTokener.TOKEN_TRUE:
+ value = Boolean.TRUE;
+ break;
+ case JsonTokener.TOKEN_FALSE:
+ value = Boolean.FALSE;
+ break;
+ case JsonTokener.TOKEN_NULL:
+ value = null;
+ break;
+ case JsonTokener.TOKEN_STRING:
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ value = lazyStrings ? new LazyString(chars) : new String(chars);
+ break;
+ case JsonTokener.TOKEN_NUMBER:
+ if (lazyNumbers) {
+ chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ value = new JsonLazyNumber(chars, tokener.isDouble);
+ } else {
+ value = parseNumber();
}
- value = map;
- return token = JsonTokener.TOKEN_OBJECT_START;
- case JsonTokener.TOKEN_TRUE:
- value = Boolean.TRUE;
- break;
- case JsonTokener.TOKEN_FALSE:
- value = Boolean.FALSE;
- break;
- case JsonTokener.TOKEN_NULL:
- value = null;
- break;
- case JsonTokener.TOKEN_STRING:
- case JsonTokener.TOKEN_SEMI_STRING:
- value = tokener.reusableBuffer.toString();
- break;
- case JsonTokener.TOKEN_NUMBER:
- if (lazyNumbers) {
- value = new JsonLazyNumber(tokener.reusableBuffer.toString(), tokener.isDouble);
- } else {
- value = parseNumber();
- }
- break;
- default:
+ break;
+ default:
}
return token;
}
private Number parseNumber() throws JsonParserException {
- String number = tokener.reusableBuffer.toString();
+ char[] number = tokener.reusableBuffer.array();
+ number = Arrays.copyOf(number, tokener.reusableBuffer.position());
+ int numLength = number.length;
try {
if (tokener.isDouble)
- return Double.parseDouble(number);
+ return JavaDoubleParser.parseDouble(number);
// Quick parse for single-digits
- if (number.length() == 1) {
- return number.charAt(0) - '0';
- } else if (number.length() == 2 && number.charAt(0) == '-') {
- return '0' - number.charAt(1);
+ if (numLength == 1) {
+ return number[0] - '0';
+ } else if (numLength == 2 && number[0] == '-') {
+ return '0' - number[1];
}
// HACK: Attempt to parse using the approximate best type for this
- boolean firstMinus = number.charAt(0) == '-';
- int length = firstMinus ? number.length() - 1 : number.length();
+ boolean firstMinus = number[0] == '-';
+ int length = firstMinus ? numLength - 1 : numLength;
// CHECKSTYLE_OFF: MagicNumber
- if (length < 10 || (length == 10 && number.charAt(firstMinus ? 1 : 0) < '2')) // 2 147 483 647
- return Integer.parseInt(number);
- if (length < 19 || (length == 19 && number.charAt(firstMinus ? 1 : 0) < '9')) // 9 223 372 036 854 775 807
- return Long.parseLong(number);
+ if (length < 10 || (length == 10 && number[firstMinus ? 1 : 0] < '2')) // 2 147 483 647
+ return Integer.parseInt(new String(number));
+ if (length < 19 || (length == 19 && number[firstMinus ? 1 : 0] < '9')) // 9 223 372 036 854 775 807
+ return Long.parseLong(new String(number));
// CHECKSTYLE_ON: MagicNumber
- return new BigInteger(number);
+ return JavaBigIntegerParser.parseBigInteger(number);
} catch (NumberFormatException e) {
- throw tokener.createParseException(e, "Malformed number: " + number, true);
+ throw tokener.createParseException(e, "Malformed number: " + new String(number), true);
}
}
}
diff --git a/src/main/java/com/grack/nanojson/JsonReader.java b/src/main/java/com/grack/nanojson/JsonReader.java
index b9ea2cb..eddae48 100644
--- a/src/main/java/com/grack/nanojson/JsonReader.java
+++ b/src/main/java/com/grack/nanojson/JsonReader.java
@@ -15,22 +15,31 @@
*/
package com.grack.nanojson;
+import ch.randelshofer.fastdoubleparser.JavaDoubleParser;
+import ch.randelshofer.fastdoubleparser.JavaFloatParser;
+
+import java.io.Closeable;
+import java.io.IOException;
import java.io.InputStream;
+import java.io.Reader;
import java.io.StringReader;
+import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.BitSet;
/**
* Streaming reader for JSON documents.
*/
-public final class JsonReader {
+public final class JsonReader implements Closeable {
private JsonTokener tokener;
private int token;
private BitSet states = new BitSet();
private int stateIndex = 0;
private boolean inObject;
private boolean first = true;
- private StringBuilder key = new StringBuilder();
+ // CHECKSTYLE_OFF: MagicNumber
+ private CharBuffer key = CharBufferPool.get(1024);
+ // CHECKSTYLE_ON: MagicNumber
/**
* The type of value that the {@link JsonReader} is positioned over.
@@ -76,12 +85,19 @@ public static JsonReader from(String s) throws JsonParserException {
return new JsonReader(new JsonTokener(new StringReader(s)));
}
+ /**
+ * Create a {@link JsonReader} from a {@link Reader}.
+ */
+ public static JsonReader from(Reader reader) throws JsonParserException {
+ return new JsonReader(new JsonTokener(reader));
+ }
+
/**
* Internal constructor.
*/
JsonReader(JsonTokener tokener) throws JsonParserException {
this.tokener = tokener;
- token = tokener.advanceToToken(false);
+ token = tokener.advanceToToken();
}
/**
@@ -90,7 +106,8 @@ public static JsonReader from(String s) throws JsonParserException {
*/
public boolean pop() throws JsonParserException {
// CHECKSTYLE_OFF: EmptyStatement
- while (!next());
+ while (!next())
+ ;
// CHECKSTYLE_ON: EmptyStatement
first = false;
inObject = states.get(--stateIndex);
@@ -102,23 +119,23 @@ public boolean pop() throws JsonParserException {
*/
public Type current() throws JsonParserException {
switch (token) {
- case JsonTokener.TOKEN_TRUE:
- case JsonTokener.TOKEN_FALSE:
- return Type.BOOLEAN;
- case JsonTokener.TOKEN_NULL:
- return Type.NULL;
- case JsonTokener.TOKEN_NUMBER:
- return Type.NUMBER;
- case JsonTokener.TOKEN_STRING:
- return Type.STRING;
- case JsonTokener.TOKEN_OBJECT_START:
- return Type.OBJECT;
- case JsonTokener.TOKEN_ARRAY_START:
- return Type.ARRAY;
- default:
- throw createTokenMismatchException(JsonTokener.TOKEN_NULL, JsonTokener.TOKEN_TRUE,
- JsonTokener.TOKEN_FALSE, JsonTokener.TOKEN_NUMBER, JsonTokener.TOKEN_STRING,
- JsonTokener.TOKEN_OBJECT_START, JsonTokener.TOKEN_ARRAY_START);
+ case JsonTokener.TOKEN_TRUE:
+ case JsonTokener.TOKEN_FALSE:
+ return Type.BOOLEAN;
+ case JsonTokener.TOKEN_NULL:
+ return Type.NULL;
+ case JsonTokener.TOKEN_NUMBER:
+ return Type.NUMBER;
+ case JsonTokener.TOKEN_STRING:
+ return Type.STRING;
+ case JsonTokener.TOKEN_OBJECT_START:
+ return Type.OBJECT;
+ case JsonTokener.TOKEN_ARRAY_START:
+ return Type.ARRAY;
+ default:
+ throw createTokenMismatchException(JsonTokener.TOKEN_NULL, JsonTokener.TOKEN_TRUE,
+ JsonTokener.TOKEN_FALSE, JsonTokener.TOKEN_NUMBER, JsonTokener.TOKEN_STRING,
+ JsonTokener.TOKEN_OBJECT_START, JsonTokener.TOKEN_ARRAY_START);
}
}
@@ -134,12 +151,15 @@ public void object() throws JsonParserException {
}
/**
- * Reads the key for the object at the current value. Does not advance to the next value.
+ * Reads the key for the object at the current value. Does not advance to the
+ * next value.
*/
public String key() throws JsonParserException {
if (!inObject)
throw tokener.createParseException(null, "Not reading an object", true);
- return key.toString();
+ char[] chars = key.array();
+ chars = Arrays.copyOf(chars, key.position());
+ return new String(chars);
}
/**
@@ -158,19 +178,20 @@ public void array() throws JsonParserException {
*/
public Object value() throws JsonParserException {
switch (token) {
- case JsonTokener.TOKEN_TRUE:
- return true;
- case JsonTokener.TOKEN_FALSE:
- return false;
- case JsonTokener.TOKEN_NULL:
- return null;
- case JsonTokener.TOKEN_NUMBER:
- return number();
- case JsonTokener.TOKEN_STRING:
- return string();
- default:
- throw createTokenMismatchException(JsonTokener.TOKEN_NULL, JsonTokener.TOKEN_TRUE, JsonTokener.TOKEN_FALSE,
- JsonTokener.TOKEN_NUMBER, JsonTokener.TOKEN_STRING);
+ case JsonTokener.TOKEN_TRUE:
+ return true;
+ case JsonTokener.TOKEN_FALSE:
+ return false;
+ case JsonTokener.TOKEN_NULL:
+ return null;
+ case JsonTokener.TOKEN_NUMBER:
+ return number();
+ case JsonTokener.TOKEN_STRING:
+ return string();
+ default:
+ throw createTokenMismatchException(JsonTokener.TOKEN_NULL, JsonTokener.TOKEN_TRUE,
+ JsonTokener.TOKEN_FALSE,
+ JsonTokener.TOKEN_NUMBER, JsonTokener.TOKEN_STRING);
}
}
@@ -190,7 +211,9 @@ public String string() throws JsonParserException {
return null;
if (token != JsonTokener.TOKEN_STRING)
throw createTokenMismatchException(JsonTokener.TOKEN_NULL, JsonTokener.TOKEN_STRING);
- return tokener.reusableBuffer.toString();
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ return new String(chars);
}
/**
@@ -211,55 +234,61 @@ else if (token == JsonTokener.TOKEN_FALSE)
public Number number() throws JsonParserException {
if (token == JsonTokener.TOKEN_NULL)
return null;
- return new JsonLazyNumber(tokener.reusableBuffer.toString(), tokener.isDouble);
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ return new JsonLazyNumber(chars, tokener.isDouble);
}
/**
* Parses the current value as a long.
*/
public long longVal() throws JsonParserException {
- String s = tokener.reusableBuffer.toString();
- return tokener.isDouble ? (long)Double.parseDouble(s) : Long.parseLong(s);
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ return tokener.isDouble ? (long) JavaDoubleParser.parseDouble(chars) : Long.parseLong(new String(chars));
}
/**
* Parses the current value as an integer.
*/
public int intVal() throws JsonParserException {
- String s = tokener.reusableBuffer.toString();
- return tokener.isDouble ? (int)Double.parseDouble(s) : Integer.parseInt(s);
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ return tokener.isDouble ? (int) JavaDoubleParser.parseDouble(chars) : Integer.parseInt(new String(chars));
}
/**
* Parses the current value as a float.
*/
public float floatVal() throws JsonParserException {
- String s = tokener.reusableBuffer.toString();
- return Float.parseFloat(s);
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ return JavaFloatParser.parseFloat(chars);
}
/**
* Parses the current value as a double.
*/
public double doubleVal() throws JsonParserException {
- String s = tokener.reusableBuffer.toString();
- return Double.parseDouble(s);
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ return JavaDoubleParser.parseDouble(chars);
}
/**
* Advance to the next value in this array or object. If no values remain,
* return to the parent array or object.
- *
+ *
* @return true if we still have values to read in this array or object,
* false if we have completed this object (and implicitly moved back
* to the parent array or object)
*/
public boolean next() throws JsonParserException {
if (stateIndex == 0) {
- throw tokener.createParseException(null, "Unabled to call next() at the root", true);
+ throw tokener.createParseException(null, "Unabled to call next() at the root", true);
}
-
- token = tokener.advanceToToken(false);
+
+ token = tokener.advanceToToken();
if (inObject) {
if (token == JsonTokener.TOKEN_OBJECT_END) {
@@ -267,20 +296,22 @@ public boolean next() throws JsonParserException {
first = false;
return false;
}
-
+
if (!first) {
if (token != JsonTokener.TOKEN_COMMA)
throw createTokenMismatchException(JsonTokener.TOKEN_COMMA, JsonTokener.TOKEN_OBJECT_END);
- token = tokener.advanceToToken(false);
+ token = tokener.advanceToToken();
}
if (token != JsonTokener.TOKEN_STRING)
throw createTokenMismatchException(JsonTokener.TOKEN_STRING);
- key.setLength(0);
- key.append(tokener.reusableBuffer); // reduce string garbage
- if ((token = tokener.advanceToToken(false)) != JsonTokener.TOKEN_COLON)
+ key.clear();
+ char[] chars = tokener.reusableBuffer.array();
+ chars = Arrays.copyOf(chars, tokener.reusableBuffer.position());
+ key.put(chars);
+ if ((token = tokener.advanceToToken()) != JsonTokener.TOKEN_COLON)
throw createTokenMismatchException(JsonTokener.TOKEN_COLON);
- token = tokener.advanceToToken(false);
+ token = tokener.advanceToToken();
} else {
if (token == JsonTokener.TOKEN_ARRAY_END) {
inObject = states.get(--stateIndex);
@@ -290,7 +321,7 @@ public boolean next() throws JsonParserException {
if (!first) {
if (token != JsonTokener.TOKEN_COMMA)
throw createTokenMismatchException(JsonTokener.TOKEN_COMMA, JsonTokener.TOKEN_ARRAY_END);
- token = tokener.advanceToToken(false);
+ token = tokener.advanceToToken();
}
}
@@ -303,13 +334,28 @@ public boolean next() throws JsonParserException {
JsonTokener.TOKEN_OBJECT_START, JsonTokener.TOKEN_ARRAY_START);
first = false;
-
+
return true;
}
-
+
+ /**
+ * Releases resources used by this JsonReader. Should be called when done
+ * reading.
+ */
+ @Override
+ public void close() throws IOException {
+ if (key != null) {
+ CharBufferPool.release(key);
+ key = null;
+ }
+ if (tokener != null) {
+ tokener.close();
+ }
+ }
+
private JsonParserException createTokenMismatchException(int... t) {
return tokener.createParseException(null, "token mismatch (expected " + Arrays.toString(t)
- + ", was " + token + ")",
+ + ", was " + token + ")",
true);
}
}
diff --git a/src/main/java/com/grack/nanojson/JsonSink.java b/src/main/java/com/grack/nanojson/JsonSink.java
index 73d8f02..4f30b6a 100644
--- a/src/main/java/com/grack/nanojson/JsonSink.java
+++ b/src/main/java/com/grack/nanojson/JsonSink.java
@@ -159,4 +159,9 @@ public interface JsonSink> {
* Ends the current array or object.
*/
SELF end();
+
+ /**
+ * Writes the key of a key/value pair.
+ */
+ SELF key(String key);
}
diff --git a/src/main/java/com/grack/nanojson/JsonTokener.java b/src/main/java/com/grack/nanojson/JsonTokener.java
index d7a45a2..3fa5807 100644
--- a/src/main/java/com/grack/nanojson/JsonTokener.java
+++ b/src/main/java/com/grack/nanojson/JsonTokener.java
@@ -17,20 +17,25 @@
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
+import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
- * Internal class for tokenizing JSON. Used by both {@link JsonParser} and {@link JsonReader}.
+ * Internal class for tokenizing JSON. Used by both {@link JsonParser} and
+ * {@link JsonReader}.
*/
-final class JsonTokener {
+final class JsonTokener implements Closeable {
// Used by tests
static final int BUFFER_SIZE = 32 * 1024;
+ static final int MAX_CHAR_BUFFER_SIZE = 512;
+
static final int BUFFER_ROOM = 256;
static final int MAX_ESCAPE = 5; // uXXXX (don't need the leading slash)
@@ -38,14 +43,14 @@ final class JsonTokener {
private int tokenCharPos, tokenCharOffset;
private boolean eof;
- protected int index;
+ private int index;
private final Reader reader;
private final char[] buffer = new char[BUFFER_SIZE];
private int bufferLength;
private final boolean utf8;
- protected StringBuilder reusableBuffer = new StringBuilder();
+ protected CharBuffer reusableBuffer = CharBufferPool.get(MAX_CHAR_BUFFER_SIZE);
protected boolean isDouble;
static final char[] TRUE = { 'r', 'u', 'e' };
@@ -64,11 +69,11 @@ final class JsonTokener {
static final int TOKEN_NUMBER = 9;
static final int TOKEN_OBJECT_START = 10;
static final int TOKEN_ARRAY_START = 11;
- static final int TOKEN_SEMI_STRING = 12;
static final int TOKEN_VALUE_MIN = TOKEN_NULL;
/**
- * A {@link Reader} that reads a UTF8 stream without decoding it for performance.
+ * A {@link Reader} that reads a UTF8 stream without decoding it for
+ * performance.
*/
private static final class PseudoUtf8Reader extends Reader {
private final InputStream buffered;
@@ -82,7 +87,7 @@ private static final class PseudoUtf8Reader extends Reader {
public int read(char[] cbuf, int off, int len) throws IOException {
int r = buffered.read(buf, off, len);
for (int i = off; i < off + r; i++)
- cbuf[i] = (char)buf[i];
+ cbuf[i] = (char) buf[i];
return r;
}
@@ -90,15 +95,15 @@ public int read(char[] cbuf, int off, int len) throws IOException {
public void close() throws IOException {
}
}
-
+
JsonTokener(Reader reader) throws JsonParserException {
this.reader = reader;
this.utf8 = false;
init();
}
-
+
JsonTokener(InputStream stm) throws JsonParserException {
- final InputStream buffered = (stm instanceof BufferedInputStream || stm instanceof ByteArrayInputStream)
+ final InputStream buffered = (stm instanceof BufferedInputStream || stm instanceof ByteArrayInputStream)
? stm
: new BufferedInputStream(stm);
buffered.mark(4);
@@ -176,30 +181,17 @@ void consumeKeyword(char first, char[] expected) throws JsonParserException {
fixupAfterRawBufferRead();
- // The token shouldn't end with something other than an ASCII letter
- switch (peekChar()) {
- case ',':
- case ':':
- case '{':
- case '}':
- case '[':
- case ']':
- case ' ':
- case '\n':
- case '\r':
- case '\t':
- break;
- default:
+ // The token should end with something other than an ASCII letter
+ if (isAsciiLetter(peekChar()))
throw createHelpfulException(first, expected, expected.length);
- }
}
/**
* Steps through to the end of the current number token (a non-digit token).
*/
void consumeTokenNumber(char savedChar) throws JsonParserException {
- reusableBuffer.setLength(0);
- reusableBuffer.append(savedChar);
+ reusableBuffer.clear();
+ reusableBuffer.put(savedChar);
isDouble = false;
// The JSON spec is way stricter about number formats than
@@ -213,9 +205,8 @@ void consumeTokenNumber(char savedChar) throws JsonParserException {
} else {
state = 2;
}
-
- outer:
- while (true) {
+
+ outer: while (true) {
int n = ensureBuffer(BUFFER_ROOM);
if (n == 0)
break outer;
@@ -226,241 +217,129 @@ void consumeTokenNumber(char savedChar) throws JsonParserException {
break outer;
int ns = -1;
- sw:
- switch (state) {
- case 1: // start leading negative
- if (nc == '0') {
- ns = 3; break sw;
- }
- if (nc > '0' && nc <= '9') {
- ns = 2; break sw;
- }
- break;
- case 2: // no leading zero
- case 3: // leading zero
- if ((nc >= '0' && nc <= '9') && state == 2) {
- ns = 2; break sw;
- }
- if (nc == '.') {
- isDouble = true;
- ns = 4; break sw;
- }
- if (nc == 'e' || nc == 'E') {
- isDouble = true;
- ns = 6; break sw;
- }
- break;
- case 4: // after period
- case 5: // after period, one digit read
- if (nc >= '0' && nc <= '9') {
- ns = 5; break sw;
- }
- if ((nc == 'e' || nc == 'E') && state == 5) {
- isDouble = true;
- ns = 6; break sw;
- }
- break;
- case 6: // after exponent
- case 7: // after exponent and sign
- if (nc == '+' || nc == '-' && state == 6) {
- ns = 7; break sw;
- }
- if (nc >= '0' && nc <= '9') {
- ns = 8; break sw;
- }
- break;
- case 8: // after digits
- if (nc >= '0' && nc <= '9') {
- ns = 8; break sw;
- }
- break;
- default:
- assert false : "Impossible"; // will throw malformed number
+ sw: switch (state) {
+ case 1: // start leading negative
+ if (nc == '0') {
+ ns = 3;
+ break sw;
+ }
+ if (nc > '0' && nc <= '9') {
+ ns = 2;
+ break sw;
+ }
+ break;
+ case 2: // no leading zero
+ case 3: // leading zero
+ if ((nc >= '0' && nc <= '9') && state == 2) {
+ ns = 2;
+ break sw;
+ }
+ if (nc == '.') {
+ isDouble = true;
+ ns = 4;
+ break sw;
+ }
+ if (nc == 'e' || nc == 'E') {
+ isDouble = true;
+ ns = 6;
+ break sw;
+ }
+ break;
+ case 4: // after period
+ case 5: // after period, one digit read
+ if (nc >= '0' && nc <= '9') {
+ ns = 5;
+ break sw;
+ }
+ if ((nc == 'e' || nc == 'E') && state == 5) {
+ isDouble = true;
+ ns = 6;
+ break sw;
+ }
+ break;
+ case 6: // after exponent
+ case 7: // after exponent and sign
+ if (nc == '+' || nc == '-' && state == 6) {
+ ns = 7;
+ break sw;
+ }
+ if (nc >= '0' && nc <= '9') {
+ ns = 8;
+ break sw;
+ }
+ break;
+ case 8: // after digits
+ if (nc >= '0' && nc <= '9') {
+ ns = 8;
+ break sw;
+ }
+ break;
+ default:
+ assert false : "Impossible"; // will throw malformed number
}
- reusableBuffer.append(nc);
+ reusableBuffer.put(nc);
index++;
if (ns == -1)
throw createParseException(null, "Malformed number: " + reusableBuffer, true);
state = ns;
}
}
-
+
if (state != 2 && state != 3 && state != 5 && state != 8)
throw createParseException(null, "Malformed number: " + reusableBuffer, true);
-
+
// Special case for -0
if (state == 3 && savedChar == '-')
isDouble = true;
-
+
fixupAfterRawBufferRead();
}
/**
- * Steps through to the end of the current string token (the unescaped double quote).
+ * Steps through to the end of the current string token (the unescaped double
+ * quote).
*/
- void consumeTokenString(int cc) throws JsonParserException {
- reusableBuffer.setLength(0);
-
- // Assume no escapes or UTF-8 in the string to start (fast path)
- start:
- while (true) {
- int n = ensureBuffer(BUFFER_ROOM);
- if (n == 0)
- throw createParseException(null, "String was not terminated before end of input", true);
-
- for (int i = 0; i < n; i++) {
- char c = stringChar();
- if (c == cc) {
- // Use the index before we fixup
- reusableBuffer.append(buffer, index - i - 1, i);
- fixupAfterRawBufferRead();
- return;
- }
- if (c == '\\' || (utf8 && (c & 0x80) != 0)) {
- reusableBuffer.append(buffer, index - i - 1, i);
- index--;
- break start;
- }
- }
-
- reusableBuffer.append(buffer, index - n, n);
- }
-
- outer:
- while (true) {
- int n = ensureBuffer(BUFFER_ROOM);
- if (n == 0)
- throw createParseException(null, "String was not terminated before end of input", true);
-
- int end = index + n;
- while (index < end) {
- char c = stringChar();
-
- if (utf8 && (c & 0x80) != 0) {
- // If it's a UTF-8 codepoint, we know it won't have special meaning
- consumeTokenStringUtf8Char(c);
- continue outer;
- }
-
- switch (c) {
- case '"':
- case '\'':
- if (c == cc) {
- fixupAfterRawBufferRead();
- return;
- } else {
- reusableBuffer.append(c);
- break;
- }
- case '\\':
- // Ensure that we have at least MAX_ESCAPE here in the buffer
- if (end - index < MAX_ESCAPE) {
- // Re-adjust the buffer end, unlikely path
- n = ensureBuffer(MAX_ESCAPE);
- end = index + n;
- // Make sure that there's enough chars for a \\uXXXX escape
- if (buffer[index] == 'u' && n < MAX_ESCAPE) {
- index = bufferLength; // Reset index to last valid location
- throw createParseException(null, "EOF encountered in the middle of a string escape", false);
- }
- }
- char escape = buffer[index++];
- switch (escape) {
- case 'b':
- reusableBuffer.append('\b');
- break;
- case 'f':
- reusableBuffer.append('\f');
- break;
- case 'n':
- reusableBuffer.append('\n');
- break;
- case 'r':
- reusableBuffer.append('\r');
- break;
- case 't':
- reusableBuffer.append('\t');
- break;
- case '"':
- case '\'':
- case '/':
- case '\\':
- reusableBuffer.append(escape);
- break;
- case 'u':
- int escaped = 0;
-
- for (int j = 0; j < 4; j++) {
- escaped <<= 4;
- int digit = buffer[index++];
- if (digit >= '0' && digit <= '9') {
- escaped |= (digit - '0');
- } else if (digit >= 'A' && digit <= 'F') {
- escaped |= (digit - 'A') + 10;
- } else if (digit >= 'a' && digit <= 'f') {
- escaped |= (digit - 'a') + 10;
- } else {
- throw createParseException(null, "Expected unicode hex escape character: "
- + (char)digit + " (" + digit + ")", false);
- }
- }
-
- reusableBuffer.append((char)escaped);
- break;
- default:
- throw createParseException(null, "Invalid escape: \\" + escape, false);
- }
- break;
- default:
- reusableBuffer.append(c);
- }
- }
-
- if (index > bufferLength) {
- index = bufferLength; // Reset index to last valid location
- throw createParseException(null, "EOF encountered in the middle of a string escape", false);
- }
- }
- }
+ void consumeTokenString() throws JsonParserException {
+ reusableBuffer.position(0);
- void consumeTokenSemiString() throws JsonParserException {
- reusableBuffer.setLength(0);
-
- start:
- while (true) {
+ // Assume no escapes or UTF-8 in the string to start (fast path)
+ start: while (true) {
int n = ensureBuffer(BUFFER_ROOM);
if (n == 0)
throw createParseException(null, "String was not terminated before end of input", true);
for (int i = 0; i < n; i++) {
char c = stringChar();
- if (isWhitespace(c) || c == ':') {
+ if (c == '"') {
// Use the index before we fixup
- reusableBuffer.append(buffer, index - i - 1, i);
+ expandBufferIfNeeded(i);
+ reusableBuffer.put(buffer, index - i - 1, i);
fixupAfterRawBufferRead();
return;
}
if (c == '\\' || (utf8 && (c & 0x80) != 0)) {
- reusableBuffer.append(buffer, index - i - 1, i);
+ expandBufferIfNeeded(i);
+ reusableBuffer.put(buffer, index - i - 1, i);
index--;
break start;
}
- if (c == '[' || c == ']' || c == '{' || c == '}' || c == ',') {
- throw createParseException(null, "Invalid character in semi-string: " + c, false);
- }
}
- reusableBuffer.append(buffer, index - n, n);
+ expandBufferIfNeeded(n);
+ reusableBuffer.put(buffer, index - n, n);
}
- outer:
- while (true) {
+ outer: while (true) {
int n = ensureBuffer(BUFFER_ROOM);
if (n == 0)
throw createParseException(null, "String was not terminated before end of input", true);
int end = index + n;
while (index < end) {
+ // Ensure at least 1 char of space for upcoming output (common case). Escapes
+ // and
+ // UTF-8 multi-byte sequences will further ensure space as needed.
+ expandBufferIfNeeded(1);
char c = stringChar();
if (utf8 && (c & 0x80) != 0) {
@@ -470,52 +349,44 @@ void consumeTokenSemiString() throws JsonParserException {
}
switch (c) {
- case ' ':
- case '\n':
- case '\r':
- case '\t':
- case ':':
- fixupAfterRawBufferRead();
- return;
- case '[':
- case ']':
- case '{':
- case '}':
- case ',':
- throw createParseException(null, "Invalid character in semi-string: " + c, false);
- case '\\':
- // Ensure that we have at least MAX_ESCAPE here in the buffer
- if (end - index < MAX_ESCAPE) {
- // Re-adjust the buffer end, unlikely path
- n = ensureBuffer(MAX_ESCAPE);
- end = index + n;
- // Make sure that there's enough chars for a \\uXXXX escape
- if (buffer[index] == 'u' && n < MAX_ESCAPE) {
- index = bufferLength; // Reset index to last valid location
- throw createParseException(null, "EOF encountered in the middle of a string escape", false);
+ case '\"':
+ fixupAfterRawBufferRead();
+ return;
+ case '\\':
+ // Ensure that we have at least MAX_ESCAPE here in the buffer
+ if (end - index < MAX_ESCAPE) {
+ // Re-adjust the buffer end, unlikely path
+ n = ensureBuffer(MAX_ESCAPE);
+ end = index + n;
+ // Make sure that there's enough chars for a \\uXXXX escape
+ if (buffer[index] == 'u' && n < MAX_ESCAPE) {
+ index = bufferLength; // Reset index to last valid location
+ throw createParseException(null,
+ "EOF encountered in the middle of a string escape",
+ false);
+ }
}
- }
- char escape = buffer[index++];
- switch (escape) {
- case 'b':
- reusableBuffer.append('\b');
- break;
- case 'f':
- reusableBuffer.append('\f');
- break;
- case 'n':
- reusableBuffer.append('\n');
- break;
- case 'r':
- reusableBuffer.append('\r');
- break;
- case 't':
- reusableBuffer.append('\t');
+ char escape = buffer[index++];
+ switch (escape) {
+ case 'b':
+ reusableBuffer.put('\b');
+ break;
+ case 'f':
+ reusableBuffer.put('\f');
+ break;
+ case 'n':
+ reusableBuffer.put('\n');
+ break;
+ case 'r':
+ reusableBuffer.put('\r');
+ break;
+ case 't':
+ reusableBuffer.put('\t');
break;
case '"':
case '/':
case '\\':
- reusableBuffer.append(escape);
+ reusableBuffer.put(escape);
break;
case 'u':
int escaped = 0;
@@ -530,25 +401,28 @@ void consumeTokenSemiString() throws JsonParserException {
} else if (digit >= 'a' && digit <= 'f') {
escaped |= (digit - 'a') + 10;
} else {
- throw createParseException(null, "Expected unicode hex escape character: "
- + (char)digit + " (" + digit + ")", false);
+ throw createParseException(null,
+ "Expected unicode hex escape character: "
+ + (char) digit + " (" + digit + ")", false);
}
}
- reusableBuffer.append((char)escaped);
+ reusableBuffer.put((char) escaped);
break;
default:
throw createParseException(null, "Invalid escape: \\" + escape, false);
}
break;
default:
- reusableBuffer.append(c);
+ reusableBuffer.put(c);
}
}
if (index > bufferLength) {
index = bufferLength; // Reset index to last valid location
- throw createParseException(null, "EOF encountered in the middle of a string escape", false);
+ throw createParseException(null,
+ "EOF encountered in the middle of a string escape",
+ false);
}
}
}
@@ -557,75 +431,82 @@ void consumeTokenSemiString() throws JsonParserException {
private void consumeTokenStringUtf8Char(char c) throws JsonParserException {
ensureBuffer(5);
+ // Worst case (supplementary plane) decodes to a surrogate pair (2 chars)
+ expandBufferIfNeeded(2);
+
// Hand-UTF8-decoding
switch (c & 0xf0) {
- case 0x80:
- case 0x90:
- case 0xa0:
- case 0xb0:
- throw createParseException(null,
- "Illegal UTF-8 continuation byte: 0x" + Integer.toHexString(c & 0xff), false);
- case 0xc0:
- // Check for illegal C0 and C1 bytes
- if ((c & 0xe) == 0)
- throw createParseException(null, "Illegal UTF-8 byte: 0x" + Integer.toHexString(c & 0xff),
- false);
- // fall-through
- case 0xd0:
- c = (char)((c & 0x1f) << 6 | (buffer[index++] & 0x3f));
- reusableBuffer.append(c);
- utf8adjust++;
- break;
- case 0xe0:
- c = (char)((c & 0x0f) << 12 | (buffer[index++] & 0x3f) << 6 | (buffer[index++] & 0x3f));
- utf8adjust += 2;
- // Check for illegally-encoded surrogate - http://unicode.org/faq/utf_bom.html#utf8-4
- if ((c >= '\ud800' && c <= '\udbff') || (c >= '\udc00' && c <= '\udfff'))
- throw createParseException(null, "Illegal UTF-8 codepoint: 0x" + Integer.toHexString(c),
- false);
- reusableBuffer.append(c);
- break;
- case 0xf0:
- if ((c & 0xf) >= 5)
- throw createParseException(null, "Illegal UTF-8 byte: 0x" + Integer.toHexString(c & 0xff),
- false);
-
- // Extended char
- switch ((c & 0xc) >> 2) {
- case 0:
- case 1:
- reusableBuffer.appendCodePoint((c & 7) << 18 | (buffer[index++] & 0x3f) << 12
- | (buffer[index++] & 0x3f) << 6 | (buffer[index++] & 0x3f));
- utf8adjust += 3;
- break;
- case 2:
- // TODO: \uFFFD (replacement char)
- int codepoint = (c & 3) << 24 | (buffer[index++] & 0x3f) << 18 | (buffer[index++] & 0x3f) << 12
- | (buffer[index++] & 0x3f) << 6 | (buffer[index++] & 0x3f);
- throw createParseException(null,
- "Unable to represent codepoint 0x" + Integer.toHexString(codepoint)
- + " in a Java string", false);
- case 3:
- codepoint = (c & 1) << 30 | (buffer[index++] & 0x3f) << 24 | (buffer[index++] & 0x3f) << 18
- | (buffer[index++] & 0x3f) << 12 | (buffer[index++] & 0x3f) << 6
- | (buffer[index++] & 0x3f);
+ case 0x80:
+ case 0x90:
+ case 0xa0:
+ case 0xb0:
throw createParseException(null,
- "Unable to represent codepoint 0x" + Integer.toHexString(codepoint)
- + " in a Java string", false);
+ "Illegal UTF-8 continuation byte: 0x" + Integer.toHexString(c & 0xff), false);
+ case 0xc0:
+ // Check for illegal C0 and C1 bytes
+ if ((c & 0xe) == 0)
+ throw createParseException(null, "Illegal UTF-8 byte: 0x" + Integer.toHexString(c & 0xff),
+ false);
+ // fall-through
+ case 0xd0:
+ c = (char) ((c & 0x1f) << 6 | (buffer[index++] & 0x3f));
+ reusableBuffer.put(c);
+ utf8adjust++;
+ break;
+ case 0xe0:
+ c = (char) ((c & 0x0f) << 12 | (buffer[index++] & 0x3f) << 6 | (buffer[index++] & 0x3f));
+ utf8adjust += 2;
+ // Check for illegally-encoded surrogate -
+ // http://unicode.org/faq/utf_bom.html#utf8-4
+ if ((c >= '\ud800' && c <= '\udbff') || (c >= '\udc00' && c <= '\udfff'))
+ throw createParseException(null, "Illegal UTF-8 codepoint: 0x" + Integer.toHexString(c),
+ false);
+ reusableBuffer.put(c);
+ break;
+ case 0xf0:
+ if ((c & 0xf) >= 5)
+ throw createParseException(null, "Illegal UTF-8 byte: 0x" + Integer.toHexString(c & 0xff),
+ false);
+
+ // Extended char
+ switch ((c & 0xc) >> 2) {
+ case 0:
+ case 1:
+ reusableBuffer.put(Character.toChars((c & 7) << 18 | (buffer[index++] & 0x3f) << 12
+ | (buffer[index++] & 0x3f) << 6 | (buffer[index++] & 0x3f)));
+ utf8adjust += 3;
+ break;
+ case 2:
+ // TODO: \uFFFD (replacement char)
+ int codepoint = (c & 3) << 24 | (buffer[index++] & 0x3f) << 18 | (buffer[index++] & 0x3f) << 12
+ | (buffer[index++] & 0x3f) << 6 | (buffer[index++] & 0x3f);
+ throw createParseException(null,
+ "Unable to represent codepoint 0x" + Integer.toHexString(codepoint)
+ + " in a Java string",
+ false);
+ case 3:
+ codepoint = (c & 1) << 30 | (buffer[index++] & 0x3f) << 24 | (buffer[index++] & 0x3f) << 18
+ | (buffer[index++] & 0x3f) << 12 | (buffer[index++] & 0x3f) << 6
+ | (buffer[index++] & 0x3f);
+ throw createParseException(null,
+ "Unable to represent codepoint 0x" + Integer.toHexString(codepoint)
+ + " in a Java string",
+ false);
+ default:
+ assert false : "Impossible";
+ }
+ break;
default:
- assert false : "Impossible";
- }
- break;
- default:
- // Regular old byte
- break;
+ // Regular old byte
+ break;
}
if (index > bufferLength)
throw createParseException(null, "UTF-8 codepoint was truncated", false);
}
/**
- * Advances a character, throwing if it is illegal in the context of a JSON string.
+ * Advances a character, throwing if it is illegal in the context of a JSON
+ * string.
*/
private char stringChar() throws JsonParserException {
char c = buffer[index++];
@@ -692,7 +573,8 @@ private int peekChar() {
}
/**
- * Ensures that there is enough room in the buffer to directly access the next N chars via buffer[].
+ * Ensures that there is enough room in the buffer to directly access the next N
+ * chars via buffer[].
*/
int ensureBuffer(int n) throws JsonParserException {
// We're good here
@@ -700,7 +582,8 @@ int ensureBuffer(int n) throws JsonParserException {
return n;
}
- // Nope, we need to read more, but we also have to retain whatever buffer we have
+ // Nope, we need to read more, but we also have to retain whatever buffer we
+ // have
if (index > 0) {
charOffset += index;
bufferLength = bufferLength - index;
@@ -748,7 +631,7 @@ private int advanceChar() throws JsonParserException {
return c;
}
-
+
int advanceCharFast() {
int c = buffer[index];
if (c == '\n') {
@@ -760,7 +643,7 @@ int advanceCharFast() {
index++;
return c;
}
-
+
private void consumeWhitespace() throws JsonParserException {
int n;
do {
@@ -781,12 +664,13 @@ private void consumeWhitespace() throws JsonParserException {
} while (n > 0);
eof = true;
}
-
+
/**
- * Consumes a token, first eating up any whitespace ahead of it. Note that number tokens are not necessarily valid
+ * Consumes a token, first eating up any whitespace ahead of it. Note that
+ * number tokens are not necessarily valid
* numbers.
*/
- int advanceToToken(boolean allowSemiString) throws JsonParserException {
+ int advanceToToken() throws JsonParserException {
int c = advanceChar();
while (isWhitespace(c))
c = advanceChar();
@@ -794,102 +678,69 @@ int advanceToToken(boolean allowSemiString) throws JsonParserException {
tokenCharPos = index + charOffset - rowPos - utf8adjust;
tokenCharOffset = charOffset + index;
- int oldIndex = index;
int token;
switch (c) {
- case -1:
- return TOKEN_EOF;
- case '[':
- token = TOKEN_ARRAY_START;
- break;
- case ']':
- token = TOKEN_ARRAY_END;
- break;
- case ',':
- token = TOKEN_COMMA;
- break;
- case ':':
- token = TOKEN_COLON;
- break;
- case '{':
- token = TOKEN_OBJECT_START;
- break;
- case '}':
- token = TOKEN_OBJECT_END;
- break;
- case 't':
- try {
+ case -1:
+ return TOKEN_EOF;
+ case '[':
+ token = TOKEN_ARRAY_START;
+ break;
+ case ']':
+ token = TOKEN_ARRAY_END;
+ break;
+ case ',':
+ token = TOKEN_COMMA;
+ break;
+ case ':':
+ token = TOKEN_COLON;
+ break;
+ case '{':
+ token = TOKEN_OBJECT_START;
+ break;
+ case '}':
+ token = TOKEN_OBJECT_END;
+ break;
+ case 't':
consumeKeyword((char) c, JsonTokener.TRUE);
token = TOKEN_TRUE;
- } catch (JsonParserException e) {
- if (allowSemiString) {
- index = oldIndex - 1;
- consumeTokenSemiString();
- token = TOKEN_SEMI_STRING;
- } else throw e;
- }
- break;
- case 'f':
- try {
- consumeKeyword((char)c, JsonTokener.FALSE);
+ break;
+ case 'f':
+ consumeKeyword((char) c, JsonTokener.FALSE);
token = TOKEN_FALSE;
- } catch (JsonParserException e) {
- if (allowSemiString) {
- index = oldIndex - 1;
- consumeTokenSemiString();
- token = TOKEN_SEMI_STRING;
- } else throw e;
- }
- break;
- case 'n':
- try {
- consumeKeyword((char)c, JsonTokener.NULL);
+ break;
+ case 'n':
+ consumeKeyword((char) c, JsonTokener.NULL);
token = TOKEN_NULL;
- } catch (JsonParserException e) {
- if (allowSemiString) {
- index = oldIndex - 1;
- consumeTokenSemiString();
- token = TOKEN_SEMI_STRING;
- } else throw e;
- }
- break;
- case '"':
- case '\'':
- consumeTokenString(c);
- token = TOKEN_STRING;
- break;
- case '-':
- case '0':
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7':
- case '8':
- case '9':
- consumeTokenNumber((char)c);
- token = TOKEN_NUMBER;
- break;
- case '+':
- case '.':
- throw createParseException(null, "Numbers may not start with '" + (char)c + "'", true);
- default:
- if (allowSemiString) {
- index--;
- consumeTokenSemiString();
- token = TOKEN_SEMI_STRING;
break;
- } else {
+ case '\"':
+ consumeTokenString();
+ token = TOKEN_STRING;
+ break;
+ case '-':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ consumeTokenNumber((char) c);
+ token = TOKEN_NUMBER;
+ break;
+ case '+':
+ case '.':
+ throw createParseException(null, "Numbers may not start with '" + (char) c + "'", true);
+ default:
if (isAsciiLetter(c))
- throw createHelpfulException((char)c, null, 0);
+ throw createHelpfulException((char) c, null, 0);
- throw createParseException(null, "Unexpected character: " + (char)c, true);
- }
+ throw createParseException(null, "Unexpected character: " + (char) c, true);
}
-
-// consumeWhitespace();
+
+ // consumeWhitespace();
return token;
}
@@ -908,6 +759,18 @@ void fixupAfterRawBufferRead() throws JsonParserException {
eof = refillBuffer();
}
+ private void expandBufferIfNeeded(int size) {
+ if (reusableBuffer.remaining() < size) {
+ int oldPos = reusableBuffer.position();
+ int increment = Math.max(512, size - reusableBuffer.remaining());
+ CharBuffer newBuffer = CharBuffer.allocate(reusableBuffer.capacity() + increment);
+ reusableBuffer.flip(); // position -> 0, limit -> oldPos
+ newBuffer.put(reusableBuffer); // copy all existing data
+ reusableBuffer = newBuffer;
+ reusableBuffer.position(oldPos); // restore write position at end
+ }
+ }
+
/**
* Throws a helpful exception based on the current alphanumeric token.
*/
@@ -919,14 +782,30 @@ JsonParserException createHelpfulException(char first, char[] expected, int fail
// Consume the whole pseudo-token to make a better error message
while (isAsciiLetter(peekChar()) && errorToken.length() < 15)
- errorToken.append((char)advanceChar());
+ errorToken.append((char) advanceChar());
return createParseException(null, "Unexpected token '" + errorToken + "'"
+ (expected == null ? "" : ". Did you mean '" + first + new String(expected) + "'?"), true);
}
/**
- * Creates a {@link JsonParserException} and fills it from the current line and char position.
+ * Releases resources used by this JsonTokener. Should be called when done
+ * tokenizing.
+ */
+ @Override
+ public void close() throws IOException {
+ if (reusableBuffer != null) {
+ CharBufferPool.release(reusableBuffer);
+ reusableBuffer = null;
+ }
+ if (reader != null) {
+ reader.close();
+ }
+ }
+
+ /**
+ * Creates a {@link JsonParserException} and fills it from the current line and
+ * char position.
*/
JsonParserException createParseException(Exception e, String message, boolean tokenPos) {
if (tokenPos)
diff --git a/src/main/java/com/grack/nanojson/JsonWriter.java b/src/main/java/com/grack/nanojson/JsonWriter.java
index 25d1dec..b9af209 100644
--- a/src/main/java/com/grack/nanojson/JsonWriter.java
+++ b/src/main/java/com/grack/nanojson/JsonWriter.java
@@ -151,7 +151,7 @@ public JsonAppendableWriter on(OutputStream out) {
*
*/
//@formatter:on
- public static JsonWriterContext indent(String indent) {
+ public static JsonWriter.JsonWriterContext indent(String indent) {
if (indent == null) {
throw new IllegalArgumentException("indent must be non-null");
}
diff --git a/src/main/java/com/grack/nanojson/JsonWriterBase.java b/src/main/java/com/grack/nanojson/JsonWriterBase.java
index 350737e..19cdfba 100644
--- a/src/main/java/com/grack/nanojson/JsonWriterBase.java
+++ b/src/main/java/com/grack/nanojson/JsonWriterBase.java
@@ -26,9 +26,9 @@
* Internal class that handles emitting to an {@link Appendable}. Users only see
* the public subclasses, {@link JsonStringWriter} and
* {@link JsonAppendableWriter}.
- *
+ *
* @param
- * A subclass of {@link JsonWriterBase}.
+ * A subclass of {@link JsonWriterBase}.
*/
class JsonWriterBase> implements
JsonSink {
@@ -50,6 +50,7 @@ class JsonWriterBase> implements
private int stateIndex = 0;
private boolean first = true;
private boolean inObject;
+ private String pendingKey;
/**
* Sequence to use for indenting.
@@ -123,8 +124,9 @@ public SELF object(String key, Map, ?> map) {
Object o = entry.getValue();
if (!(entry.getKey() instanceof String))
throw new JsonWriterException("Invalid key type for map: "
- + (entry.getKey() == null ? "null" : entry.getKey()
- .getClass()));
+ + (entry.getKey() == null ? "null"
+ : entry.getKey()
+ .getClass()));
String k = (String) entry.getKey();
value(k, o);
}
@@ -152,6 +154,8 @@ public SELF value(Object o) {
return nul();
else if (o instanceof String)
return value((String) o);
+ else if (o instanceof LazyString)
+ return value(o.toString());
else if (o instanceof Number)
return value(((Number) o));
else if (o instanceof Boolean)
@@ -166,7 +170,9 @@ else if (o.getClass().isArray()) {
for (int i = 0; i < length; i++)
value(Array.get(o, i));
return end();
- } else
+ } else if (o instanceof JsonConvertible)
+ return value(((JsonConvertible) o).toJsonValue());
+ else
throw new JsonWriterException("Unable to handle type: "
+ o.getClass());
}
@@ -177,6 +183,8 @@ public SELF value(String key, Object o) {
return nul(key);
else if (o instanceof String)
return value(key, (String) o);
+ else if (o instanceof LazyString)
+ return value(key, o.toString());
else if (o instanceof Number)
return value(key, (Number) o);
else if (o instanceof Boolean)
@@ -191,7 +199,9 @@ else if (o.getClass().isArray()) {
for (int i = 0; i < length; i++)
value(Array.get(o, i));
return end();
- } else
+ } else if (o instanceof JsonConvertible)
+ return value(key, ((JsonConvertible) o).toJsonValue());
+ else
throw new JsonWriterException("Unable to handle type: "
+ o.getClass());
}
@@ -243,7 +253,7 @@ public SELF value(float d) {
@Override
public SELF value(Number n) {
preValue();
- if (n == null)
+ if (n == null || nullish(n))
raw(NULL);
else
raw(n.toString());
@@ -372,12 +382,25 @@ public SELF end() {
return castThis();
}
+ @Override
+ public SELF key(String key) {
+ if (key == null)
+ throw new NullPointerException("key");
+ if (pendingKey != null)
+ throw new JsonWriterException(
+ "Invalid call to emit a key immediately after emitting a key");
+ pendingKey = key;
+ return castThis();
+ }
+
/**
* Ensures that the object is in the finished state.
- *
+ *
* @throws JsonWriterException
- * if the written JSON is not properly balanced, ie: all arrays
- * and objects that were started have been properly ended.
+ * if the written JSON is not properly balanced, ie:
+ * all arrays
+ * and objects that were started have been properly
+ * ended.
*/
protected void doneInternal() {
if (stateIndex > 0)
@@ -434,7 +457,7 @@ private void raw(char c) {
if (utf8) {
if (bo + 1 > BUFFER_SIZE)
flush();
- bb[bo++] = (byte)c;
+ bb[bo++] = (byte) c;
} else {
buffer.append(c);
if (buffer.length() > BUFFER_SIZE) {
@@ -472,6 +495,12 @@ private void pre() {
}
private void preValue() {
+ if (pendingKey != null) {
+ String key = pendingKey;
+ pendingKey = null;
+ preValue(key);
+ return;
+ }
if (inObject)
throw new JsonWriterException(
"Invalid call to emit a keyless value while writing an object");
@@ -483,6 +512,9 @@ private void preValue(String key) {
if (!inObject)
throw new JsonWriterException(
"Invalid call to emit a key value while not writing an object");
+ if (pendingKey != null)
+ throw new JsonWriterException(
+ "Invalid call to emit a key value immediately after emitting a key");
pre();
@@ -505,82 +537,106 @@ private void emitStringValue(String s) {
c = s.charAt(i);
switch (c) {
- case '\\':
- case '"':
- raw('\\');
- raw(c);
- break;
- case '/':
- // Special case to ensure that doesn't appear in JSON
- // output
- if (b == '<')
+ case '\\':
+ case '"':
raw('\\');
- raw(c);
- break;
- case '\b':
- raw("\\b");
- break;
- case '\t':
- raw("\\t");
- break;
- case '\n':
- raw("\\n");
- break;
- case '\f':
- raw("\\f");
- break;
- case '\r':
- raw("\\r");
- break;
- default:
- if (shouldBeEscaped(c)) {
- if (c < 0x100) {
- raw(UNICODE_SMALL);
- raw(HEX[(c >> 4) & 0xf]);
- raw(HEX[c & 0xf]);
+ raw(c);
+ break;
+ case '/':
+ // Special case to ensure that doesn't appear in JSON
+ // output
+ if (b == '<')
+ raw('\\');
+ raw(c);
+ break;
+ case '\b':
+ raw("\\b");
+ break;
+ case '\t':
+ raw("\\t");
+ break;
+ case '\n':
+ raw("\\n");
+ break;
+ case '\f':
+ raw("\\f");
+ break;
+ case '\r':
+ raw("\\r");
+ break;
+ default:
+ if (shouldBeEscaped(c)) {
+ if (c < 0x100) {
+ raw(UNICODE_SMALL);
+ raw(HEX[(c >> 4) & 0xf]);
+ raw(HEX[c & 0xf]);
+ } else {
+ raw(UNICODE_LARGE);
+ raw(HEX[(c >> 12) & 0xf]);
+ raw(HEX[(c >> 8) & 0xf]);
+ raw(HEX[(c >> 4) & 0xf]);
+ raw(HEX[c & 0xf]);
+ }
} else {
- raw(UNICODE_LARGE);
- raw(HEX[(c >> 12) & 0xf]);
- raw(HEX[(c >> 8) & 0xf]);
- raw(HEX[(c >> 4) & 0xf]);
- raw(HEX[c & 0xf]);
- }
- } else {
- if (utf8) {
- if (bo + 4 > BUFFER_SIZE) // 4 is the max char size
- flush();
- if (c < 0x80) {
- bb[bo++] = (byte) c;
- } else if (c < 0x800) {
- bb[bo++] = (byte) (0xc0 | c >> 6);
- bb[bo++] = (byte) (0x80 | c & 0x3f);
- } else if (c < 0xd800) {
- bb[bo++] = (byte) (0xe0 | c >> 12);
- bb[bo++] = (byte) (0x80 | (c >> 6) & 0x3f);
- bb[bo++] = (byte) (0x80 | c & 0x3f);
- } else if (c < 0xdfff) {
- // TODO: bad surrogates
- i++;
-
- int fc = Character.toCodePoint(c, s.charAt(i));
- if (fc < 0x1fffff) {
- bb[bo++] = (byte) (0xf0 | fc >> 18);
- bb[bo++] = (byte) (0x80 | (fc >> 12) & 0x3f);
- bb[bo++] = (byte) (0x80 | (fc >> 6) & 0x3f);
- bb[bo++] = (byte) (0x80 | fc & 0x3f);
+ if (utf8) {
+ // Ensure space for the largest possible UTF-8 sequence (4 bytes) before
+ // encoding. Even if this char ultimately encodes to 1,2 or 3 bytes,
+ // reserving for 4 keeps logic simple and guarantees we never start a
+ // multi-byte sequence that would be split across a flush.
+ if (bo + 4 > BUFFER_SIZE) // 4 is the max UTF-8 byte length for a single Unicode scalar
+ // value
+ flush();
+ if (c < 0x80) {
+ bb[bo++] = (byte) c;
+ } else if (c < 0x800) {
+ bb[bo++] = (byte) (0xc0 | c >> 6);
+ bb[bo++] = (byte) (0x80 | c & 0x3f);
+ } else if (c < 0xd800) {
+ bb[bo++] = (byte) (0xe0 | c >> 12);
+ bb[bo++] = (byte) (0x80 | (c >> 6) & 0x3f);
+ bb[bo++] = (byte) (0x80 | c & 0x3f);
+ } else if (Character.isHighSurrogate(c)) {
+ // Surrogate pair handling (supplementary plane character)
+ // We have a high surrogate; must be followed by a low surrogate to form a valid
+ // code point.
+ if (i + 1 >= s.length()) {
+ throw new JsonWriterException("Invalid high surrogate at end of string");
+ }
+ char lowSurrogate = s.charAt(i + 1);
+ if (!Character.isLowSurrogate(lowSurrogate)) {
+ throw new JsonWriterException("Invalid surrogate pair: "
+ + "high surrogate not followed by low surrogate");
+ }
+ // Need 4 bytes for any supplementary code point in UTF-8. Flush first if
+ // insufficient space
+ // so the 4-byte sequence is never split across buffers.
+ if (bo + 4 > BUFFER_SIZE)
+ flush();
+ i++; // consume the low surrogate
+ int fc = Character.toCodePoint(c, lowSurrogate); // full scalar value
+ // Unicode scalar values are defined only up to U+10FFFF
+ // (exclusive upper bound 0x110000).
+ if (fc < 0x110000) {
+ bb[bo++] = (byte) (0xf0 | (fc >> 18));
+ bb[bo++] = (byte) (0x80 | (fc >> 12) & 0x3f);
+ bb[bo++] = (byte) (0x80 | (fc >> 6) & 0x3f);
+ bb[bo++] = (byte) (0x80 | fc & 0x3f);
+ } else {
+ throw new JsonWriterException(
+ "Unable to encode character 0x" + Integer.toHexString(fc));
+ }
+ } else if (Character.isLowSurrogate(c)) {
+ throw new JsonWriterException(
+ "Invalid low surrogate without preceding high surrogate");
} else {
- throw new JsonWriterException("Unable to encode character 0x"
- + Integer.toHexString(fc));
+ bb[bo++] = (byte) (0xe0 | c >> 12);
+ bb[bo++] = (byte) (0x80 | (c >> 6) & 0x3f);
+ bb[bo++] = (byte) (0x80 | c & 0x3f);
}
} else {
- bb[bo++] = (byte) (0xe0 | c >> 12);
- bb[bo++] = (byte) (0x80 | (c >> 6) & 0x3f);
- bb[bo++] = (byte) (0x80 | c & 0x3f);
+ raw(c);
}
- } else {
- raw(c);
}
- }
}
}
@@ -594,4 +650,25 @@ private boolean shouldBeEscaped(char c) {
return c < ' ' || (c >= '\u0080' && c < '\u00a0')
|| (c >= '\u2000' && c < '\u2100');
}
+
+ /**
+ * Returns true if the number becomes null when converted to JSON. json.org spec
+ * does not specify
+ * NaN or Infinity as numbers, and modern JavaScript engines convert them to
+ * null.
+ *
+ * @param n a number
+ * @return true if the number is nullish.
+ */
+ private boolean nullish(Number n) {
+ if (n instanceof Double) {
+ Double d = (Double) n;
+ return d.isNaN() || d.isInfinite();
+ }
+ if (n instanceof Float) {
+ Float f = (Float) n;
+ return f.isNaN() || f.isInfinite();
+ }
+ return false;
+ }
}
diff --git a/src/main/java/com/grack/nanojson/LazyString.java b/src/main/java/com/grack/nanojson/LazyString.java
new file mode 100644
index 0000000..0b70535
--- /dev/null
+++ b/src/main/java/com/grack/nanojson/LazyString.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2011 The nanojson Authors
+ *
+ * Licensed 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.
+ */
+package com.grack.nanojson;
+
+import java.util.Arrays;
+
+public class LazyString implements CharSequence {
+
+ private final int length;
+
+ private char[] value;
+ private String stringValue;
+
+ private final Object lock = new Object();
+
+ public LazyString(char[] value) {
+ this.value = value;
+ this.length = value.length;
+ }
+
+ public LazyString(String value) {
+ this.stringValue = value;
+ this.length = value.length();
+ }
+
+ @Override
+ public int length() {
+ return length;
+ }
+
+ @Override
+ public char charAt(int index) {
+ String str = stringValue; // Local ref copy to avoid race
+ if (str != null) {
+ return str.charAt(index);
+ }
+ char[] arr = value; // Local ref copy to avoid race
+ if (arr != null) {
+ return arr[index];
+ }
+ // Fallback if value was nullified
+ return toString().charAt(index);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ String str = stringValue; // Local ref copy to avoid race
+ if (str != null) {
+ return str.subSequence(start, end);
+ }
+ char[] arr = value; // Local ref copy to avoid race
+ if (arr != null) {
+ return new LazyString(Arrays.copyOfRange(arr, start, end));
+ }
+ // Fallback to string-based subsequence
+ return toString().subSequence(start, end);
+ }
+
+ public String toString() {
+ if (stringValue != null) {
+ return stringValue;
+ }
+ synchronized (lock) {
+ if (stringValue == null) {
+ stringValue = new String(value);
+ }
+ value = null; // Clear the char array to save memory
+ }
+ return stringValue;
+ }
+
+ @Override
+ public int hashCode() {
+ String str = stringValue; // Local ref copy to avoid race
+ if (str != null) {
+ return str.hashCode();
+ }
+ char[] arr = value; // Local ref copy to avoid race
+ if (arr != null) {
+ return Arrays.hashCode(arr);
+ }
+ return toString().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof CharSequence))
+ return false;
+ if (obj instanceof LazyString) {
+ LazyString other = (LazyString) obj;
+ String str = stringValue; // Local ref copy to avoid race
+ String otherStr = other.stringValue; // Local ref copy to avoid race
+ if (str != null && otherStr != null) {
+ return str.equals(otherStr);
+ } else if (str != null) {
+ return str.contentEquals((CharSequence) other);
+ } else if (otherStr != null) {
+ return otherStr.contentEquals((CharSequence) this);
+ }
+ // Both are LazyString without stringValue
+ char[] arr = value; // Local ref copy to avoid race
+ char[] otherArr = other.value; // Local ref copy to avoid race
+ if (arr != null && otherArr != null) {
+ return Arrays.equals(arr, otherArr);
+ }
+ // Fallback to string comparison
+ return toString().equals(other.toString());
+ }
+ return toString().contentEquals((CharSequence) obj);
+ }
+}
diff --git a/src/test/java/com/grack/nanojson/JsonBuilderTest.java b/src/test/java/com/grack/nanojson/JsonBuilderTest.java
new file mode 100644
index 0000000..81e5d65
--- /dev/null
+++ b/src/test/java/com/grack/nanojson/JsonBuilderTest.java
@@ -0,0 +1,29 @@
+package com.grack.nanojson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class JsonBuilderTest {
+ @Test
+ void failureKeyInArray() {
+ assertThrows(JsonWriterException.class, () ->
+ new JsonBuilder<>(new JsonArray()).key("a"));
+ }
+
+ @Test
+ void failureKeyWhileKeyPending() {
+ assertThrows(JsonWriterException.class, () ->
+ new JsonBuilder<>(new JsonObject()).key("a").key("b"));
+ }
+
+ @Test
+ void separateKeyWriting() {
+ JsonObject actual = new JsonBuilder<>(new JsonObject()).key("a").value(1).key("b").value(2).done();
+ JsonObject expected = new JsonObject();
+ expected.put("a", 1);
+ expected.put("b", 2);
+ assertEquals(expected, actual);
+ }
+}
diff --git a/src/test/java/com/grack/nanojson/JsonNumberTest.java b/src/test/java/com/grack/nanojson/JsonNumberTest.java
index 45d2d55..3ff4475 100644
--- a/src/test/java/com/grack/nanojson/JsonNumberTest.java
+++ b/src/test/java/com/grack/nanojson/JsonNumberTest.java
@@ -15,111 +15,113 @@
*/
package com.grack.nanojson;
-import static org.junit.Assert.assertEquals;
+import org.junit.jupiter.api.Test;
import java.math.BigInteger;
-import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Attempts to test that numbers are correctly round-tripped.
*/
-public class JsonNumberTest {
- // CHECKSTYLE_OFF: MagicNumber
- // CHECKSTYLE_OFF: JavadocMethod
- @Test
- public void testBasicNumberRead() throws JsonParserException {
- JsonArray array = JsonParser.array().from("[1, 1.0, 1.00]");
- assertEquals(Integer.class, array.get(0).getClass());
- assertEquals(Double.class, array.get(1).getClass());
- assertEquals(Double.class, array.get(2).getClass());
- }
+class JsonNumberTest {
+ // CHECKSTYLE_OFF: MagicNumber
+ // CHECKSTYLE_OFF: JavadocMethod
+ @Test
+ void basicNumberRead() throws JsonParserException {
+ JsonArray array = JsonParser.array().from("[1, 1.0, 1.00]");
+ assertEquals(1, ((Number) array.get(0)).intValue());
+ assertEquals(1.0, ((Number) array.get(1)).doubleValue(), 0.0);
+ assertEquals(1.0, ((Number) array.get(2)).doubleValue(), 0.0);
+ }
- @Test
- public void testBasicNumberWrite() {
- JsonArray array = JsonArray.from(1, 1.0, 1.0f);
- assertEquals("[1,1.0,1.0]", JsonWriter.string().array(array).done());
- }
+ @Test
+ void basicNumberWrite() {
+ JsonArray array = JsonArray.from(1, 1.0, 1.0f);
+ assertEquals("[1,1.0,1.0]", JsonWriter.string().array(array).done());
+ }
- @Test
- public void testLargeIntRead() throws JsonParserException {
- JsonArray array = JsonParser.array().from("[-300000000,300000000]");
- assertEquals(Integer.class, array.get(0).getClass());
- assertEquals(-300000000, array.get(0));
- assertEquals(Integer.class, array.get(1).getClass());
- assertEquals(300000000, array.get(1));
- }
+ @Test
+ void largeIntRead() throws JsonParserException {
+ JsonArray array = JsonParser.array().from("[-300000000,300000000]");
+ assertEquals(-300000000, ((Number) array.get(0)).intValue());
+ assertEquals(300000000, ((Number) array.get(1)).intValue());
+ }
- @Test
- public void testLargeIntWrite() {
- JsonArray array = JsonArray.from(-300000000, 300000000);
- assertEquals("[-300000000,300000000]", JsonWriter.string().array(array)
- .done());
- }
+ @Test
+ void largeIntWrite() {
+ JsonArray array = JsonArray.from(-300000000, 300000000);
+ assertEquals("[-300000000,300000000]", JsonWriter.string().array(array)
+ .done());
+ }
- @Test
- public void testLongRead() throws JsonParserException {
- JsonArray array = JsonParser.array().from("[-3000000000,3000000000]");
- assertEquals(Long.class, array.get(0).getClass());
- assertEquals(-3000000000L, array.get(0));
- assertEquals(Long.class, array.get(1).getClass());
- assertEquals(3000000000L, array.get(1));
- }
+ @Test
+ void longRead() throws JsonParserException {
+ JsonArray array = JsonParser.array().from("[-3000000000,3000000000]");
+ assertEquals(-3000000000L, ((Number) array.get(0)).longValue());
+ assertEquals(3000000000L, ((Number) array.get(1)).longValue());
+ }
- @Test
- public void testLongWrite() {
- JsonArray array = JsonArray.from(1L, -3000000000L, 3000000000L);
- assertEquals("[1,-3000000000,3000000000]",
- JsonWriter.string().array(array).done());
- }
+ @Test
+ void longWrite() {
+ JsonArray array = JsonArray.from(1L, -3000000000L, 3000000000L);
+ assertEquals("[1,-3000000000,3000000000]",
+ JsonWriter.string().array(array).done());
+ }
- @Test
- public void testBigIntRead() throws JsonParserException {
- JsonArray array = JsonParser.array().from(
- "[-30000000000000000000,30000000000000000000]");
- assertEquals(BigInteger.class, array.get(0).getClass());
- assertEquals(new BigInteger("-30000000000000000000"), array.get(0));
- assertEquals(BigInteger.class, array.get(1).getClass());
- assertEquals(new BigInteger("30000000000000000000"), array.get(1));
- }
+ @Test
+ void bigIntRead() throws JsonParserException {
+ JsonArray array = JsonParser.array().from(
+ "[-30000000000000000000,30000000000000000000]");
+ // cast to ensure it's a number
+ assertEquals("-30000000000000000000", ((Number) array.get(0)).toString());
+ assertEquals("30000000000000000000", ((Number) array.get(1)).toString());
+ }
- @Test
- public void testBigIntWrite() {
- JsonArray array = JsonArray.from(BigInteger.ONE, new BigInteger(
- "-30000000000000000000"),
- new BigInteger("30000000000000000000"));
- assertEquals("[1,-30000000000000000000,30000000000000000000]",
- JsonWriter.string().array(array).done());
- }
+ @Test
+ void bigIntWrite() {
+ JsonArray array = JsonArray.from(BigInteger.ONE, new BigInteger(
+ "-30000000000000000000"),
+ new BigInteger("30000000000000000000"));
+ assertEquals("[1,-30000000000000000000,30000000000000000000]",
+ JsonWriter.string().array(array).done());
+ }
- /**
- * Tests a bug where longs were silently truncated to floats.
- */
- @Test
- public void testLongBuilder() {
- JsonObject o = JsonObject.builder().value("long", 0xffffffffffffL)
- .done();
- assertEquals(0xffffffffffffL, o.getNumber("long").longValue());
- }
+ /**
+ * Tests a bug where longs were silently truncated to floats.
+ */
+ @Test
+ void longBuilder() {
+ JsonObject o = JsonObject.builder().value("long", 0xffffffffffffL)
+ .done();
+ assertEquals(0xffffffffffffL, o.getNumber("long").longValue());
+ }
- /**
- * Test around the edges of the integral types.
- */
- @Test
- public void testAroundEdges() throws JsonParserException {
- JsonArray array = JsonArray.from(Integer.MAX_VALUE,
- ((long) Integer.MAX_VALUE) + 1, Integer.MIN_VALUE,
- ((long) Integer.MIN_VALUE) - 1, Long.MAX_VALUE, BigInteger
- .valueOf(Long.MAX_VALUE).add(BigInteger.ONE),
- Long.MIN_VALUE,
- BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE));
- String json = JsonWriter.string().array(array).done();
- assertEquals(
- "[2147483647,2147483648,-2147483648,-2147483649,9223372036854775807,"
- + "9223372036854775808,-9223372036854775808,-9223372036854775809]",
- json);
- JsonArray array2 = JsonParser.array().from(json);
- String json2 = JsonWriter.string().array(array2).done();
- assertEquals(json, json2);
- }
+ /**
+ * Test around the edges of the integral types.
+ */
+ @Test
+ void aroundEdges() throws JsonParserException {
+ JsonArray array = JsonArray.from(Integer.MAX_VALUE,
+ ((long) Integer.MAX_VALUE) + 1, Integer.MIN_VALUE,
+ ((long) Integer.MIN_VALUE) - 1, Long.MAX_VALUE, BigInteger
+ .valueOf(Long.MAX_VALUE).add(BigInteger.ONE),
+ Long.MIN_VALUE,
+ BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE));
+ String json = JsonWriter.string().array(array).done();
+ assertEquals(
+ "[2147483647,2147483648,-2147483648,-2147483649,9223372036854775807,"
+ + "9223372036854775808,-9223372036854775808,-9223372036854775809]",
+ json);
+ JsonArray array2 = JsonParser.array().from(json);
+ String json2 = JsonWriter.string().array(array2).done();
+ assertEquals(json, json2);
+ }
+
+ @Test
+ void trailingDecimalLazy() throws JsonParserException {
+ Object value = JsonParser.any().withLazyNumbers().from("1.000");
+ String json = JsonWriter.string().value(value).done();
+ assertEquals("1.000", json);
+ }
}
diff --git a/src/test/java/com/grack/nanojson/JsonParserTest.java b/src/test/java/com/grack/nanojson/JsonParserTest.java
index 7b89b8f..dbbf3f5 100644
--- a/src/test/java/com/grack/nanojson/JsonParserTest.java
+++ b/src/test/java/com/grack/nanojson/JsonParserTest.java
@@ -1,12 +1,12 @@
/*
* Copyright 2011 The nanojson Authors
- *
+ *
* Licensed 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
@@ -15,29 +15,28 @@
*/
package com.grack.nanojson;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
/**
* Test for {@link JsonParser}.
*/
-public class JsonParserTest {
+class JsonParserTest {
private static final Charset UTF8;
static {
@@ -48,88 +47,85 @@ public class JsonParserTest {
// CHECKSTYLE_OFF: JavadocMethod
// CHECKSTYLE_OFF: EmptyBlock
@Test
- public void testWhitespace() throws JsonParserException {
+ void whitespace() throws JsonParserException {
assertEquals(JsonObject.class,
JsonParser.object().from(" \t\r\n { \t\r\n \"abc\" \t\r\n : \t\r\n 1 \t\r\n } \t\r\n ")
.getClass());
assertEquals("{}", JsonParser.object().from("{}").toString());
}
-
+
@Test
- public void testWhitespaceSimpler() throws JsonParserException {
+ void whitespaceSimpler() throws JsonParserException {
assertEquals(JsonObject.class,
JsonParser.object().from(" {} ")
.getClass());
}
-
@Test
- public void testWriterOutput() throws JsonParserException {
+ void writerOutput() throws JsonParserException {
//@formatter:off
String json = JsonWriter.string()
- .object()
- .object("a")
- .array("b")
- .object()
- .value("a", 1)
- .value("b", 2)
- .end()
- .object()
- .value("c", 1.0)
- .value("d", 2.0)
- .end()
+ .object()
+ .object("a")
+ .array("b")
+ .object()
+ .value("a", 1)
+ .value("b", 2)
+ .end()
+ .object()
+ .value("c", 1.0)
+ .value("d", 2.0)
.end()
- .value("c", JsonArray.from("v0", "v1", "v2"))
.end()
+ .value("c", JsonArray.from("v0", "v1", "v2"))
.end()
- .done();
+ .end()
+ .done();
//@formatter:on
-
- // Just make sure it can be read - don't validate
- JsonParser.object().from(json);
+ JsonParser.object().from(json); // ensure parseable
}
-
+
@Test
- public void testEmptyObject() throws JsonParserException {
+ void emptyObject() throws JsonParserException {
assertEquals(JsonObject.class, JsonParser.object().from("{}").getClass());
assertEquals("{}", JsonParser.object().from("{}").toString());
}
@Test
- public void testObjectOneElement() throws JsonParserException {
+ void objectOneElement() throws JsonParserException {
assertEquals(JsonObject.class, JsonParser.object().from("{\"a\":1}").getClass());
assertEquals("{a=1}", JsonParser.object().from("{\"a\":1}").toString());
}
@Test
- public void testObjectTwoElements() throws JsonParserException {
+ void objectTwoElements() throws JsonParserException {
JsonObject obj = JsonParser.object().from("{\"a\":1,\"B\":1}");
assertEquals(JsonObject.class, obj.getClass());
- assertEquals(1, obj.get("B"));
- assertEquals(1, obj.get("a"));
+ assertEquals(1, obj.getInt("B"));
+ assertEquals(1, obj.getInt("a"));
assertEquals(2, obj.size());
}
@Test
- public void testEmptyArray() throws JsonParserException {
+ void emptyArray() throws JsonParserException {
assertEquals(JsonArray.class, JsonParser.array().from("[]").getClass());
assertEquals("[]", JsonParser.array().from("[]").toString());
}
@Test
- public void testArrayOneElement() throws JsonParserException {
+ void arrayOneElement() throws JsonParserException {
assertEquals(JsonArray.class, JsonParser.array().from("[1]").getClass());
assertEquals("[1]", JsonParser.array().from("[1]").toString());
}
@Test
- public void testArrayTwoElements() throws JsonParserException {
+ void arrayTwoElements() throws JsonParserException {
assertEquals(JsonArray.class, JsonParser.array().from("[1,1]").getClass());
assertEquals("[1, 1]", JsonParser.array().from("[1,1]").toString());
}
@Test
- public void testBasicTypes() throws JsonParserException {
+ void basicTypes() throws JsonParserException {
assertEquals("true", JsonParser.any().from("true").toString());
assertEquals("false", JsonParser.any().from("false").toString());
assertNull(JsonParser.any().from("null"));
@@ -140,9 +136,9 @@ public void testBasicTypes() throws JsonParserException {
}
@Test
- public void testArrayWithEverything() throws JsonParserException {
+ void arrayWithEverything() throws JsonParserException {
JsonArray a = JsonParser.array().from("[1, -1.0e6, \"abc\", [1,2,3], {\"abc\":123}, true, false]");
- assertEquals("[1, -1000000.0, abc, [1, 2, 3], {abc=123}, true, false]", a.toString());
+ assertEquals("[1, -1.0e6, abc, [1, 2, 3], {abc=123}, true, false]", a.toString());
assertEquals(1.0, a.getDouble(0), 0.001f);
assertEquals(1, a.getInt(0));
assertEquals(-1000000, a.getInt(1));
@@ -155,94 +151,95 @@ public void testArrayWithEverything() throws JsonParserException {
}
@Test
- public void testObjectWithEverything() throws JsonParserException {
+ void objectWithEverything() throws JsonParserException {
// TODO: Is this deterministic if we use string keys?
JsonObject o = JsonParser.object().from(
"{\"abc\":123, \"def\":456.0, \"ghi\":[true, false], \"jkl\":null, \"mno\":true}");
- assertEquals(null, o.get("jkl"));
+ assertNull(o.get("jkl"));
assertTrue(o.containsKey("jkl"));
- assertEquals(123, o.get("abc"));
+ assertEquals(123, ((Number) o.get("abc")).intValue());
assertEquals(Arrays.asList(true, false), o.get("ghi"));
- assertEquals(456.0, o.get("def"));
+ assertEquals(456.0, ((Number) o.get("def")).doubleValue());
assertEquals(true, o.get("mno"));
assertEquals(5, o.size());
assertEquals(123, o.getInt("abc"));
assertEquals(456, o.getInt("def"));
- assertEquals(true, o.getArray("ghi").getBoolean(0));
- assertEquals(null, o.get("jkl"));
+ assertTrue(o.getArray("ghi").getBoolean(0));
+ assertNull(o.get("jkl"));
assertTrue(o.isNull("jkl"));
assertTrue(o.getBoolean("mno"));
}
@Test
- public void testStringEscapes() throws JsonParserException {
- assertEquals("\n", JsonParser.any().from("\"\\n\""));
- assertEquals("\r", JsonParser.any().from("\"\\r\""));
- assertEquals("\t", JsonParser.any().from("\"\\t\""));
- assertEquals("\b", JsonParser.any().from("\"\\b\""));
- assertEquals("\f", JsonParser.any().from("\"\\f\""));
- assertEquals("/", JsonParser.any().from("\"/\""));
- assertEquals("\\", JsonParser.any().from("\"\\\\\""));
- assertEquals("\"", JsonParser.any().from("\"\\\"\""));
- assertEquals("\0", JsonParser.any().from("\"\\u0000\""));
- assertEquals("\u8000", JsonParser.any().from("\"\\u8000\""));
- assertEquals("\uffff", JsonParser.any().from("\"\\uffff\""));
- assertEquals("\uFFFF", JsonParser.any().from("\"\\uFFFF\""));
+ void stringEscapes() throws JsonParserException {
+ assertEquals("\n", JsonParser.any().from("\"\\n\"").toString());
+ assertEquals("\r", JsonParser.any().from("\"\\r\"").toString());
+ assertEquals("\t", JsonParser.any().from("\"\\t\"").toString());
+ assertEquals("\b", JsonParser.any().from("\"\\b\"").toString());
+ assertEquals("\f", JsonParser.any().from("\"\\f\"").toString());
+ assertEquals("/", JsonParser.any().from("\"/\"").toString());
+ assertEquals("\\", JsonParser.any().from("\"\\\\\"").toString());
+ assertEquals("\"", JsonParser.any().from("\"\\\"\"").toString());
+ assertEquals("\0", JsonParser.any().from("\"\\u0000\"").toString());
+ assertEquals("\u8000", JsonParser.any().from("\"\\u8000\"").toString());
+ assertEquals("\uffff", JsonParser.any().from("\"\\uffff\"").toString());
+ assertEquals("\uFFFF", JsonParser.any().from("\"\\uFFFF\"").toString());
assertEquals("all together: \\/\n\r\t\b\f (fin)",
- JsonParser.any().from("\"all together: \\\\\\/\\n\\r\\t\\b\\f (fin)\""));
+ JsonParser.any().from("\"all together: \\\\\\/\\n\\r\\t\\b\\f (fin)\"").toString());
}
@Test
- public void testStringEscapesAroundBufferBoundary() throws JsonParserException {
+ void stringEscapesAroundBufferBoundary() throws JsonParserException {
char[] c = new char[JsonTokener.BUFFER_SIZE - 1024];
- Arrays.fill(c, ' ');
+ Arrays.fill(c, ' ');
String base = new String(c);
for (int i = 0; i < 2048; i++) {
base += " ";
- assertEquals("\u0055", JsonParser.any().from(base + "\"\\u0055\""));
+ assertEquals("\u0055", JsonParser.any().from(base + "\"\\u0055\"").toString());
}
}
@Test
- public void testStringsAroundBufferBoundary() throws JsonParserException {
+ void stringsAroundBufferBoundary() throws JsonParserException {
char[] c = new char[JsonTokener.BUFFER_SIZE - 16];
- Arrays.fill(c, ' ');
+ Arrays.fill(c, ' ');
String base = new String(c);
for (int i = 0; i < 32; i++) {
base += " ";
- assertEquals(base, JsonParser.any().from('"' + base + '"'));
+ assertEquals(base, JsonParser.any().from('"' + base + '"').toString());
}
}
@Test
- public void testNumbers() throws JsonParserException {
+ void numbers() throws JsonParserException {
String[] testCases = new String[] { "0", "1", "-0", "-1", "0.1", "1.1", "-0.1", "0.10", "-0.10", "0e1", "0e0",
"-0e-1", "0.0e0", "-0.0e0", "9" };
for (String testCase : testCases) {
- Number n = (Number)JsonParser.any().from(testCase);
+ Number n = (Number) JsonParser.any().from(testCase);
assertEquals(Double.parseDouble(testCase), n.doubleValue(), Double.MIN_NORMAL);
- Number n2 = (Number)JsonParser.any().from(testCase.toUpperCase());
+ Number n2 = (Number) JsonParser.any().from(testCase.toUpperCase());
assertEquals(Double.parseDouble(testCase.toUpperCase()), n2.doubleValue(), Double.MIN_NORMAL);
}
}
/**
- * Test that negative zero ends up as negative zero in both the parser and the writer.
+ * Test that negative zero ends up as negative zero in both the parser and the
+ * writer.
*/
@Test
- public void testNegativeZero() throws JsonParserException {
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0.0")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0.0e0")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0e0")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0e1")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0e-1")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0e-0")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0e-01")).doubleValue()));
- assertEquals("-0.0", Double.toString(((Number)JsonParser.any().from("-0e-000000000001")).doubleValue()));
+ void negativeZero() throws JsonParserException {
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0.0")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0.0e0")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0e0")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0e1")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0e-1")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0e-0")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0e-01")).doubleValue()));
+ assertEquals("-0.0", Double.toString(((Number) JsonParser.any().from("-0e-000000000001")).doubleValue()));
assertEquals("-0.0", JsonWriter.string(-0.0));
assertEquals("-0.0", JsonWriter.string(-0.0f));
@@ -252,21 +249,24 @@ public void testNegativeZero() throws JsonParserException {
* Test the basic numbers from -100 to 100 as a sanity check.
*/
@Test
- public void testBasicNumbers() throws JsonParserException {
+ void basicNumbers() throws JsonParserException {
for (int i = -100; i <= +100; i++) {
- assertEquals(i, (int)(Integer)JsonParser.any().from("" + i));
+ Number n = (Number) JsonParser.any().from(Integer.toString(i));
+ assertEquals(i, n.intValue());
}
}
@Test
- public void testBigint() throws JsonParserException {
+ void bigint() throws JsonParserException {
JsonObject o = JsonParser.object().from("{\"v\":123456789123456789123456789}");
- BigInteger bigint = (BigInteger)o.get("v");
- assertEquals("123456789123456789123456789", bigint.toString());
+ Object raw = o.get("v");
+ // May be parsed as JsonLazyNumber or BigInteger depending on laziness settings
+ String s = raw.toString();
+ assertEquals("123456789123456789123456789", s);
}
@Test
- public void testFailWrongType() {
+ void failWrongType() {
try {
JsonParser.object().from("1");
fail("Should have failed to parse");
@@ -276,7 +276,7 @@ public void testFailWrongType() {
}
@Test
- public void testFailNull() {
+ void failNull() {
try {
JsonParser.object().from("null");
fail("Should have failed to parse");
@@ -286,7 +286,7 @@ public void testFailNull() {
}
@Test
- public void testFailNoJson1() {
+ void failNoJson1() {
try {
JsonParser.object().from("");
fail("Should have failed to parse");
@@ -296,7 +296,7 @@ public void testFailNoJson1() {
}
@Test
- public void testFailNoJson2() {
+ void failNoJson2() {
try {
JsonParser.object().from(" ");
fail("Should have failed to parse");
@@ -306,7 +306,7 @@ public void testFailNoJson2() {
}
@Test
- public void testFailNoJson3() {
+ void failNoJson3() {
try {
JsonParser.object().from(" ");
fail("Should have failed to parse");
@@ -316,7 +316,7 @@ public void testFailNoJson3() {
}
@Test
- public void testFailNumberEdgeCases() {
+ void failNumberEdgeCases() {
String[] edgeCases = { "-", ".", "e", "01", "-01", "+01", "01.1", "-01.1", "+01.1", ".1", "-.1", "+.1", "+1",
"0.", "-0.", "+0.", "0.e", "-0.e", "+0.e", "0e", "-0e", "+0e", "0e-", "-0e-", "+0e-", "0e+", "-0e+",
"+0e+", "-e", "+e", "2.", "-2.", "-1.e1", "1.e1", "0.e1" };
@@ -347,10 +347,11 @@ public void testFailNumberEdgeCases() {
}
/**
- * See http://seriot.ch/json/parsing.html and https://github.com/mmastrac/nanojson/issues/3.
+ * See http://seriot.ch/json/parsing.html and
+ * https://github.com/mmastrac/nanojson/issues/3.
*/
@Test
- public void testFailNumberEdgeCasesFromJSONSuite() {
+ void failNumberEdgeCasesFromJSONSuite() {
String[] edgeCases = { "[-2.]", "[0.e1]", "[2.e+3]", "[2.e-3]", "[2.e3]", "[1.]" };
for (String edgeCase : edgeCases) {
try {
@@ -363,10 +364,11 @@ public void testFailNumberEdgeCasesFromJSONSuite() {
}
/**
- * See http://seriot.ch/json/parsing.html and https://github.com/mmastrac/nanojson/issues/3.
+ * See http://seriot.ch/json/parsing.html and
+ * https://github.com/mmastrac/nanojson/issues/3.
*/
@Test
- public void testFailNumberEdgeCasesFromJSONSuiteNoArray() {
+ void failNumberEdgeCasesFromJSONSuiteNoArray() {
String[] edgeCases = { "-2.", "0.e1", "2.e+3", "2.e-3", "2.e3", "1." };
for (String edgeCase : edgeCases) {
try {
@@ -379,7 +381,7 @@ public void testFailNumberEdgeCasesFromJSONSuiteNoArray() {
}
@Test
- public void testFailBustedNumber1() {
+ void failBustedNumber1() {
try {
// There's no 'f' in double, but it treats it as a new token
JsonParser.object().from("123f");
@@ -390,7 +392,7 @@ public void testFailBustedNumber1() {
}
@Test
- public void testFailBustedNumber2() {
+ void failBustedNumber2() {
try {
// Badly formed number
JsonParser.object().from("-1-1");
@@ -401,7 +403,7 @@ public void testFailBustedNumber2() {
}
@Test
- public void testFailBustedString1() {
+ void failBustedString1() {
try {
// Missing " at end
JsonParser.object().from("\"abc");
@@ -412,7 +414,7 @@ public void testFailBustedString1() {
}
@Test
- public void testFailBustedString2() {
+ void failBustedString2() {
try {
// \n in middle of string
JsonParser.object().from("\"abc\n\"");
@@ -423,7 +425,7 @@ public void testFailBustedString2() {
}
@Test
- public void testFailBustedString3() {
+ void failBustedString3() {
try {
// Bad escape "\x" in middle of string
JsonParser.object().from("\"abc\\x\"");
@@ -434,7 +436,7 @@ public void testFailBustedString3() {
}
@Test
- public void testFailBustedString4() {
+ void failBustedString4() {
try {
// Bad escape "\\u123x" in middle of string
JsonParser.object().from("\"\\u123x\"");
@@ -445,7 +447,7 @@ public void testFailBustedString4() {
}
@Test
- public void testFailBustedString5() {
+ void failBustedString5() {
try {
// Incomplete unicode escape
JsonParser.object().from("\"\\u222\"");
@@ -456,7 +458,7 @@ public void testFailBustedString5() {
}
@Test
- public void testFailBustedString6() {
+ void failBustedString6() {
try {
// String that terminates halfway through a unicode escape
JsonParser.object().from("\"\\u222");
@@ -467,7 +469,7 @@ public void testFailBustedString6() {
}
@Test
- public void testFailBustedString7() {
+ void failBustedString7() {
try {
// String that terminates halfway through a regular escape
JsonParser.object().from("\"\\");
@@ -478,7 +480,7 @@ public void testFailBustedString7() {
}
@Test
- public void testFailArrayTrailingComma1() {
+ void failArrayTrailingComma1() {
try {
JsonParser.object().from("[,]");
fail();
@@ -488,7 +490,7 @@ public void testFailArrayTrailingComma1() {
}
@Test
- public void testFailArrayTrailingComma2() {
+ void failArrayTrailingComma2() {
try {
JsonParser.object().from("[1,]");
fail();
@@ -498,7 +500,7 @@ public void testFailArrayTrailingComma2() {
}
@Test
- public void testFailObjectTrailingComma1() {
+ void failObjectTrailingComma1() {
try {
JsonParser.object().from("{,}");
fail();
@@ -508,7 +510,7 @@ public void testFailObjectTrailingComma1() {
}
@Test
- public void testFailObjectTrailingComma2() {
+ void failObjectTrailingComma2() {
try {
JsonParser.object().from("{\"abc\":123,}");
fail();
@@ -518,7 +520,7 @@ public void testFailObjectTrailingComma2() {
}
@Test
- public void testFailObjectBadKey1() {
+ void failObjectBadKey1() {
try {
JsonParser.object().from("{true:1}");
fail();
@@ -528,7 +530,7 @@ public void testFailObjectBadKey1() {
}
@Test
- public void testFailObjectBadKey2() {
+ void failObjectBadKey2() {
try {
JsonParser.object().from("{2:1}");
fail();
@@ -538,7 +540,7 @@ public void testFailObjectBadKey2() {
}
@Test
- public void testFailObjectBadColon1() {
+ void failObjectBadColon1() {
try {
JsonParser.object().from("{\"abc\":}");
fail();
@@ -548,7 +550,7 @@ public void testFailObjectBadColon1() {
}
@Test
- public void testFailObjectBadColon2() {
+ void failObjectBadColon2() {
try {
JsonParser.object().from("{\"abc\":1:}");
fail();
@@ -558,7 +560,7 @@ public void testFailObjectBadColon2() {
}
@Test
- public void testFailObjectBadColon3() {
+ void failObjectBadColon3() {
try {
JsonParser.object().from("{:\"abc\":1}");
fail();
@@ -568,7 +570,7 @@ public void testFailObjectBadColon3() {
}
@Test
- public void testFailBadKeywords1() {
+ void failBadKeywords1() {
try {
JsonParser.object().from("truef");
fail();
@@ -578,7 +580,7 @@ public void testFailBadKeywords1() {
}
@Test
- public void testFailBadKeywords2() {
+ void failBadKeywords2() {
try {
JsonParser.object().from("true1");
fail();
@@ -588,7 +590,7 @@ public void testFailBadKeywords2() {
}
@Test
- public void testFailBadKeywords3() {
+ void failBadKeywords3() {
try {
JsonParser.object().from("tru");
fail();
@@ -598,7 +600,7 @@ public void testFailBadKeywords3() {
}
@Test
- public void testFailBadKeywords4() {
+ void failBadKeywords4() {
try {
JsonParser.object().from("[truef,true]");
fail();
@@ -608,7 +610,7 @@ public void testFailBadKeywords4() {
}
@Test
- public void testFailBadKeywords5() {
+ void failBadKeywords5() {
try {
JsonParser.object().from("grue");
fail();
@@ -618,7 +620,7 @@ public void testFailBadKeywords5() {
}
@Test
- public void testFailBadKeywords6() {
+ void failBadKeywords6() {
try {
JsonParser.object().from("trueeeeeeeeeeeeeeeeeeee");
fail();
@@ -628,7 +630,7 @@ public void testFailBadKeywords6() {
}
@Test
- public void testFailBadKeywords7() {
+ void failBadKeywords7() {
try {
JsonParser.object().from("g");
fail();
@@ -638,7 +640,7 @@ public void testFailBadKeywords7() {
}
@Test
- public void testFailTrailingCommaMultiline() {
+ void failTrailingCommaMultiline() {
String testString = "{\n\"abc\":123,\n\"def\":456,\n}";
try {
JsonParser.object().from(testString);
@@ -652,7 +654,7 @@ public void testFailTrailingCommaMultiline() {
* Ensures that we're correctly tracking UTF-8 character positions.
*/
@Test
- public void testFailTrailingCommaUTF8() {
+ void failTrailingCommaUTF8() {
ByteArrayInputStream in1 = new ByteArrayInputStream("{\n\"abc\":123,\"def\":456,}".getBytes(Charset
.forName("UTF-8")));
ByteArrayInputStream in2 = new ByteArrayInputStream(
@@ -676,58 +678,58 @@ public void testFailTrailingCommaUTF8() {
}
@Test
- public void testEncodingUTF8() throws JsonParserException {
+ void encodingUTF8() throws JsonParserException {
testEncoding(UTF8);
testEncodingBOM(UTF8);
}
@Test
- public void testEncodingUTF16LE() throws JsonParserException {
+ void encodingUTF16LE() throws JsonParserException {
Charset charset = Charset.forName("UTF-16LE");
testEncoding(charset);
testEncodingBOM(charset);
}
@Test
- public void testEncodingUTF16BE() throws JsonParserException {
+ void encodingUTF16BE() throws JsonParserException {
Charset charset = Charset.forName("UTF-16BE");
testEncoding(charset);
testEncodingBOM(charset);
}
@Test
- public void testEncodingUTF32LE() throws JsonParserException {
+ void encodingUTF32LE() throws JsonParserException {
Charset charset = Charset.forName("UTF-32LE");
testEncoding(charset);
testEncodingBOM(charset);
}
@Test
- public void testEncodingUTF32BE() throws JsonParserException {
+ void encodingUTF32BE() throws JsonParserException {
Charset charset = Charset.forName("UTF-32BE");
testEncoding(charset);
testEncodingBOM(charset);
}
@Test
- public void testValidUTF8Codepoint() throws JsonParserException {
+ void validUTF8Codepoint() throws JsonParserException {
assertEquals("\ud83d\ude8a",
- JsonParser.any().from(new ByteArrayInputStream("\"\ud83d\ude8a\"".getBytes(UTF8))));
+ JsonParser.any().from(new ByteArrayInputStream("\"\ud83d\ude8a\"".getBytes(UTF8))).toString());
}
@Test
- public void testValidUTF8Codepoint2() throws JsonParserException {
+ void validUTF8Codepoint2() throws JsonParserException {
assertEquals("\u2602",
- JsonParser.any().from(new ByteArrayInputStream("\"\u2602\"".getBytes(UTF8))));
+ JsonParser.any().from(new ByteArrayInputStream("\"\u2602\"".getBytes(UTF8))).toString());
}
@Test
- public void testIllegalUTF8Bytes() {
+ void illegalUTF8Bytes() {
// Test the always-illegal bytes
int[] failures = new int[] { 0xc0, 0xc1, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff };
for (int i = 0; i < failures.length; i++) {
try {
- JsonParser.object().from(new ByteArrayInputStream(new byte[] { '"', (byte)failures[i], '"' }));
+ JsonParser.object().from(new ByteArrayInputStream(new byte[] { '"', (byte) failures[i], '"' }));
} catch (JsonParserException e) {
testException(e, 1, 2, "UTF-8");
}
@@ -736,7 +738,7 @@ public void testIllegalUTF8Bytes() {
// Test the continuation bytes outside of a continuation
for (int i = 0x80; i <= 0xBF; i++) {
try {
- JsonParser.object().from(new ByteArrayInputStream(new byte[] { '"', (byte)i, '"' }));
+ JsonParser.object().from(new ByteArrayInputStream(new byte[] { '"', (byte) i, '"' }));
} catch (JsonParserException e) {
testException(e, 1, 2, "UTF-8");
}
@@ -744,10 +746,11 @@ public void testIllegalUTF8Bytes() {
}
/**
- * See http://seriot.ch/parsing_json.html and https://github.com/mmastrac/nanojson/issues/3.
+ * See http://seriot.ch/parsing_json.html and
+ * https://github.com/mmastrac/nanojson/issues/3.
*/
@Test
- public void testIllegalUTF8StringFromJSONSuite() {
+ void illegalUTF8StringFromJSONSuite() {
try {
JsonParser.object().from(new ByteArrayInputStream(new byte[] {
'"', (byte) 0xed, (byte) 0xa0, (byte) 0x80, '"' }));
@@ -774,7 +777,7 @@ private void testEncodingBOM(Charset charset) throws JsonParserException {
}
@Test
- public void failureTestsFromYui() throws IOException {
+ void failureTestsFromYui() throws IOException {
InputStream input = getClass().getClassLoader().getResourceAsStream("yui_fail_cases.txt");
String[] failCases = readAsUtf8(input).split("\n");
@@ -789,7 +792,7 @@ public void failureTestsFromYui() throws IOException {
}
@Test
- public void tortureTest() throws JsonParserException, IOException {
+ void tortureTest() throws JsonParserException, IOException {
InputStream input = getClass().getClassLoader().getResourceAsStream("sample.json");
JsonObject o = JsonParser.object().from(readAsUtf8(input));
assertNotNull(o.get("a"));
@@ -803,26 +806,26 @@ public void tortureTest() throws JsonParserException, IOException {
}
@Test
- public void tortureTestUrl() throws JsonParserException {
+ void tortureTestUrl() throws JsonParserException {
JsonObject o = JsonParser.object().from(getClass().getClassLoader().getResource("sample.json"));
assertNotNull(o.getObject("a").getArray("b\uecee\u8324\u007a\\\ue768.N"));
}
@Test
- public void tortureTestStream() throws JsonParserException {
+ void tortureTestStream() throws JsonParserException {
JsonObject o = JsonParser.object().from(getClass().getClassLoader().getResourceAsStream("sample.json"));
assertNotNull(o.getObject("a").getArray("b\uecee\u8324\u007a\\\ue768.N"));
}
@Test
- public void testIssue38() throws JsonParserException, IOException {
+ void issue38() throws JsonParserException, IOException {
// https://github.com/mmastrac/nanojson/issues/38
InputStream input = getClass().getClassLoader().getResourceAsStream("issue-38.json");
JsonParser.any().from(readAsUtf8(input));
}
@Test
- public void testEscapeSequencesAcrossBufferBoundary() throws JsonParserException {
+ void escapeSequencesAcrossBufferBoundary() throws JsonParserException {
String s1 = "";
String s2 = "";
@@ -839,7 +842,7 @@ public void testEscapeSequencesAcrossBufferBoundary() throws JsonParserException
}
@Test
- public void testFailTruncatedEscapeAcrossBufferBoundary() {
+ void failTruncatedEscapeAcrossBufferBoundary() {
String s1 = "\\u123";
String s2 = "";
for (int i = 0; i < 126; i++) {
@@ -856,18 +859,18 @@ public void testFailTruncatedEscapeAcrossBufferBoundary() {
JsonParser.object().from("\"" + s2 + s1);
fail();
} catch (JsonParserException e) {
- assertTrue(e.getMessage(), e.getMessage().contains("EOF"));
+ assertTrue(e.getMessage().contains("EOF"), e.getMessage());
}
}
}
/**
* Tests from json.org: http://www.json.org/JSON_checker/
- *
+ *
* Skips two tests that don't match reality (ie: Chrome).
*/
@Test
- public void jsonOrgTest() throws IOException {
+ void jsonOrgTest() throws IOException {
InputStream input = getClass().getClassLoader().getResourceAsStream("json_org_test.zip");
ZipInputStream zip = new ZipInputStream(input);
ZipEntry ze;
@@ -886,7 +889,7 @@ public void jsonOrgTest() throws IOException {
boolean positive = ze.getName().startsWith("test/pass");
int offset = 0;
- int size = (int)ze.getSize();
+ int size = (int) ze.getSize();
byte[] buffer = new byte[size];
while (size > 0) {
int r = zip.read(buffer, offset, buffer.length - offset);
@@ -931,14 +934,14 @@ private String readAsUtf8(InputStream input) throws IOException {
}
private void testException(JsonParserException e, int linePos, int charPos) {
- assertEquals(e.getMessage() + " incorrect location",
- "line " + linePos + " char " + charPos,
- "line " + e.getLinePosition() + " char " + e.getCharPosition());
+ assertEquals("line " + linePos + " char " + charPos,
+ "line " + e.getLinePosition() + " char " + e.getCharPosition(),
+ e.getMessage() + " incorrect location");
}
private void testException(JsonParserException e, int linePos, int charPos, String inError) {
assertEquals("line " + linePos + " char " + charPos,
"line " + e.getLinePosition() + " char " + e.getCharPosition());
- assertTrue("Error did not contain '" + inError + "': " + e.getMessage(), e.getMessage().contains(inError));
+ assertTrue(e.getMessage().contains(inError), "Error did not contain '" + inError + "': " + e.getMessage());
}
}
diff --git a/src/test/java/com/grack/nanojson/JsonReaderTest.java b/src/test/java/com/grack/nanojson/JsonReaderTest.java
index 5913d50..9812bcb 100644
--- a/src/test/java/com/grack/nanojson/JsonReaderTest.java
+++ b/src/test/java/com/grack/nanojson/JsonReaderTest.java
@@ -15,9 +15,9 @@
*/
package com.grack.nanojson;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -25,9 +25,9 @@
import java.io.InputStream;
import java.util.ArrayList;
-import org.junit.Test;
-
import com.grack.nanojson.Users.Friend;
+
+import org.junit.jupiter.api.Test;
import com.grack.nanojson.Users.User;
/**
@@ -39,7 +39,7 @@ public class JsonReaderTest {
* Read a simple object.
*/
@Test
- public void testObject() throws JsonParserException {
+ void object() throws JsonParserException {
JsonReader reader = JsonReader.from("{\"a\":1}");
assertEquals(JsonReader.Type.OBJECT, reader.current());
reader.object();
@@ -54,7 +54,7 @@ public void testObject() throws JsonParserException {
* Read a simple array.
*/
@Test
- public void testArray() throws JsonParserException {
+ void array() throws JsonParserException {
JsonReader reader = JsonReader.from("[\"a\",1,null]");
assertEquals(JsonReader.Type.ARRAY, reader.current());
reader.array();
@@ -72,12 +72,12 @@ public void testArray() throws JsonParserException {
assertFalse(reader.next());
}
-
+
/**
* Assert all the things.
*/
@Test
- public void testNestedDetailed() throws JsonParserException {
+ void nestedDetailed() throws JsonParserException {
String json = createNestedJson();
JsonReader reader = JsonReader.from(json);
@@ -126,13 +126,13 @@ public void testNestedDetailed() throws JsonParserException {
assertFalse(reader.next());
}
-
+
/**
- * Same test as {@link JsonReaderTest#testNestedDetailed()}, less assertions to get a better
+ * Same test as {@link JsonReaderTest#nestedDetailed()}, less assertions to get a better
* feel for the API.
*/
@Test
- public void testNestedLight() throws JsonParserException {
+ void nestedLight() throws JsonParserException {
String json = createNestedJson();
JsonReader reader = JsonReader.from(json);
@@ -176,7 +176,7 @@ public void testNestedLight() throws JsonParserException {
* Test reading an multiple arrays (including an empty one) in a object.
*/
@Test
- public void testArraysInObject() throws JsonParserException {
+ void arraysInObject() throws JsonParserException {
String json = createArraysInObject();
JsonReader reader = JsonReader.from(json);
@@ -213,7 +213,7 @@ private String createArraysInObject() {
* Test the {@link Users} class from java-json-benchmark.
*/
@Test
- public void testJsonBenchmarkUser() throws JsonParserException {
+ void jsonBenchmarkUser() throws JsonParserException {
JsonReader reader = JsonReader.from(getClass().getResourceAsStream("/users.json"));
parseUsers(reader);
diff --git a/src/test/java/com/grack/nanojson/JsonTypesTest.java b/src/test/java/com/grack/nanojson/JsonTypesTest.java
index 0e2a627..c2dbcf5 100644
--- a/src/test/java/com/grack/nanojson/JsonTypesTest.java
+++ b/src/test/java/com/grack/nanojson/JsonTypesTest.java
@@ -15,24 +15,26 @@
*/
package com.grack.nanojson;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigInteger;
import java.util.Arrays;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
/**
* Test for the various JSON types.
*/
-public class JsonTypesTest {
+class JsonTypesTest {
// CHECKSTYLE_OFF: MagicNumber
// CHECKSTYLE_OFF: JavadocMethod
@Test
- public void testObjectInt() {
+ void objectInt() {
JsonObject o = new JsonObject();
o.put("key", 1);
assertEquals(1, o.getInt("key"));
@@ -42,39 +44,39 @@ public void testObjectInt() {
assertEquals(1, o.getNumber("key"));
assertEquals(1, o.get("key"));
- assertEquals(null, o.getString("key"));
+ assertNull(o.getString("key"));
assertEquals("foo", o.getString("key", "foo"));
assertFalse(o.isNull("key"));
}
@Test
- public void testObjectString() {
+ void objectString() {
JsonObject o = new JsonObject();
o.put("key", "1");
assertEquals(0, o.getInt("key"));
assertEquals(0L, o.getLong("key"));
assertEquals(0, o.getDouble("key"), 0.0001f);
assertEquals(0f, o.getFloat("key"), 0.0001f);
- assertEquals(null, o.getNumber("key"));
+ assertNull(o.getNumber("key"));
assertEquals("1", o.get("key"));
assertFalse(o.isNull("key"));
}
@Test
- public void testObjectNull() {
+ void objectNull() {
JsonObject o = new JsonObject();
o.put("key", null);
assertEquals(0, o.getInt("key"));
assertEquals(0L, o.getLong("key"));
assertEquals(0, o.getDouble("key"), 0.0001f);
assertEquals(0f, o.getFloat("key"), 0.0001f);
- assertEquals(null, o.getNumber("key"));
- assertEquals(null, o.get("key"));
+ assertNull(o.getNumber("key"));
+ assertNull(o.get("key"));
assertTrue(o.isNull("key"));
}
@Test
- public void testArrayInt() {
+ void arrayInt() {
JsonArray o = new JsonArray(Arrays.asList((String) null, null, null,
null));
o.set(3, 1);
@@ -85,13 +87,13 @@ public void testArrayInt() {
assertEquals(1, o.getNumber(3));
assertEquals(1, o.get(3));
- assertEquals(null, o.getString(3));
+ assertNull(o.getString(3));
assertEquals("foo", o.getString(3, "foo"));
assertFalse(o.isNull(3));
}
@Test
- public void testArrayString() {
+ void arrayString() {
JsonArray o = new JsonArray(Arrays.asList((String) null, null, null,
null));
o.set(3, "1");
@@ -99,40 +101,40 @@ public void testArrayString() {
assertEquals(0L, o.getLong(3));
assertEquals(0, o.getDouble(3), 0.0001f);
assertEquals(0, o.getFloat(3), 0.0001f);
- assertEquals(null, o.getNumber(3));
+ assertNull(o.getNumber(3));
assertEquals("1", o.get(3));
assertFalse(o.isNull(3));
}
@Test
- public void testArrayNull() {
+ void arrayNull() {
JsonArray o = new JsonArray(Arrays.asList((String) null, null, null,
null));
o.set(3, null);
assertEquals(0, o.getInt(3));
assertEquals(0, o.getDouble(3), 0.0001f);
assertEquals(0, o.getFloat(3), 0.0001f);
- assertEquals(null, o.getNumber(3));
- assertEquals(null, o.get(3));
+ assertNull(o.getNumber(3));
+ assertNull(o.get(3));
assertTrue(o.isNull(3));
assertTrue(o.has(3));
}
@Test
- public void testArrayBounds() {
+ void arrayBounds() {
JsonArray o = new JsonArray(Arrays.asList((String) null, null, null,
null));
assertEquals(0, o.getInt(4));
assertEquals(0, o.getDouble(4), 0.0001f);
assertEquals(0, o.getFloat(4), 0.0001f);
- assertEquals(null, o.getNumber(4));
- assertEquals(null, o.get(4));
+ assertNull(o.getNumber(4));
+ assertNull(o.get(4));
assertFalse(o.isNull(4));
assertFalse(o.has(4));
}
@Test
- public void testJsonArrayBuilder() {
+ void jsonArrayBuilder() {
// @formatter:off
JsonArray a = JsonArray.builder().value(true).value(1.0).value(1.0f)
.value(1).value(new BigInteger("1234567890")).value("hi")
@@ -147,7 +149,7 @@ public void testJsonArrayBuilder() {
}
@Test
- public void testJsonObjectBuilder() {
+ void jsonObjectBuilder() {
// @formatter:off
JsonObject a = JsonObject
.builder()
@@ -185,23 +187,26 @@ public void testJsonObjectBuilder() {
}
}
- @Test(expected = JsonWriterException.class)
- public void testJsonArrayBuilderFailCantCloseRoot() {
- JsonArray.builder().end();
+ @Test
+ void jsonArrayBuilderFailCantCloseRoot() {
+ assertThrows(JsonWriterException.class, () ->
+ JsonArray.builder().end());
}
- @Test(expected = JsonWriterException.class)
- public void testJsonArrayBuilderFailCantAddKeyToArray() {
- JsonArray.builder().value("abc", 1);
+ @Test
+ void jsonArrayBuilderFailCantAddKeyToArray() {
+ assertThrows(JsonWriterException.class, () ->
+ JsonArray.builder().value("abc", 1));
}
- @Test(expected = JsonWriterException.class)
- public void testJsonArrayBuilderFailCantAddNonKeyToObject() {
- JsonObject.builder().value(1);
+ @Test
+ void jsonArrayBuilderFailCantAddNonKeyToObject() {
+ assertThrows(JsonWriterException.class, () ->
+ JsonObject.builder().value(1));
}
@Test
- public void testJsonKeyOrder() {
+ void jsonKeyOrder() {
JsonObject a = JsonObject
.builder()
.value("key01", 1)
diff --git a/src/test/java/com/grack/nanojson/JsonWriterTest.java b/src/test/java/com/grack/nanojson/JsonWriterTest.java
index 062750f..fe98906 100644
--- a/src/test/java/com/grack/nanojson/JsonWriterTest.java
+++ b/src/test/java/com/grack/nanojson/JsonWriterTest.java
@@ -15,9 +15,7 @@
*/
package com.grack.nanojson;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
@@ -25,332 +23,396 @@
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
-import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* Test for {@link JsonWriter}.
*/
-public class JsonWriterTest {
- private static final Charset UTF8 = StandardCharsets.UTF_8;
-
- // CHECKSTYLE_OFF: MagicNumber
- // CHECKSTYLE_OFF: JavadocMethod
- // CHECKSTYLE_OFF: EmptyBlock
- /**
- * Test emitting simple values.
- */
- @Test
- public void testSimpleValues() {
- assertEquals("true", JsonWriter.string().value(true).done());
- assertEquals("null", JsonWriter.string().nul().done());
- assertEquals("1.0", JsonWriter.string().value(1.0).done());
- assertEquals("1.0", JsonWriter.string().value(1.0f).done());
- assertEquals("1", JsonWriter.string().value(1).done());
- assertEquals("\"abc\"", JsonWriter.string().value("abc").done());
- }
-
- /**
- * Write progressively longer strings to see if we can tickle a boundary
- * exception.
- */
- @Test
- public void testStreamWriterWithNonBMPStringAroundBufferSize() throws JsonParserException {
- char[] c = new char[JsonWriterBase.BUFFER_SIZE - 128];
- Arrays.fill(c, ' ');
- String base = new String(c);
- for (int i = 0; i < 256; i++) {
- base += " ";
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- String s = base + new String(new int[] { 0x10ffff }, 0, 1);
- JsonWriter.on(bytes).value(s).done();
- assertEquals(s, JsonParser.any().from(new String(bytes.toByteArray(), UTF8)));
- }
- }
-
- /**
- * Write progressively longer strings to see if we can tickle a boundary
- * exception.
- */
- @Test
- public void testStreamWriterWithBMPStringAroundBufferSize() throws JsonParserException {
- char[] c = new char[JsonWriterBase.BUFFER_SIZE - 128];
- Arrays.fill(c, ' ');
- String base = new String(c);
- for (int i = 0; i < 256; i++) {
- base += " ";
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- String s = base + new String(new int[] { 0xffff }, 0, 1);
- JsonWriter.on(bytes).value(s).done();
- assertEquals(s, JsonParser.any().from(new String(bytes.toByteArray(), UTF8)));
- }
- }
-
- /**
- * Write progressively longer string + array to see if we can tickle a
- * boundary exception.
- */
- @Test
- public void testStreamWriterWithArrayAroundBufferSize() throws JsonParserException {
- char[] c = new char[JsonWriterBase.BUFFER_SIZE - 128];
- Arrays.fill(c, ' ');
- String base = new String(c);
- for (int i = 0; i < 256; i++) {
- base += " ";
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- String s = base + new String(new int[] { 0x10ffff }, 0, 1);
- JsonWriter.on(bytes).array().value(s).nul().end().done();
- String s2 = new String(bytes.toByteArray(), UTF8);
- JsonArray array = JsonParser.array().from(s2);
- assertEquals(s, array.get(0));
- assertEquals(null, array.get(1));
- }
- }
-
- /**
- * Test various ways of writing null, as well as various situations.
- */
- @Test
- public void testNull() {
- assertEquals("null", JsonWriter.string().value((String) null).done());
- assertEquals("null", JsonWriter.string().value((Number) null).done());
- assertEquals("null", JsonWriter.string().nul().done());
- assertEquals("[null]", JsonWriter.string().array().value((String) null)
- .end().done());
- assertEquals("[null]", JsonWriter.string().array().value((Number) null)
- .end().done());
- assertEquals("[null]", JsonWriter.string().array().nul().end().done());
- assertEquals("{\"a\":null}",
- JsonWriter.string().object().value("a", (String) null).end()
- .done());
- assertEquals("{\"a\":null}",
- JsonWriter.string().object().value("a", (Number) null).end()
- .done());
- assertEquals("{\"a\":null}", JsonWriter.string().object().nul("a")
- .end().done());
- }
-
- /**
- * Test escaping of chars < 256.
- */
- @Test
- public void testStringControlCharacters() {
- StringBuilder chars = new StringBuilder();
- for (int i = 0; i < 0xa0; i++)
- chars.append((char) i);
- chars.append("\u20ff");
-
- assertEquals(
- "\"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010"
- + "\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d"
- + "\\u001e\\u001f !\\\"#$%&'()*+,-./0123456789:;<=>?@"
- + "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\u0080\\u0081\\u0082"
- + "\\u0083\\u0084\\u0085\\u0086\\u0087\\u0088\\u0089\\u008a\\u008b\\u008c\\u008d\\u008e\\u008f"
- + "\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097\\u0098\\u0099\\u009a\\u009b\\u009c"
- + "\\u009d\\u009e\\u009f\\u20ff\"",
- JsonWriter.string(chars.toString()));
- }
-
- /**
- * Test escaping of chars < 256.
- */
- @Test
- public void testEscape() {
- StringBuilder chars = new StringBuilder();
- for (int i = 0; i < 0xa0; i++)
- chars.append((char) i);
-
- assertEquals(
- "\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010"
- + "\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d"
- + "\\u001e\\u001f !\\\"#$%&'()*+,-./0123456789:;<=>?@"
- + "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\u0080\\u0081\\u0082"
- + "\\u0083\\u0084\\u0085\\u0086\\u0087\\u0088\\u0089\\u008a\\u008b\\u008c\\u008d\\u008e\\u008f"
- + "\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097\\u0098\\u0099\\u009a\\u009b\\u009c"
- + "\\u009d\\u009e\\u009f",
- JsonWriter.escape(chars.toString()));
- }
-
- /**
- * Torture test for UTF8 character encoding.
- */
- @Test
- public void testBMPCharacters() throws Exception {
- StringBuilder builder = new StringBuilder();
- for (int i = 0; i < 0xD000; i++) {
- builder.append((char)i);
- }
- builder.append("\ue000");
- builder.append("\uefff");
- builder.append("\uf000");
- builder.append("\uffff");
-
- // Base string
- String s = JsonWriter.string(builder.toString());
- assertEquals(builder.toString(), (String)JsonParser.any().from(s));
-
- // Ensure that it also matches the PrintStream output
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- JsonWriter.on(new PrintStream(bytes, false, "UTF-8")).value(builder.toString()).done();
- assertEquals(builder.toString(), (String)JsonParser.any().from(new String(bytes.toByteArray(),
- UTF8)));
-
- // Ensure that it also matches the stream output
- bytes = new ByteArrayOutputStream();
- JsonWriter.on(bytes).value(builder.toString()).done();
- assertEquals(builder.toString(), (String)JsonParser.any().from(new String(bytes.toByteArray(),
- UTF8)));
- }
-
- /**
- * Torture test for UTF8 character encoding outside the basic multilingual plane.
- */
- @Test
- public void testNonBMP() throws Exception {
- StringBuilder builder = new StringBuilder();
- builder.appendCodePoint(0x10000); // Start of non-BMP
- builder.appendCodePoint(0x1f601); // GRINNING FACE WITH SMILING EYES
- builder.appendCodePoint(0x10ffff); // Character.MAX_CODE_POINT
-
- // Base string
- String s = JsonWriter.string(builder.toString());
- assertEquals(builder.toString(), (String)JsonParser.any().from(s));
-
- // Ensure that it also matches the PrintStream output
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- JsonWriter.on(new PrintStream(bytes, false, "UTF-8")).value(builder.toString()).done();
- assertEquals(builder.toString(), (String)JsonParser.any().from(new String(bytes.toByteArray(),
- UTF8)));
-
- // Ensure that it also matches the stream output
- bytes = new ByteArrayOutputStream();
- JsonWriter.on(bytes).value(builder.toString()).done();
- assertEquals(builder.toString(), (String)JsonParser.any().from(new String(bytes.toByteArray(),
- UTF8)));
- }
-
- /**
- * Basic {@link OutputStream} smoke test.
- */
- @Test
- public void testWriteToUTF8Stream() {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- JsonWriter.on(bytes).object().value("a\n", 1)
- .value("b", 2).end().done();
- assertEquals("{\"a\\n\":1,\"b\":2}", new String(bytes.toByteArray(),
- UTF8));
- }
-
- /**
- * Basic {@link PrintStream} smoke test.
- */
- @Test
- public void testWriteToSystemOutLikeStream() throws Exception {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- JsonWriter.on(new PrintStream(bytes, false, "UTF-8")).object().value("a\n", 1)
- .value("b", 2).end().done();
-
- assertEquals("{\"a\\n\":1,\"b\":2}", new String(bytes.toByteArray(),
- UTF8));
- }
-
- /**
- * Test escaping of / when following < to handle </script>.
- */
- @Test
- public void testScriptEndEscaping() {
- assertEquals("\"<\\/script>\"", JsonWriter.string(""));
- assertEquals("\"/script\"", JsonWriter.string("/script"));
- }
-
- /**
- * Test a simple array.
- */
- @Test
- public void testArray() {
- String json = JsonWriter.string().array().value(true).value(false)
- .value(true).end().done();
- assertEquals("[true,false,true]", json);
- }
-
- /**
- * Test an empty array.
- */
- @Test
- public void testArrayEmpty() {
- String json = JsonWriter.string().array().end().done();
- assertEquals("[]", json);
- }
-
- /**
- * Test an array of empty arrays.
- */
- @Test
- public void testArrayOfEmpty() {
- String json = JsonWriter.string().array().array().end().array().end()
- .end().done();
- assertEquals("[[],[]]", json);
- }
-
- /**
- * Test a nested array.
- */
- @Test
- public void testNestedArray() {
- String json = JsonWriter.string().array().array().array().value(true)
- .value(false).value(true).end().end().end().done();
- assertEquals("[[[true,false,true]]]", json);
- }
-
- /**
- * Test a nested array.
- */
- @Test
- public void testNestedArray2() {
- String json = JsonWriter.string().array().value(true).array().array()
- .value(false).end().end().value(true).end().done();
- assertEquals("[true,[[false]],true]", json);
- }
-
- /**
- * Test a simple object.
- */
- @Test
- public void testObject() {
- String json = JsonWriter.string().object().value("a", true)
- .value("b", false).value("c", true).end().done();
- assertEquals("{\"a\":true,\"b\":false,\"c\":true}", json);
- }
-
- /**
- * Test a simple object with indent.
- */
- @Test
- public void testObjectIndent() {
- String json = JsonWriter.indent(" ").string().object()
- .value("a", true).value("b", false).value("c", true).end()
- .done();
- assertEquals("{\n \"a\":true,\n \"b\":false,\n \"c\":true\n}", json);
- }
-
- /**
- * Test a nested object.
- */
- @Test
- public void testNestedObject() {
- String json = JsonWriter.string().object().object("a")
- .value("b", false).value("c", true).end().end().done();
- assertEquals("{\"a\":{\"b\":false,\"c\":true}}", json);
- }
-
- /**
- * Test a nested object and array.
- */
- @Test
- public void testNestedObjectArray() {
- //@formatter:off
+class JsonWriterTest {
+ private static final Charset UTF8 = StandardCharsets.UTF_8;
+
+ // CHECKSTYLE_OFF: MagicNumber
+ // CHECKSTYLE_OFF: JavadocMethod
+ // CHECKSTYLE_OFF: EmptyBlock
+
+ /**
+ * Test emitting simple values.
+ */
+ @Test
+ void simpleValues() {
+ assertEquals("true", JsonWriter.string().value(true).done());
+ assertEquals("null", JsonWriter.string().nul().done());
+ assertEquals("1.0", JsonWriter.string().value(1.0).done());
+ assertEquals("1.0", JsonWriter.string().value(1.0f).done());
+ assertEquals("1", JsonWriter.string().value(1).done());
+ assertEquals("\"abc\"", JsonWriter.string().value("abc").done());
+ }
+
+ /**
+ * Write progressively longer strings to see if we can tickle a boundary
+ * exception.
+ */
+ @Test
+ void streamWriterWithNonBMPStringAroundBufferSize() throws JsonParserException {
+ char[] c = new char[JsonWriterBase.BUFFER_SIZE - 128];
+ Arrays.fill(c, ' ');
+ StringBuilder base = new StringBuilder(new String(c));
+ for (int i = 0; i < 256; i++) {
+ base.append(" ");
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ String s = base + new String(new int[]{0x10ffff}, 0, 1);
+ JsonWriter.on(bytes).value(s).done();
+ assertEquals(s, ((LazyString) JsonParser.any().from(new String(bytes.toByteArray(), UTF8))).toString());
+ }
+ }
+
+ /**
+ * Write progressively longer strings to see if we can tickle a boundary
+ * exception.
+ */
+ @Test
+ void streamWriterWithBMPStringAroundBufferSize() throws JsonParserException {
+ char[] c = new char[JsonWriterBase.BUFFER_SIZE - 128];
+ Arrays.fill(c, ' ');
+ String base = new String(c);
+ for (int i = 0; i < 256; i++) {
+ base += " ";
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ String s = base + new String(new int[]{0xffff}, 0, 1);
+ JsonWriter.on(bytes).value(s).done();
+ Object parsed = JsonParser.any().from(new String(bytes.toByteArray(), UTF8));
+ assertEquals(s, parsed instanceof LazyString ? parsed.toString() : parsed);
+ }
+ }
+
+ /**
+ * Write progressively longer string + array to see if we can tickle a
+ * boundary exception.
+ */
+ @Test
+ void streamWriterWithArrayAroundBufferSize() throws JsonParserException {
+ char[] c = new char[JsonWriterBase.BUFFER_SIZE - 128];
+ Arrays.fill(c, ' ');
+ String base = new String(c);
+ for (int i = 0; i < 256; i++) {
+ base += " ";
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ String s = base + new String(new int[]{0x10ffff}, 0, 1);
+ JsonWriter.on(bytes).array().value(s).nul().end().done();
+ String s2 = new String(bytes.toByteArray(), UTF8);
+ JsonArray array = JsonParser.array().from(s2);
+ Object first = array.get(0);
+ assertEquals(s, first instanceof LazyString ? first.toString() : first);
+ assertNull(array.get(1));
+ }
+ }
+
+ /**
+ * Test various ways of writing null, as well as various situations.
+ */
+ @Test
+ void testNull() {
+ assertEquals("null", JsonWriter.string().value((String) null).done());
+ assertEquals("null", JsonWriter.string().value((Number) null).done());
+ assertEquals("null", JsonWriter.string().nul().done());
+ assertEquals("[null]", JsonWriter.string().array().value((String) null)
+ .end().done());
+ assertEquals("[null]", JsonWriter.string().array().value((Number) null)
+ .end().done());
+ assertEquals("[null]", JsonWriter.string().array().nul().end().done());
+ assertEquals("{\"a\":null}",
+ JsonWriter.string().object().value("a", (String) null).end()
+ .done());
+ assertEquals("{\"a\":null}",
+ JsonWriter.string().object().value("a", (Number) null).end()
+ .done());
+ assertEquals("{\"a\":null}", JsonWriter.string().object().nul("a")
+ .end().done());
+ }
+
+ @Test
+ void separateKeyWriting() {
+ assertEquals("{\"a\":null}",
+ JsonWriter.string().object().key("a").value((Number) null).end()
+ .done());
+ assertEquals("{\"a\":{\"b\":null}}",
+ JsonWriter.string().object().key("a").object().value("b", (Number) null)
+ .end().end().done());
+ }
+
+ /**
+ * Test escaping of chars < 256.
+ */
+ @Test
+ void stringControlCharacters() {
+ StringBuilder chars = new StringBuilder();
+ for (int i = 0; i < 0xa0; i++)
+ chars.append((char) i);
+ chars.append("\u20ff");
+
+ assertEquals(
+ "\"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010"
+ + "\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d"
+ + "\\u001e\\u001f !\\\"#$%&'()*+,-./0123456789:;<=>?@"
+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\u0080\\u0081\\u0082"
+ + "\\u0083\\u0084\\u0085\\u0086\\u0087\\u0088\\u0089\\u008a\\u008b\\u008c\\u008d\\u008e\\u008f"
+ + "\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097\\u0098\\u0099\\u009a\\u009b\\u009c"
+ + "\\u009d\\u009e\\u009f\\u20ff\"",
+ JsonWriter.string(chars.toString()));
+ }
+
+ /**
+ * Test escaping of chars < 256.
+ */
+ @Test
+ void escape() {
+ StringBuilder chars = new StringBuilder();
+ for (int i = 0; i < 0xa0; i++)
+ chars.append((char) i);
+
+ assertEquals(
+ "\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010"
+ + "\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d"
+ + "\\u001e\\u001f !\\\"#$%&'()*+,-./0123456789:;<=>?@"
+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\u0080\\u0081\\u0082"
+ + "\\u0083\\u0084\\u0085\\u0086\\u0087\\u0088\\u0089\\u008a\\u008b\\u008c\\u008d\\u008e\\u008f"
+ + "\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097\\u0098\\u0099\\u009a\\u009b\\u009c"
+ + "\\u009d\\u009e\\u009f",
+ JsonWriter.escape(chars.toString()));
+ }
+
+ /**
+ * Torture test for UTF8 character encoding.
+ */
+ @Test
+ void bmpCharacters() throws Exception {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < 0xD000; i++) {
+ builder.append((char) i);
+ }
+ builder.append("\ue000");
+ builder.append("\uefff");
+ builder.append("\uf000");
+ builder.append("\uffff");
+
+ // Base string
+ String s = JsonWriter.string(builder.toString());
+ Object parsed = JsonParser.any().from(s);
+ assertEquals(builder.toString(), parsed instanceof LazyString ? parsed.toString() : parsed);
+
+ // Ensure that it also matches the PrintStream output
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JsonWriter.on(new PrintStream(bytes, false, "UTF-8")).value(builder.toString()).done();
+ parsed = JsonParser.any().from(new String(bytes.toByteArray(), UTF8));
+ assertEquals(builder.toString(), parsed instanceof LazyString ? parsed.toString() : parsed);
+
+ // Ensure that it also matches the stream output
+ bytes = new ByteArrayOutputStream();
+ JsonWriter.on(bytes).value(builder.toString()).done();
+ parsed = JsonParser.any().from(new String(bytes.toByteArray(), UTF8));
+ assertEquals(builder.toString(), parsed instanceof LazyString ? parsed.toString() : parsed);
+ }
+
+ /**
+ * Torture test for UTF8 character encoding outside the basic multilingual
+ * plane.
+ */
+ @Test
+ void nonBMP() throws Exception {
+ StringBuilder builder = new StringBuilder();
+ builder.appendCodePoint(0x10000); // Start of non-BMP
+ builder.appendCodePoint(0x1f601); // GRINNING FACE WITH SMILING EYES
+ builder.appendCodePoint(0x10ffff); // Character.MAX_CODE_POINT
+
+ // Base string
+ String s = JsonWriter.string(builder.toString());
+ assertEquals(builder.toString(), ((LazyString) JsonParser.any().from(s)).toString());
+
+ // Ensure that it also matches the PrintStream output
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JsonWriter.on(new PrintStream(bytes, false, "UTF-8")).value(builder.toString()).done();
+ assertEquals(builder.toString(), ((LazyString) JsonParser.any().from(new String(bytes.toByteArray(),
+ UTF8))).toString());
+
+ // Ensure that it also matches the stream output
+ bytes = new ByteArrayOutputStream();
+ JsonWriter.on(bytes).value(builder.toString()).done();
+ assertEquals(builder.toString(), ((LazyString) JsonParser.any().from(new String(bytes.toByteArray(),
+ UTF8))).toString());
+ }
+
+ /**
+ * Basic {@link OutputStream} smoke test.
+ */
+ @Test
+ void writeToUTF8Stream() {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JsonWriter.on(bytes).object().value("a\n", 1)
+ .value("b", 2).end().done();
+ assertEquals("{\"a\\n\":1,\"b\":2}", new String(bytes.toByteArray(),
+ UTF8));
+ }
+
+ /**
+ * Basic {@link PrintStream} smoke test.
+ */
+ @Test
+ void writeToSystemOutLikeStream() throws Exception {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ JsonWriter.on(new PrintStream(bytes, false, "UTF-8")).object().value("a\n", 1)
+ .value("b", 2).end().done();
+
+ assertEquals("{\"a\\n\":1,\"b\":2}", new String(bytes.toByteArray(),
+ UTF8));
+ }
+
+ /**
+ * Test escaping of / when following < to handle </script>.
+ */
+ @Test
+ void scriptEndEscaping() {
+ assertEquals("\"<\\/script>\"", JsonWriter.string(""));
+ assertEquals("\"/script\"", JsonWriter.string("/script"));
+ }
+
+ /**
+ * Test a simple array.
+ */
+ @Test
+ void array() {
+ String json = JsonWriter.string().array().value(true).value(false)
+ .value(true).end().done();
+ assertEquals("[true,false,true]", json);
+ }
+
+ /**
+ * Test an empty array.
+ */
+ @Test
+ void arrayEmpty() {
+ String json = JsonWriter.string().array().end().done();
+ assertEquals("[]", json);
+ }
+
+ /**
+ * Test the auto-conversion of Writables.
+ */
+ @Test
+ void writable() {
+ assertEquals("null", JsonWriter.string((JsonConvertible) () -> null));
+ assertEquals("[]", JsonWriter.string((JsonConvertible) ArrayList::new));
+ assertEquals("{}", JsonWriter.string((JsonConvertible) HashMap::new));
+ assertEquals("\"\"", JsonWriter.string((JsonConvertible) () -> ""));
+ assertEquals("1", JsonWriter.string((JsonConvertible) () -> Integer.valueOf(1)));
+ assertEquals("1.0", JsonWriter.string((JsonConvertible) () -> Double.valueOf(1.0)));
+ assertEquals("1", JsonWriter.string((JsonConvertible) () -> Long.valueOf(1)));
+ assertEquals("1.0", JsonWriter.string((JsonConvertible) () -> Float.valueOf(1.0f)));
+ assertEquals(
+ "[null,[1,2,3],{\"a\":1,\"b\":2.0,\"c\":\"a\",\"d\":null,\"e\":[]}]",
+ JsonWriter.string((JsonConvertible) () -> (JsonConvertible) () -> {
+ ArrayList list = new ArrayList<>();
+ list.add(null);
+ list.add((JsonConvertible) () -> new int[]{1, 2, 3});
+ list.add((JsonConvertible) () -> {
+ HashMap map = new HashMap<>();
+ map.put("a", 1);
+ map.put("b", 2.0);
+ map.put("c", "a");
+ map.put("d", null);
+ map.put("e", (JsonConvertible) ArrayList::new);
+ return map;
+ });
+ return list;
+ }));
+ assertEquals(
+ "Unable to handle type: class java.lang.Object",
+ assertThrows(
+ JsonWriterException.class,
+ () -> JsonWriter.string((JsonConvertible) Object::new)).getMessage());
+ assertEquals(
+ "Unable to handle type: class java.lang.Object",
+ assertThrows(
+ JsonWriterException.class,
+ () -> JsonWriter.string((JsonConvertible) () -> Arrays.asList("d", 1, new Object())))
+ .getMessage());
+ }
+
+ /**
+ * Test an array of empty arrays.
+ */
+ @Test
+ void arrayOfEmpty() {
+ String json = JsonWriter.string().array().array().end().array().end()
+ .end().done();
+ assertEquals("[[],[]]", json);
+ }
+
+ /**
+ * Test a nested array.
+ */
+ @Test
+ void nestedArray() {
+ String json = JsonWriter.string().array().array().array().value(true)
+ .value(false).value(true).end().end().end().done();
+ assertEquals("[[[true,false,true]]]", json);
+ }
+
+ /**
+ * Test a nested array.
+ */
+ @Test
+ void nestedArray2() {
+ String json = JsonWriter.string().array().value(true).array().array()
+ .value(false).end().end().value(true).end().done();
+ assertEquals("[true,[[false]],true]", json);
+ }
+
+ /**
+ * Test a simple object.
+ */
+ @Test
+ void object() {
+ String json = JsonWriter.string().object().value("a", true)
+ .value("b", false).value("c", true).end().done();
+ assertEquals("{\"a\":true,\"b\":false,\"c\":true}", json);
+ }
+
+ /**
+ * Test a simple object with indent.
+ */
+ @Test
+ void objectIndent() {
+ String json = JsonWriter.indent(" ").string().object()
+ .value("a", true).value("b", false).value("c", true).end()
+ .done();
+ assertEquals("{\n \"a\":true,\n \"b\":false,\n \"c\":true\n}", json);
+ }
+
+ /**
+ * Test a nested object.
+ */
+ @Test
+ void nestedObject() {
+ String json = JsonWriter.string().object().object("a")
+ .value("b", false).value("c", true).end().end().done();
+ assertEquals("{\"a\":{\"b\":false,\"c\":true}}", json);
+ }
+
+ /**
+ * Test a nested object and array.
+ */
+ @Test
+ void nestedObjectArray() {
+ //@formatter:off
String json = JsonWriter.string()
.object()
.object("a")
@@ -369,17 +431,17 @@ public void testNestedObjectArray() {
.end()
.done();
//@formatter:on
- assertEquals(
- "{\"a\":{\"b\":[{\"a\":1,\"b\":2},{\"c\":1.0,\"d\":2.0}],\"c\":[\"a\",\"b\",\"c\"]}}",
- json);
- }
-
- /**
- * Test a nested object and array.
- */
- @Test
- public void testNestedObjectArrayIndent() {
- //@formatter:off
+ assertEquals(
+ "{\"a\":{\"b\":[{\"a\":1,\"b\":2},{\"c\":1.0,\"d\":2.0}],\"c\":[\"a\",\"b\",\"c\"]}}",
+ json);
+ }
+
+ /**
+ * Test a nested object and array.
+ */
+ @Test
+ void nestedObjectArrayIndent() {
+ //@formatter:off
String json = JsonWriter.indent(" ").string()
.object()
.object("a")
@@ -399,199 +461,210 @@ public void testNestedObjectArrayIndent() {
.done();
//@formatter:on
- assertEquals(
- "{\n \"a\":{\n \"b\":[{\n \"a\":1,\n \"b\":2\n },{\n"
- + " \"c\":1.0,\n \"d\":2.0\n }],\n"
- + " \"c\":[\"a\",\"b\",\"c\"]\n }\n}", json);
- }
-
- /**
- * Tests the {@link Appendable} code.
- */
- @Test
- public void testAppendable() {
- StringWriter writer = new StringWriter();
- JsonWriter.on(writer).object().value("abc", "def").end().done();
- assertEquals("{\"abc\":\"def\"}", writer.toString());
- }
-
- /**
- * Tests the {@link OutputStream} code.
- */
- @Test
- public void testOutputStream() {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- JsonWriter.on(out).object().value("abc", "def").end().done();
- assertEquals("{\"abc\":\"def\"}",
- new String(out.toByteArray(), UTF8));
- }
-
- @Test
- public void testQuickJson() {
- assertEquals("true", JsonWriter.string(true));
- }
-
- @Test
- public void testQuickJsonArray() {
- assertEquals("[1,2,3]", JsonWriter.string(JsonArray.from(1, 2, 3)));
- }
-
- @Test
- public void testQuickArray() {
- assertEquals("[1,2,3]", JsonWriter.string(Arrays.asList(1, 2, 3)));
- }
-
- @Test
- public void testQuickArrayEmpty() {
- assertEquals("[]", JsonWriter.string(Collections.emptyList()));
- }
-
- @Test
- public void testQuickObjectArray() {
- assertEquals("[1,2,3]", JsonWriter.string(new Object[] { 1, 2, 3 }));
- }
-
- @Test
- public void testQuickObjectArrayNested() {
- assertEquals(
- "[[1,2],[[3]]]",
- JsonWriter.string(new Object[] { new Object[] { 1, 2 },
- new Object[] { new Object[] { 3 } } }));
- }
-
- @Test
- public void testQuickObjectArrayEmpty() {
- assertEquals("[]", JsonWriter.string(new Object[0]));
- }
-
- @Test
- public void testObjectArrayInMap() {
- JsonObject o = new JsonObject();
- o.put("array of string", new String[] { "a", "b", "c" });
- o.put("array of Boolean", new Boolean[] { true, false });
- o.put("array of int", new int[] { 1, 2, 3 });
- o.put("array of JsonObject",
- new JsonObject[] { new JsonObject(), null });
-
- String[] bits = { "\"array of JsonObject\":[{},null]",
- "\"array of Boolean\":[true,false]",
- "\"array of string\":[\"a\",\"b\",\"c\"]",
- "\"array of int\":[1,2,3]" };
- String s = JsonWriter.string(o);
- for (String bit : bits) {
- assertTrue("Didn't contain " + bit, s.contains(bit));
- }
- }
-
- @Test
- public void testFailureNoKeyInObject() {
- try {
- JsonWriter.string().object().value(true).end().done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureNoKeyInObject2() {
- try {
- JsonWriter.string().object().value("a", 1).value(true).end().done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureKeyInArray() {
- try {
- JsonWriter.string().array().value("x", true).end().done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureKeyInArray2() {
- try {
- JsonWriter.string().array().value(1).value("x", true).end().done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureNotFullyClosed() {
- try {
- JsonWriter.string().array().value(1).done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureNotFullyClosed2() {
- try {
- JsonWriter.string().array().done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureEmpty() {
- try {
- JsonWriter.string().done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureEmpty2() {
- try {
- JsonWriter.string().end();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureMoreThanOneRoot() {
- try {
- JsonWriter.string().value(1).value(1).done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureMoreThanOneRoot2() {
- try {
- JsonWriter.string().array().value(1).end().value(1).done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
-
- @Test
- public void testFailureMoreThanOneRoot3() {
- try {
- JsonWriter.string().array().value(1).end().array().value(1).end()
- .done();
- fail();
- } catch (JsonWriterException e) {
- // OK
- }
- }
+ assertEquals(
+ "{\n \"a\":{\n \"b\":[{\n \"a\":1,\n \"b\":2\n },{\n"
+ + " \"c\":1.0,\n \"d\":2.0\n }],\n"
+ + " \"c\":[\"a\",\"b\",\"c\"]\n }\n}",
+ json);
+ }
+
+ /**
+ * Tests the {@link Appendable} code.
+ */
+ @Test
+ void appendable() {
+ StringWriter writer = new StringWriter();
+ JsonWriter.on(writer).object().value("abc", "def").end().done();
+ assertEquals("{\"abc\":\"def\"}", writer.toString());
+ }
+
+ /**
+ * Tests the {@link OutputStream} code.
+ */
+ @Test
+ void outputStream() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ JsonWriter.on(out).object().value("abc", "def").end().done();
+ assertEquals("{\"abc\":\"def\"}",
+ new String(out.toByteArray(), UTF8));
+ }
+
+ @Test
+ void quickJson() {
+ assertEquals("true", JsonWriter.string(true));
+ }
+
+ @Test
+ void quickJsonArray() {
+ assertEquals("[1,2,3]", JsonWriter.string(JsonArray.from(1, 2, 3)));
+ }
+
+ @Test
+ void quickArray() {
+ assertEquals("[1,2,3]", JsonWriter.string(Arrays.asList(1, 2, 3)));
+ }
+
+ @Test
+ void quickArrayEmpty() {
+ assertEquals("[]", JsonWriter.string(Collections.emptyList()));
+ }
+
+ @Test
+ void quickObjectArray() {
+ assertEquals("[1,2,3]", JsonWriter.string(new Object[]{1, 2, 3}));
+ }
+
+ @Test
+ void quickObjectArrayNested() {
+ assertEquals(
+ "[[1,2],[[3]]]",
+ JsonWriter.string(new Object[]{new Object[]{1, 2},
+ new Object[]{new Object[]{3}}}));
+ }
+
+ @Test
+ void quickObjectArrayEmpty() {
+ assertEquals("[]", JsonWriter.string(new Object[0]));
+ }
+
+ @Test
+ void objectArrayInMap() {
+ JsonObject o = new JsonObject();
+ o.put("array of string", new String[]{"a", "b", "c"});
+ o.put("array of Boolean", new Boolean[]{true, false});
+ o.put("array of int", new int[]{1, 2, 3});
+ o.put("array of JsonObject",
+ new JsonObject[]{new JsonObject(), null});
+
+ String[] bits = {"\"array of JsonObject\":[{},null]",
+ "\"array of Boolean\":[true,false]",
+ "\"array of string\":[\"a\",\"b\",\"c\"]",
+ "\"array of int\":[1,2,3]"};
+ String s = JsonWriter.string(o);
+ for (String bit : bits) {
+ assertTrue(s.contains(bit), "Didn't contain " + bit);
+ }
+ }
+
+ @Test
+ void failureNoKeyInObject() {
+ try {
+ JsonWriter.string().object().value(true).end().done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureNoKeyInObject2() {
+ try {
+ JsonWriter.string().object().value("a", 1).value(true).end().done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureKeyInArray() {
+ try {
+ JsonWriter.string().array().value("x", true).end().done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureKeyInArray2() {
+ try {
+ JsonWriter.string().array().value(1).value("x", true).end().done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureRepeatedKey() {
+ assertThrows(JsonWriterException.class, () -> JsonWriter.string().object().key("a").value("b", 2).end().done());
+ }
+
+ @Test
+ void failureRepeatedKey2() {
+ assertThrows(JsonWriterException.class, () -> JsonWriter.string().object().key("a").key("b").end().done());
+ }
+
+ @Test
+ void failureNotFullyClosed() {
+ try {
+ JsonWriter.string().array().value(1).done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureNotFullyClosed2() {
+ try {
+ JsonWriter.string().array().done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureEmpty() {
+ try {
+ JsonWriter.string().done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureEmpty2() {
+ try {
+ JsonWriter.string().end();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureMoreThanOneRoot() {
+ try {
+ JsonWriter.string().value(1).value(1).done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureMoreThanOneRoot2() {
+ try {
+ JsonWriter.string().array().value(1).end().value(1).done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
+
+ @Test
+ void failureMoreThanOneRoot3() {
+ try {
+ JsonWriter.string().array().value(1).end().array().value(1).end()
+ .done();
+ fail();
+ } catch (JsonWriterException e) {
+ // OK
+ }
+ }
}