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");
+                }
+            }
+        }
     }
 
 }