JOHNZON-367 Fix Snippet buffering issues and expand tests
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java b/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java
index a979c45..e3b8ec9 100644
--- a/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java
@@ -16,7 +16,6 @@
*/
package org.apache.johnzon.core;
-import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
@@ -27,23 +26,60 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
-import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
+import static org.apache.johnzon.core.JsonGeneratorFactoryImpl.GENERATOR_BUFFER_LENGTH;
+
+/**
+ * Constructs short snippets of serialized JSON text representations of
+ * JsonValue instances in a way that is ideal for error messages.
+ *
+ * Instances of Snippet are thread-safe, reusable and memory-safe. Snippet
+ * serializes only enough of the json to fill the desired snippet size and
+ * is therefore safe to use regardless of the size of the JsonValue.
+ */
public class Snippet {
private final int max;
private final JsonGeneratorFactory generatorFactory;
+ /**
+ * This constructor should be used only in static or other scenarios were
+ * there is no JsonGeneratorFactory instance in scope.
+ *
+ * @param max the maximum length of the serialized json produced via of()
+ */
public Snippet(final int max) {
- this(max, Json.createGeneratorFactory(Collections.EMPTY_MAP));
+ this(max, new JsonGeneratorFactoryImpl(new HashMap<String, Object>() {
+ {
+ this.put(GENERATOR_BUFFER_LENGTH, max);
+ }
+ }));
}
+ /**
+ * This is the preferred approach to using Snippet in any context where
+ * there is an existing JsonGeneratorFactory in scope.
+ *
+ * @param max the maximum length of the serialized json produced via of()
+ * @param generatorFactory the JsonGeneratorFactory created by the user
+ */
public Snippet(final int max, final JsonGeneratorFactory generatorFactory) {
this.max = max;
this.generatorFactory = generatorFactory;
}
+ /**
+ * Create a serialized json representation of the supplied
+ * JsonValue, truncating the value to the specified max length.
+ * Truncated text appears with a suffix of "..."
+ *
+ * This method is thread safe.
+ *
+ * @param value the JsonValue to be serialized as json text
+ * @return a potentially truncated json text
+ */
public String of(final JsonValue value) {
switch (value.getValueType()) {
case TRUE: return "true";
@@ -58,30 +94,52 @@
}
}
+ /**
+ * Create a serialized json representation of the supplied
+ * JsonValue, truncating the value to the specified max length.
+ * Truncated text appears with a suffix of "..."
+ *
+ * This method is thread safe.
+ *
+ * Avoid using this method in any context where there already
+ * is a JsonGeneratorFactory instance in scope. For those scenarios
+ * use the constructor that accepts a JsonGeneratorFactory instead.
+ *
+ * @param value the JsonValue to be serialized as json text
+ * @param max the maximum length of the serialized json text
+ * @return a potentially truncated json text
+ */
public static String of(final JsonValue value, final int max) {
return new Snippet(max).of(value);
}
+ /**
+ * There are several buffers involved in the creation of a json string.
+ * This class carefully manages them all.
+ *
+ * JsonGeneratorImpl with a 64k buffer (by default)
+ * ObjectStreamWriter with an 8k buffer
+ * SnippetOutputStream with a buffer of maxSnippetLength
+ *
+ * As we create json via calling the JsonGenerator it is critical we
+ * flush the work in progress all the way through these buffers and into
+ * the final SnippetOutputStream buffer.
+ *
+ * If we do not, we risk creating up to 64k of json when we may only
+ * need 50 bytes. We could potentially optimize this code so the
+ * buffer held by JsonGeneratorImpl is also the maxSnippetLength.
+ */
class Buffer implements Closeable {
private final JsonGenerator generator;
private final SnippetOutputStream snippet;
- private Runnable close;
private Buffer() {
this.snippet = new SnippetOutputStream(max);
this.generator = generatorFactory.createGenerator(snippet);
- this.close = () -> {
- try {
- generator.close();
- } finally {
- this.close = () -> {
- };
- }
- };
}
private void write(final JsonValue value) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
return;
}
@@ -96,116 +154,160 @@
}
default: {
generator.write(value);
+ generator.flush();
}
}
}
private void write(final JsonArray array) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
return;
}
if (array.isEmpty()) {
generator.write(array);
+ generator.flush();
return;
}
generator.writeStartArray();
+ generator.flush();
for (final JsonValue jsonValue : array) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
break;
}
write(jsonValue);
}
generator.writeEnd();
+ generator.flush();
}
private void write(final JsonObject object) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
return;
}
if (object.isEmpty()) {
generator.write(object);
+ generator.flush();
return;
}
generator.writeStartObject();
+ generator.flush();
for (final Map.Entry<String, JsonValue> entry : object.entrySet()) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
break;
}
write(entry.getKey(), entry.getValue());
}
generator.writeEnd();
+ generator.flush();
}
private void write(final String name, final JsonValue value) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
return;
}
switch (value.getValueType()) {
case ARRAY:
generator.writeStartArray(name);
+ generator.flush();
final JsonArray array = value.asJsonArray();
for (final JsonValue jsonValue : array) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
break;
}
write(jsonValue);
}
generator.writeEnd();
+ generator.flush();
break;
case OBJECT:
generator.writeStartObject(name);
+ generator.flush();
final JsonObject object = value.asJsonObject();
for (final Map.Entry<String, JsonValue> keyval : object.entrySet()) {
- if (snippet.isComplete()) {
+ if (snippet.terminate()) {
break;
}
write(keyval.getKey(), keyval.getValue());
}
generator.writeEnd();
+ generator.flush();
break;
- default: generator.write(name, value);
+ default: {
+ generator.write(name, value);
+ generator.flush();
+ }
}
}
private String get() {
- // If close is not called the string may be empty
- close();
- return snippet.get();
+ generator.flush();
+ return snippet.isTruncated() ? snippet.get() + "..." : snippet.get();
}
@Override
public void close() {
- close.run();
+ generator.close();
}
}
+ /**
+ * Specialized OutputStream with three internal states:
+ * Writing, Completed, Truncated.
+ *
+ * When there is still space left for more json, the
+ * state will be Writing
+ *
+ * If the last write brought is exactly to the end of
+ * the max length, the state will be Completed.
+ *
+ * If the last write brought us over the max length, the
+ * state will be Truncated.
+ */
static class SnippetOutputStream extends OutputStream {
private final ByteArrayOutputStream buffer;
private OutputStream mode;
public SnippetOutputStream(final int max) {
- this.buffer = new ByteArrayOutputStream();
+ this.buffer = new ByteArrayOutputStream(Math.min(max, 8192));
this.mode = new Writing(max, buffer);
}
public String get() {
- if (isComplete()) {
- return buffer.toString() + "...";
- } else {
- return buffer.toString();
- }
+ return buffer.toString();
}
- public boolean isComplete() {
- return mode instanceof Ignoring;
+ /**
+ * Calling this method implies the need to continue
+ * writing and a question on if that is ok.
+ *
+ * It impacts internal state in the same way as
+ * calling a write method.
+ *
+ * @return true if no more writes are possible
+ */
+ public boolean terminate() {
+ if (mode instanceof Truncated) {
+ return true;
+ }
+
+ if (mode instanceof Completed) {
+ mode = new Truncated();
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean isTruncated() {
+ return mode instanceof Truncated;
}
@Override
@@ -256,7 +358,7 @@
if (++count < max) {
out.write(b);
} else {
- endReached();
+ maxReached(new Truncated());
}
}
@@ -271,30 +373,63 @@
if (remaining <= 0) {
- endReached();
+ maxReached(new Truncated());
+
+ } else if (len == remaining) {
+
+ count += len;
+ out.write(b, off, remaining);
+ maxReached(new Completed());
} else if (len > remaining) {
+ count += len;
out.write(b, off, remaining);
- endReached();
+ maxReached(new Truncated());
} else {
+ count += len;
out.write(b, off, len);
}
}
- private void endReached() throws IOException {
- mode = new Ignoring();
- flush();
- close();
+ private void maxReached(final OutputStream mode) throws IOException {
+ SnippetOutputStream.this.mode = mode;
+ out.flush();
+ out.close();
}
}
- static class Ignoring extends OutputStream {
+ /**
+ * Signifies the last write was fully written, but there is
+ * no more space for future writes.
+ */
+ class Completed extends OutputStream {
+ @Override
+ public void write(final int b) throws IOException {
+ SnippetOutputStream.this.mode = new Truncated();
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ if (len > 0) {
+ SnippetOutputStream.this.mode = new Truncated();
+ }
+ }
+ }
+
+ /**
+ * Signifies the last write was not completely written and there was
+ * no more space for this or future writes.
+ */
+ static class Truncated extends OutputStream {
@Override
public void write(final int b) throws IOException {
}
- }
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ }
+ }
}
}
diff --git a/johnzon-core/src/test/java/org/apache/johnzon/core/SnippetTest.java b/johnzon-core/src/test/java/org/apache/johnzon/core/SnippetTest.java
index 7ccf90c..44887e8 100644
--- a/johnzon-core/src/test/java/org/apache/johnzon/core/SnippetTest.java
+++ b/johnzon-core/src/test/java/org/apache/johnzon/core/SnippetTest.java
@@ -19,240 +19,518 @@
import org.junit.Test;
import javax.json.Json;
-import javax.json.JsonObject;
import javax.json.JsonValue;
+import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonParser;
import java.io.ByteArrayInputStream;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+import static javax.json.JsonValue.ValueType.ARRAY;
+import static javax.json.JsonValue.ValueType.OBJECT;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
public class SnippetTest {
@Test
public void simple() {
- final String jsonText = "{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
+ final JsonValue value = parse("{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}");
// This snippet is smaller than the allowed size. It should show in entirety.
- assertEquals("{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", Snippet.of(object, 100));
+ assertSnippet(value, "{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", 100);
// This snippet is exactly 50 characters when formatted. We should see no "..." at the end.
- assertEquals("{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", Snippet.of(object, 50));
+ assertSnippet(value, "{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", 50);
// This snippet is too large. We should see the "..." at the end.
- assertEquals("{\"name\":\"string\",\"value\":\"stri...", Snippet.of(object, 30));
+ assertSnippet(value, "{\"name\":\"string\",\"value\":\"stri...", 30);
}
@Test
public void mapOfArray() {
- final String jsonText = "{\"name\": [\"red\", \"green\", \"blue\"], \"value\": [\"orange\", \"yellow\", \"purple\"]}";
+ final JsonValue value = parse("{\"name\": [\"red\", \"green\", \"blue\"], \"value\": [\"orange\", \"yellow\", \"purple\"]}");
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
-
- assertEquals("{\"name\":[\"red\",\"green\",\"blue\"],\"value\":[\"orange\",\"yellow\",\"purple\"]}", Snippet.of(object, 200));
- assertEquals("{\"name\":[\"red\",\"green\",\"blue\"],\"value\":[\"orange\",\"...", Snippet.of(object, 50));
+ assertSnippet(value, "{\"name\":[\"red\",\"green\",\"blue\"],\"value\":[\"orange\",\"yellow\",\"purple\"]}", 200);
+ assertSnippet(value, "{\"name\":[\"red\",\"green\",\"blue\"],\"value\":[\"orange\",\"yellow\",\"purple\"]}", 68);
+ assertSnippet(value, "{\"name\":[\"red\",\"green\",\"blue\"],\"value\":[\"orange\",\"...", 50);
}
@Test
public void mapOfObject() {
- final String jsonText = "{\"name\": {\"name\": \"red\", \"value\": \"green\", \"type\": \"blue\"}," +
- " \"value\": {\"name\": \"orange\", \"value\": \"purple\", \"type\": \"yellow\"}}";
+ final JsonValue value = parse("{\"name\": {\"name\": \"red\", \"value\": \"green\", \"type\": \"blue\"}," +
+ " \"value\": {\"name\": \"orange\", \"value\": \"purple\", \"type\": \"yellow\", \"scope\": \"brown\"}}");
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
+ assertSnippet(value, "{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}," +
+ "\"value\":{\"name\":\"orange\",\"value\":\"purple\",\"type\":\"yellow\",\"scope\":\"brown\"}}", 200);
- assertEquals("{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}," +
- "\"value\":{\"name\":\"orange\",\"value\":\"purple\",\"type\":\"yellow\"}}", Snippet.of(object, 200));
+ assertSnippet(value, "{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}," +
+ "\"value\":{\"name\":\"orange\",\"value\":\"purple\",\"type\":\"yellow\",\"scope\":\"brown\"}}", 128);
- assertEquals("{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}," +
- "\"value\":{\"name\":\"orange\",\"value\":\"purple\",\"type...", Snippet.of(object, 100));
+ assertSnippet(value, "{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}," +
+ "\"value\":{\"name\":\"orange\",\"value\":\"purple\",\"type...", 100);
}
@Test
public void mapOfNestedMaps() {
- final String jsonText = "{\"name\": {\"name\": {\"name\": {\"name\": \"red\", \"value\": \"green\", \"type\": \"blue\"}}}}";
+ final JsonValue value = parse("{\"name\": {\"name\": {\"name\": {\"name\": \"red\", \"value\": \"green\", \"type\": \"blue\"}}}}");
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
-
- assertEquals("{\"name\":{\"name\":{\"name\":{\"name\":\"red\"," +
- "\"value\":\"green\",\"type\":\"blue\"}}}}", Snippet.of(object, 100));
-
- assertEquals("{\"name\":{\"name\":{\"name\":{\"name\":\"red\",\"value\":\"gre...", Snippet.of(object, 50));
+ assertSnippet(value, "{\"name\":{\"name\":{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}}}}", 100);
+ assertSnippet(value, "{\"name\":{\"name\":{\"name\":{\"name\":\"red\",\"value\":\"green\",\"type\":\"blue\"}}}}", 71);
+ assertSnippet(value, "{\"name\":{\"name\":{\"name\":{\"name\":\"red\",\"value\":\"gre...", 50);
}
@Test
public void mapOfString() {
- final String jsonText = "{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
- assertEquals("{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", Snippet.of(object, 50));
- assertEquals("{\"name\":\"string\",\"value\":\"stri...", Snippet.of(object, 30));
+ final JsonValue value = parse("{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}");
+ assertSnippet(value, "{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", 100);
+ assertSnippet(value, "{\"name\":\"string\",\"value\":\"string\",\"type\":\"string\"}", 50);
+ assertSnippet(value, "{\"name\":\"string\",\"value\":\"stri...", 30);
}
@Test
public void mapOfNumber() {
- final String jsonText = "{\"name\":1234,\"value\":5,\"type\":67890}";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
+ final JsonValue value = parse("{\"name\":1234,\"value\":5,\"type\":67890,\"scope\":null}");
- assertEquals("{\"name\":1234,\"value\":5,\"type\":67890}", Snippet.of(object, 40));
- assertEquals("{\"name\":1234,\"value\":5,\"type\":...", Snippet.of(object, 30));
+ assertSnippet(value, "{\"name\":1234,\"value\":5,\"type\":67890,\"scope\":null}", 100);
+ assertSnippet(value, "{\"name\":1234,\"value\":5,\"type\":67890,\"scope\":null}", 49);
+ assertSnippet(value, "{\"name\":1234,\"value\":5,\"type\":...", 30);
}
@Test
public void mapOfTrue() {
- final String jsonText = "{\"name\":true,\"value\":true,\"type\":true}";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
+ final JsonValue value = parse("{\"name\":true,\"value\":true,\"type\":true,\"scope\":true}");
- assertEquals("{\"name\":true,\"value\":true,\"type\":true}", Snippet.of(object, 40));
- assertEquals("{\"name\":true,\"value\":true,\"typ...", Snippet.of(object, 30));
+ assertSnippet(value, "{\"name\":true,\"value\":true,\"type\":true,\"scope\":true}", 60);
+ assertSnippet(value, "{\"name\":true,\"value\":true,\"type\":true,\"scope\":true}", 51);
+ assertSnippet(value, "{\"name\":true,\"value\":true,\"typ...", 30);
}
@Test
public void mapOfFalse() {
- final String jsonText = "{\"name\":false,\"value\":false,\"type\":false}";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
+ final JsonValue value = parse("{\"name\":false,\"value\":false,\"type\":false}");
- assertEquals("{\"name\":false,\"value\":false,\"type\":false}", Snippet.of(object, 50));
- assertEquals("{\"name\":false,\"value\":false,\"t...", Snippet.of(object, 30));
+ assertSnippet(value, "{\"name\":false,\"value\":false,\"type\":false}", 50);
+ assertSnippet(value, "{\"name\":false,\"value\":false,\"type\":false}", 41);
+ assertSnippet(value, "{\"name\":false,\"value...", 20);
}
@Test
public void mapOfNull() {
- final String jsonText = "{\"name\":null,\"value\":null,\"type\":null}";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonObject object = jsonParser.getObject();
+ final JsonValue value = parse("{\"name\":null,\"value\":null,\"type\":null,\"scope\":null}");
- assertEquals("{\"name\":null,\"value\":null,\"type\":null}", Snippet.of(object, 50));
- assertEquals("{\"name\":null,\"value\":null,\"typ...", Snippet.of(object, 30));
+ assertSnippet(value, "{\"name\":null,\"value\":null,\"type\":null,\"scope\":null}", 60);
+ assertSnippet(value, "{\"name\":null,\"value\":null,\"type\":null,\"scope\":null}", 51);
+ assertSnippet(value, "{\"name\":null,\"value\":null,\"typ...", 30);
}
@Test
public void arrayOfArray() {
- final String jsonText = "[[\"red\",\"green\"], [1,22,333], [{\"r\": 255,\"g\": 165}], [true, false]]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[[\"red\",\"green\"], [1,22,333], [{\"r\": 255,\"g\": 165}], [true, false]]");
- assertEquals("[[\"red\",\"green\"],[1,22,333],[{\"r\":255,\"g\":165}],[true,false]]", Snippet.of(object, 100));
- assertEquals("[[\"red\",\"green\"],[1,22,333],[{\"r\":255,\"g...", Snippet.of(object, 40));
+ assertSnippet(value, "[[\"red\",\"green\"],[1,22,333],[{\"r\":255,\"g\":165}],[true,false]]", 100);
+ assertSnippet(value, "[[\"red\",\"green\"],[1,22,333],[{\"r\":255,\"g\":165}],[true,false]]", 61);
+ assertSnippet(value, "[[\"red\",\"green\"],[1,22,333],[{\"r\":255,\"g...", 40);
}
@Test
public void arrayOfObject() {
- final String jsonText = "[{\"r\": 255,\"g\": \"165\"},{\"g\": 0,\"a\": \"0\"},{\"transparent\": false}]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[{\"r\": 255,\"g\": \"165\"},{\"g\": 0,\"a\": \"0\"},{\"transparent\": false}]");
- assertEquals("[{\"r\":255,\"g\":\"165\"},{\"g\":0,\"a\":\"0\"},{\"transparent\":false}]", Snippet.of(object, 100));
- assertEquals("[{\"r\":255,\"g\":\"165\"},{\"g\":0,\"a...", Snippet.of(object, 30));
+ assertSnippet(value, "[{\"r\":255,\"g\":\"165\"},{\"g\":0,\"a\":\"0\"},{\"transparent\":false}]", 100);
+ assertSnippet(value, "[{\"r\":255,\"g\":\"165\"},{\"g\":0,\"a\":\"0\"},{\"transparent\":false}]", 59);
+ assertSnippet(value, "[{\"r\":255,\"g\":\"165\"},{\"g\":0,\"a...", 30);
}
@Test
public void arrayOfString() {
- final String jsonText = "[\"red\", \"green\", \"blue\", \"orange\", \"yellow\", \"purple\"]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[\"red\", \"green\", \"blue\", \"orange\", \"yellow\", \"purple\"]");
- assertEquals("[\"red\",\"green\",\"blue\",\"orange\",\"yellow\",\"purple\"]", Snippet.of(object, 100));
- assertEquals("[\"red\",\"green\",\"blue\",\"orange\"...", Snippet.of(object, 30));
+ assertSnippet(value, "[\"red\",\"green\",\"blue\",\"orange\",\"yellow\",\"purple\"]", 100);
+ assertSnippet(value, "[\"red\",\"green\",\"blue\",\"orange\",\"yellow\",\"purple\"]", 49);
+ assertSnippet(value, "[\"red\",\"green\",\"blue\",\"orange\"...", 30);
}
@Test
public void arrayOfNumber() {
- final String jsonText = "[1,22,333,4444,55555,666666,7777777,88888888,999999999]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[1,22,333,4444,55555,666666,7777777,88888888,999999999]");
- assertEquals("[1,22,333,4444,55555,666666,7777777,88888888,999999999]", Snippet.of(object, 100));
- assertEquals("[1,22,333,4444,55555,666666,77...", Snippet.of(object, 30));
+ assertSnippet(value, "[1,22,333,4444,55555,666666,7777777,88888888,999999999]", 100);
+ assertSnippet(value, "[1,22,333,4444,55555,666666,7777777,88888888,999999999]", 55);
+ assertSnippet(value, "[1,22,333,4444,55555,666666,77...", 30);
}
@Test
public void arrayOfTrue() {
- final String jsonText = "[true,true,true,true,true,true,true,true]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[true,true,true,true,true,true,true,true]");
- assertEquals("[true,true,true,true,true,true,true,true]", Snippet.of(object, 100));
- assertEquals("[true,true,true,true,true,true...", Snippet.of(object, 30));
+ assertSnippet(value, "[true,true,true,true,true,true,true,true]", 100);
+ assertSnippet(value, "[true,true,true,true,true,true,true,true]", 41);
+ assertSnippet(value, "[true,true,true,true,true,true...", 30);
}
@Test
public void arrayOfFalse() {
- final String jsonText = "[false,false,false,false,false,false,false]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[false,false,false,false,false,false,false]");
- assertEquals("[false,false,false,false,false,false,false]", Snippet.of(object, 100));
- assertEquals("[false,false,false,false,false...", Snippet.of(object, 30));
+ assertSnippet(value, "[false,false,false,false,false,false,false]", 100);
+ assertSnippet(value, "[false,false,false,false,false,false,false]", 43);
+ assertSnippet(value, "[false,false,false,false,false...", 30);
}
@Test
public void arrayOfNull() {
- final String jsonText = "[null,null,null,null,null,null]";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("[null,null,null,null,null,null]");
- assertEquals("[null,null,null,null,null,null]", Snippet.of(object, 50));
- assertEquals("[null,null,null...", Snippet.of(object, 15));
+ assertSnippet(value, "[null,null,null,null,null,null]", 50);
+ assertSnippet(value, "[null,null,null,null,null,null]", 31);
+ assertSnippet(value, "[null,null,null...", 15);
}
@Test
public void string() {
- final String jsonText = "\"This is a \\\"string\\\" with quotes in it. It should be properly escaped.\"";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("\"This is a \\\"string\\\" with quotes in it. It should be properly escaped.\"");
- assertEquals("\"This is a \\\"string\\\" with quotes in it. It should be properly escaped.\"", Snippet.of(object, 100));
- assertEquals("\"This is a \\\"string\\\" with quotes in it. It shoul...", Snippet.of(object, 50));
+ assertSnippet(value, "\"This is a \\\"string\\\" with quotes in it. It should be properly escaped.\"", 100);
+ assertSnippet(value, "\"This is a \\\"string\\\" with quotes in it. It should be properly escaped.\"", 73);
+ assertSnippet(value, "\"This is a \\\"string\\\" with quotes in it. It shoul...", 50);
}
@Test
public void number() {
- final String jsonText = "1223334444555556666667777777.88888888999999999";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("1223334444555556666667777777.88888888999999999");
- assertEquals("1223334444555556666667777777.88888888999999999", Snippet.of(object, 50));
- assertEquals("1223334444555556666667777777.8...", Snippet.of(object, 30));
+ assertSnippet(value, "1223334444555556666667777777.88888888999999999", 50);
+ assertSnippet(value, "1223334444555556666667777777.88888888999999999", 46);
+ assertSnippet(value, "1223334444555556666667777777.8...", 30);
}
@Test
public void trueValue() {
- final String jsonText = "true";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("true");
- assertEquals("true", Snippet.of(object, 50));
+ assertOptimizedSnippet(value, "true", 50);
// we don't trim 'true' -- showing users something like 't...' doesn't make much sense
- assertEquals("true", Snippet.of(object, 1));
+ assertOptimizedSnippet(value, "true", 1);
}
@Test
public void falseValue() {
- final String jsonText = "false";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("false");
- assertEquals("false", Snippet.of(object, 50));
+ assertOptimizedSnippet(value, "false", 50);
// we don't trim 'false' -- showing users something like 'f...' doesn't make much sense
- assertEquals("false", Snippet.of(object, 1));
+ assertOptimizedSnippet(value, "false", 1);
}
@Test
public void nullValue() {
- final String jsonText = "null";
- final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(jsonText.getBytes()));
- final JsonValue object = jsonParser.getValue();
+ final JsonValue value = parse("null");
- assertEquals("null", Snippet.of(object, 50));
+ assertOptimizedSnippet(value, "null", 50);
// we don't trim 'null' -- showing users something like 'n...' doesn't make much sense
- assertEquals("null", Snippet.of(object, 1));
+ assertOptimizedSnippet(value, "null", 1);
+ }
+
+ private JsonValue parse(final String json) {
+ final JsonParser jsonParser = Json.createParser(new ByteArrayInputStream(json.getBytes()));
+ return jsonParser.getValue();
+ }
+
+ private void assertSnippet(final JsonValue object, final String expected, final int i) {
+ final TrackingJsonGeneratorFactory factory = new TrackingJsonGeneratorFactory();
+ final String actual = new Snippet(i, factory).of(object);
+
+ // Assert the resulting string contents
+ assertEquals(expected, actual);
+
+ // Assert the resulting string length
+ if (expected.endsWith("...")) {
+ assertEquals(i + 3, actual.length());
+ } else {
+ assertTrue(actual.length() <= i);
+ }
+
+ /*
+ * Close methods are supposed to idempotent and
+ * safe to call many times, but let's be nice and
+ * ensure it is only called once.
+ */
+ assertEquals(1, factory.calls.stream()
+ .filter("close()"::equals)
+ .count());
+
+ /*
+ * When writing arrays or objects, assert we stopped
+ * calling write methods on the JsonGenerator once the
+ * end of the snippet is reached.
+ */
+ if (expected.endsWith("...") && isType(object, ARRAY, OBJECT)) {
+ /*
+ * Serialize the json value, truncating nothing
+ */
+ final TrackingJsonGeneratorFactory full = new TrackingJsonGeneratorFactory();
+ new Snippet(Integer.MAX_VALUE, full).of(object);
+
+ /*
+ * Assert that the calls made in truncated version are less
+ * than when we serialized the entire json value.
+ */
+ assertTrue(factory.calls.size() < full.calls.size());
+ }
+ }
+
+ private static boolean isType(final JsonValue value, final JsonValue.ValueType... types) {
+ for (final JsonValue.ValueType type : types) {
+ if (value.getValueType() == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * We assert that a plain string was returned for a null, true or false
+ * and no JsonGenerator was created. These values should also not be
+ * truncated.
+ */
+ private void assertOptimizedSnippet(final JsonValue object, final String expected, final int i) {
+ final TrackingJsonGeneratorFactory factory = new TrackingJsonGeneratorFactory();
+ final String actual = new Snippet(i, factory).of(object);
+
+ // Assert the resulting string contents
+ assertEquals(expected, actual);
+
+ /*
+ * We should not be constructing or using a JsonGenerator
+ * for these basic types which are effectively constants
+ */
+ assertEquals(0, factory.calls.size());
+ }
+
+ /**
+ * Track all calls made to JsonGenerator so we can ensure Snippet is
+ * not trying to serialize entire Json documents. Despite the output
+ * of Snippet appearing properly truncated, buffering can still cause
+ * the entire json value to be serialized.
+ *
+ * The only way to test this is not happening is to track all calls
+ * made to the JsonGenerator and assert they do in fact stop.
+ */
+ public static class TrackingJsonGeneratorFactory extends JsonGeneratorFactoryImpl {
+ private final List<String> calls = new ArrayList<>();
+
+ public TrackingJsonGeneratorFactory() {
+ super(Collections.EMPTY_MAP);
+ }
+
+ @Override
+ public JsonGenerator createGenerator(final OutputStream out) {
+ return new TrackingGenerator(super.createGenerator(out));
+ }
+
+ @Override
+ public JsonGenerator createGenerator(final Writer writer) {
+ return new TrackingGenerator(super.createGenerator(writer));
+ }
+
+ class TrackingGenerator implements JsonGenerator {
+
+ private final JsonGenerator delegate;
+
+ public TrackingGenerator(final JsonGenerator delegate) {
+ record("<init>");
+ this.delegate = delegate;
+ }
+
+ private void record(final String method, final Object... args) {
+ final String argString = Stream.of(args)
+ .map(Object::toString)
+ .reduce((s, s2) -> s + "," + s2)
+ .orElse("");
+ calls.add(String.format("%s(%s)", method, argString));
+ }
+
+ public List<String> getCalls() {
+ return calls;
+ }
+
+ @Override
+ public JsonGenerator writeStartObject() {
+ record("writeStartObject");
+ return delegate.writeStartObject();
+ }
+
+ @Override
+ public JsonGenerator writeStartObject(final String name) {
+ record("writeStartObject", name);
+ return delegate.writeStartObject(name);
+ }
+
+ @Override
+ public JsonGenerator writeStartArray() {
+ record("writeStartArray");
+ return delegate.writeStartArray();
+ }
+
+ @Override
+ public JsonGenerator writeStartArray(final String name) {
+ record("writeStartArray", name);
+ return delegate.writeStartArray(name);
+ }
+
+ @Override
+ public JsonGenerator writeKey(final String name) {
+ record("writeKey", name);
+ return delegate.writeKey(name);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final JsonValue value) {
+ record("write", name, value.getValueType());
+ assertJsonType(value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final String value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final BigInteger value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final BigDecimal value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final int value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final long value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final double value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator write(final String name, final boolean value) {
+ record("write", name, value);
+ return delegate.write(name, value);
+ }
+
+ @Override
+ public JsonGenerator writeNull(final String name) {
+ record("writeNull");
+ return delegate.writeNull(name);
+ }
+
+ @Override
+ public JsonGenerator writeEnd() {
+ record("writeEnd");
+ return delegate.writeEnd();
+ }
+
+ @Override
+ public JsonGenerator write(final JsonValue value) {
+ record("write", value.getValueType());
+ assertJsonType(value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final String value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final BigDecimal value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final BigInteger value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final int value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final long value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final double value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator write(final boolean value) {
+ record("write", value);
+ return delegate.write(value);
+ }
+
+ @Override
+ public JsonGenerator writeNull() {
+ record("writeNull");
+ return delegate.writeNull();
+ }
+
+ @Override
+ public void close() {
+ record("close");
+ delegate.close();
+ }
+
+ @Override
+ public void flush() {
+ record("flush");
+ delegate.flush();
+ }
+
+ /**
+ * Snippet should not be asking the JsonGenerator to be serializing
+ * an objet or an array as this would cause the entire portion of
+ * json to be written even if we need a small chunk.
+ */
+ private void assertJsonType(final JsonValue value) {
+ if (isType(value, ARRAY, OBJECT)){
+ fail("should never be called");
+ }
+ }
+ }
}
}