JOHNZON-367 Optimize buffering when new Buffered interface is supported
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/Buffered.java b/johnzon-core/src/main/java/org/apache/johnzon/core/Buffered.java
new file mode 100644
index 0000000..83f2897
--- /dev/null
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/Buffered.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.johnzon.core;
+
+/**
+ * A <tt>Buffered</tt> is a source or destination of data that is buffered
+ * before writing or reading.  The bufferSize method allows all participants
+ * in the underlying stream to align on this buffer size for optimization.
+ *
+ * This interface is designed in the spirit of {@code java.io.Flushable} and
+ * {@code java.io.Closeable}
+ *
+ * @since 1.2.17
+ */
+public interface Buffered {
+
+    /**
+     * The buffer size used by this stream while reading input or before writing
+     * output to the underlying stream.
+     */
+    int bufferSize();
+}
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java
index 8d1f6ec..87e23a7 100644
--- a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java
@@ -20,6 +20,7 @@
 
 import static java.util.Arrays.asList;
 
+import java.io.Flushable;
 import java.io.OutputStream;
 import java.io.Writer;
 import java.nio.charset.Charset;
@@ -39,7 +40,8 @@
     );
     //key caching currently disabled
     private final boolean pretty;
-    private final BufferStrategy.BufferProvider<char[]> bufferProvider;
+    private final Buffer buffer;
+    private volatile Buffer customBuffer;
 
     public JsonGeneratorFactoryImpl(final Map<String, ?> config) {
         
@@ -52,26 +54,54 @@
               throw new IllegalArgumentException("buffer length must be greater than zero");
           }
 
-          this.bufferProvider = getBufferProvider().newCharProvider(bufferSize);
+          this.buffer = new Buffer(getBufferProvider().newCharProvider(bufferSize), bufferSize);
     }
 
     @Override
     public JsonGenerator createGenerator(final Writer writer) {
-        return new JsonGeneratorImpl(writer, bufferProvider, pretty);
+        return new JsonGeneratorImpl(writer, getBufferProvider(writer), pretty);
     }
 
     @Override
     public JsonGenerator createGenerator(final OutputStream out) {
-        return new JsonGeneratorImpl(out, bufferProvider, pretty);
+        return new JsonGeneratorImpl(out, getBufferProvider(out), pretty);
     }
 
     @Override
     public JsonGenerator createGenerator(final OutputStream out, final Charset charset) {
-        return new JsonGeneratorImpl(out,charset, bufferProvider, pretty);
+        return new JsonGeneratorImpl(out,charset, getBufferProvider(out), pretty);
+    }
+
+    private BufferStrategy.BufferProvider<char[]> getBufferProvider(final Flushable flushable) {
+        if (!(flushable instanceof Buffered)) {
+            return buffer.provider;
+        }
+
+        final int bufferSize = Buffered.class.cast(flushable).bufferSize();
+
+        if (customBuffer != null && customBuffer.size == bufferSize) {
+            return customBuffer.provider;
+        }
+
+        synchronized (this) {
+            customBuffer = new Buffer(getBufferProvider().newCharProvider(bufferSize), bufferSize);
+            return customBuffer.provider;
+        }
     }
 
     @Override
     public Map<String, ?> getConfigInUse() {
         return Collections.unmodifiableMap(internalConfig);
     }
+
+    private static final class Buffer {
+        private final BufferStrategy.BufferProvider<char[]> provider;
+        private final int size;
+
+        private Buffer(final BufferStrategy.BufferProvider<char[]>
+ bufferProvider, final int bufferSize) {
+            this.provider = bufferProvider;
+            this.size = bufferSize;
+        }
+    }
 }
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 9903331..ba16ed9 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
@@ -24,9 +24,11 @@
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
-import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Supplier;
 
 import static org.apache.johnzon.core.JsonGeneratorFactoryImpl.GENERATOR_BUFFER_LENGTH;
 
@@ -123,11 +125,13 @@
      */
     class Buffer implements Closeable {
         private final JsonGenerator generator;
-        private final SnippetOutputStream snippet;
+        private final SnippetWriter snippet;
+        private Runnable flush;
 
         private Buffer() {
-            this.snippet = new SnippetOutputStream(max);
+            this.snippet = new SnippetWriter(max);
             this.generator = generatorFactory.createGenerator(snippet);
+            this.flush = generator::flush;
         }
 
         private void write(final JsonValue value) {
@@ -215,7 +219,7 @@
         }
 
         private boolean terminate() {
-            generator.flush();
+            flush.run();
             return snippet.terminate();
         }
 
@@ -228,172 +232,181 @@
         public void close() {
             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(Math.min(max, 8192));
-            this.mode = new Writing(max, buffer);
-        }
-
-        public String get() {
-            return buffer.toString();
-        }
 
         /**
-         * Calling this method implies the need to continue
-         * writing and a question on if that is ok.
+         * Specialized Writer with three internal states:
+         * Writing, Completed, Truncated.
          *
-         * It impacts internal state in the same way as
-         * calling a write method.
+         * When there is still space left for more json, the
+         * state will be Writing
          *
-         * @return true if no more writes are possible
+         * 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.
          */
-        public boolean terminate() {
-            if (mode instanceof Truncated) {
-                return true;
+        class SnippetWriter extends Writer implements Buffered {
+
+            private final ByteArrayOutputStream buffer;
+            private Mode mode;
+            private Supplier<Integer> bufferSize;
+
+            public SnippetWriter(final int max) {
+                final int size = Math.min(max, 8192);
+
+                this.buffer = new ByteArrayOutputStream(size);
+                this.mode = new Writing(max, new OutputStreamWriter(buffer));
+
+                /*
+                 * The first time the buffer size is requested, disable flushing
+                 * as we know our requested buffer size will be respected
+                 */
+                this.bufferSize = () -> {
+                    // disable flushing
+                    flush = () -> {
+                    };
+                    // future calls can just return the size
+                    bufferSize = () -> size;
+                    return size;
+                };
             }
 
-            if (mode instanceof Completed) {
-                mode = new Truncated();
-                return true;
-            }
-
-            return false;
-        }
-
-        public boolean isTruncated() {
-            return mode instanceof Truncated;
-        }
-
-        @Override
-        public void write(final int b) throws IOException {
-            mode.write(b);
-        }
-
-        @Override
-        public void write(final byte[] b) throws IOException {
-            mode.write(b);
-        }
-
-        @Override
-        public void write(final byte[] b, final int off, final int len) throws IOException {
-            mode.write(b, off, len);
-        }
-
-        @Override
-        public void flush() throws IOException {
-            mode.flush();
-        }
-
-        @Override
-        public void close() throws IOException {
-            mode.close();
-        }
-
-        class Writing extends OutputStream {
-            private final int max;
-            private int count;
-            private final OutputStream out;
-
-            public Writing(final int max, final OutputStream out) {
-                this.max = max;
-                this.out = out;
+            public String get() {
+                return buffer.toString();
             }
 
             @Override
-            public void write(final int b) throws IOException {
-                if (++count < max) {
-                    out.write(b);
-                } else {
-                    maxReached(new Truncated());
+            public int bufferSize() {
+                return bufferSize.get();
+            }
+
+            /**
+             * 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
+            public void write(final char[] cbuf, final int off, final int len) throws IOException {
+                mode.write(cbuf, off, len);
+            }
+
+            @Override
+            public void flush() throws IOException {
+                mode.flush();
+            }
+
+            @Override
+            public void close() throws IOException {
+                mode.close();
+            }
+
+            abstract class Mode extends Writer {
+                @Override
+                public void flush() throws IOException {
+                }
+
+                @Override
+                public void close() throws IOException {
                 }
             }
 
-            @Override
-            public void write(final byte[] b) throws IOException {
-                write(b, 0, b.length);
-            }
+            class Writing extends Mode {
+                private final int max;
+                private int count;
+                private final Writer writer;
 
-            @Override
-            public void write(final byte[] b, final int off, final int len) throws IOException {
-                final int remaining = max - count;
+                public Writing(final int max, final Writer writer) {
+                    this.max = max;
+                    this.writer = writer;
+                }
 
-                if (remaining <= 0) {
+                @Override
+                public void write(final char[] cbuf, final int off, final int len) throws IOException {
+                    final int remaining = max - count;
 
-                    maxReached(new Truncated());
+                    if (remaining <= 0) {
 
-                } else if (len == remaining) {
+                        maxReached(new Truncated());
 
-                    count += len;
-                    out.write(b, off, remaining);
-                    maxReached(new Completed());
+                    } else if (len == remaining) {
 
-                } else if (len > remaining) {
+                        count += len;
+                        writer.write(cbuf, off, remaining);
+                        maxReached(new Completed());
 
-                    count += len;
-                    out.write(b, off, remaining);
-                    maxReached(new Truncated());
+                    } else if (len > remaining) {
 
-                } else {
-                    count += len;
-                    out.write(b, off, len);
+                        count += len;
+                        writer.write(cbuf, off, remaining);
+                        maxReached(new Truncated());
+
+                    } else {
+                        count += len;
+                        writer.write(cbuf, off, len);
+                    }
+                }
+
+                @Override
+                public void flush() throws IOException {
+                    writer.flush();
+                }
+
+                @Override
+                public void close() throws IOException {
+                    writer.close();
+                }
+
+                private void maxReached(final Mode mode) throws IOException {
+                    SnippetWriter.this.mode = mode;
+                    writer.flush();
+                    writer.close();
                 }
             }
 
-            private void maxReached(final OutputStream mode) throws IOException {
-                SnippetOutputStream.this.mode = mode;
-                out.flush();
-                out.close();
-            }
-        }
+            /**
+             * Signifies the last write was fully written, but there is
+             * no more space for future writes.
+             */
+            class Completed extends Mode {
 
-        /**
-         * 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();
+                @Override
+                public void write(final char[] cbuf, final int off, final int len) throws IOException {
+                    if (len > 0) {
+                        SnippetWriter.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 {
+            /**
+             * Signifies the last write was not completely written and there was
+             * no more space for this or future writes.
+             */
+            class Truncated extends Mode {
+                @Override
+                public void write(final char[] cbuf, final int off, final int len) throws IOException {
+                }
             }
         }
     }