This is an automated email from the ASF dual-hosted git repository. rmannibucau pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push: new 6571c776 [JOHNZON-369] adding org.apache.johnzon.boundedoutputstreamwriter support 6571c776 is described below commit 6571c7768b4897034a134b8664068b0f8bb2b02e Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Wed Apr 27 09:48:53 2022 +0200 [JOHNZON-369] adding org.apache.johnzon.boundedoutputstreamwriter support --- .../johnzon/core/JsonGeneratorFactoryImpl.java | 20 ++++- .../org/apache/johnzon/core/JsonGeneratorImpl.java | 9 -- .../johnzon/core/io/BoundedOutputStreamWriter.java | 98 ++++++++++++++++++++++ .../johnzon/core/JsonGeneratorFactoryImplTest.java | 70 ++++++++++++++++ .../core/io/BoundedOutputStreamWriterTest.java | 64 ++++++++++++++ src/site/markdown/index.md | 11 +++ 6 files changed, 260 insertions(+), 12 deletions(-) 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 c4748697..af1b905d 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 @@ -18,6 +18,8 @@ */ package org.apache.johnzon.core; +import org.apache.johnzon.core.io.BoundedOutputStreamWriter; + import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.Optional.ofNullable; @@ -35,21 +37,24 @@ import javax.json.stream.JsonGeneratorFactory; public class JsonGeneratorFactoryImpl extends AbstractJsonFactory implements JsonGeneratorFactory { public static final String GENERATOR_BUFFER_LENGTH = "org.apache.johnzon.default-char-buffer-generator"; + public static final String BOUNDED_OUTPUT_STREAM_WRITER_LEN = "org.apache.johnzon.boundedoutputstreamwriter"; public static final int DEFAULT_GENERATOR_BUFFER_LENGTH = Integer.getInteger(GENERATOR_BUFFER_LENGTH, 64 * 1024); //64k static final Collection<String> SUPPORTED_CONFIG_KEYS = asList( - JsonGenerator.PRETTY_PRINTING, GENERATOR_BUFFER_LENGTH, BUFFER_STRATEGY, ENCODING + JsonGenerator.PRETTY_PRINTING, GENERATOR_BUFFER_LENGTH, BUFFER_STRATEGY, ENCODING, BOUNDED_OUTPUT_STREAM_WRITER_LEN ); private final Charset defaultEncoding; //key caching currently disabled private final boolean pretty; + private final int boundedOutputStreamWriter; private final BufferStrategy.BufferProvider<char[]> bufferProvider; public JsonGeneratorFactoryImpl(final Map<String, ?> config) { super(config, SUPPORTED_CONFIG_KEYS, null); this.pretty = getBool(JsonGenerator.PRETTY_PRINTING, false); + this.boundedOutputStreamWriter = getInt(BOUNDED_OUTPUT_STREAM_WRITER_LEN, -1); this.defaultEncoding = ofNullable(getString(ENCODING, null)) .map(Charset::forName) .orElse(UTF_8); @@ -68,12 +73,21 @@ public class JsonGeneratorFactoryImpl extends AbstractJsonFactory implements Jso @Override public JsonGenerator createGenerator(final OutputStream out) { - return new JsonGeneratorImpl(new OutputStreamWriter(out, defaultEncoding), bufferProvider, pretty); + return new JsonGeneratorImpl( + boundedOutputStreamWriter <= 0 ? + new OutputStreamWriter(out, defaultEncoding) : + new BoundedOutputStreamWriter(out, defaultEncoding, boundedOutputStreamWriter), + bufferProvider, pretty); } @Override public JsonGenerator createGenerator(final OutputStream out, final Charset charset) { - return new JsonGeneratorImpl(out,charset, bufferProvider, pretty); + final Charset cs = charset == null ? defaultEncoding : charset; + return new JsonGeneratorImpl( + boundedOutputStreamWriter <= 0 ? + new OutputStreamWriter(out, cs) : + new BoundedOutputStreamWriter(out, cs, boundedOutputStreamWriter), + bufferProvider, pretty); } @Override diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorImpl.java b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorImpl.java index 0913804f..c011b1f6 100644 --- a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorImpl.java +++ b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorImpl.java @@ -27,13 +27,10 @@ import javax.json.JsonValue; import javax.json.stream.JsonGenerationException; import javax.json.stream.JsonGenerator; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.Serializable; import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; -import java.nio.charset.Charset; import java.util.Iterator; import java.util.Map; @@ -70,18 +67,12 @@ class JsonGeneratorImpl implements JsonGenerator, JsonChars, Serializable { JsonGeneratorImpl(final Writer writer, final BufferStrategy.BufferProvider<char[]> bufferProvider, final boolean prettyPrint) { this.writer = writer; - //this.cache = cache; this.buffer = bufferProvider.newBuffer(); this.bufferProvider = bufferProvider; this.prettyPrint = prettyPrint; state.push(GeneratorState.INITIAL); } - JsonGeneratorImpl(final OutputStream out, final Charset encoding, final BufferStrategy.BufferProvider<char[]> bufferProvider, - final boolean prettyPrint) { - this(new OutputStreamWriter(out, encoding), bufferProvider, prettyPrint); - } - private void writeEol() { if (prettyPrint) { justWrite(EOL); diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/io/BoundedOutputStreamWriter.java b/johnzon-core/src/main/java/org/apache/johnzon/core/io/BoundedOutputStreamWriter.java new file mode 100644 index 00000000..d0a2100c --- /dev/null +++ b/johnzon-core/src/main/java/org/apache/johnzon/core/io/BoundedOutputStreamWriter.java @@ -0,0 +1,98 @@ +/* + * 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.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.channels.Channels; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; + +/** + * {@link java.io.OutputStreamWriter} delegating directly to a {@link sun.nio.cs.StreamEncoder} with a controlled underlying buffer size. + * It enables to wrap an {@link OutputStream} as a {@link Writer} but with a faster feedback than a default + * {@link java.io.OutputStreamWriter} which uses a 8k buffer by default (encapsulated). + * <p> + * Note: the "flush error" can be of 2 characters (lcb in StreamEncoder) but we can't do much better when encoding. + */ +public class BoundedOutputStreamWriter extends Writer { + private final Writer delegate; + + public BoundedOutputStreamWriter(final OutputStream outputStream, + final Charset charset, + final int maxSize) { + delegate = Channels.newWriter( + Channels.newChannel(outputStream), + charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE), + maxSize); + } + + @Override + public void write(final int c) throws IOException { + delegate.write(c); + } + + @Override + public void write(final char[] chars, final int off, final int len) throws IOException { + delegate.write(chars, off, len); + } + + @Override + public void write(final String str, final int off, final int len) throws IOException { + delegate.write(str, off, len); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public void write(char[] cbuf) throws IOException { + delegate.write(cbuf); + } + + @Override + public void write(final String str) throws IOException { + delegate.write(str); + } + + @Override + public Writer append(final CharSequence csq) throws IOException { + return delegate.append(csq); + } + + @Override + public Writer append(final CharSequence csq, final int start, final int end) throws IOException { + return delegate.append(csq, start, end); + } + + @Override + public Writer append(final char c) throws IOException { + return delegate.append(c); + } +} diff --git a/johnzon-core/src/test/java/org/apache/johnzon/core/JsonGeneratorFactoryImplTest.java b/johnzon-core/src/test/java/org/apache/johnzon/core/JsonGeneratorFactoryImplTest.java new file mode 100644 index 00000000..6f02665d --- /dev/null +++ b/johnzon-core/src/test/java/org/apache/johnzon/core/JsonGeneratorFactoryImplTest.java @@ -0,0 +1,70 @@ +/* + * 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; + +import org.junit.Test; + +import javax.json.stream.JsonGenerator; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.emptyMap; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class JsonGeneratorFactoryImplTest { + @Test + public void boundedOutputStream() throws UnsupportedEncodingException { + final Map<String, Object> boundedConfig = new HashMap<>(); + boundedConfig.put(JsonGeneratorFactoryImpl.BOUNDED_OUTPUT_STREAM_WRITER_LEN, 1); + boundedConfig.put(JsonGeneratorFactoryImpl.GENERATOR_BUFFER_LENGTH, 1); + + final ByteArrayOutputStream bounded = new ByteArrayOutputStream(); + final ByteArrayOutputStream defaultOut = new ByteArrayOutputStream(); + + try (final JsonGenerator boundedGenerator = new JsonGeneratorFactoryImpl(boundedConfig).createGenerator(bounded); + final JsonGenerator defaultGenerator = new JsonGeneratorFactoryImpl(emptyMap()).createGenerator(defaultOut)) { + assertEquals(0, defaultOut.size()); + assertEquals(0, bounded.size()); + + boundedGenerator.writeStartObject(); + defaultGenerator.writeStartObject(); + assertEquals(0, defaultOut.size()); + assertEquals(0, bounded.size()); + + boundedGenerator.write("k", "val"); + defaultGenerator.write("k", "val"); + assertEquals(0, defaultOut.size()); + assertEquals(8, bounded.size()); + // this is the interesting part, there is still some buffering in the StreamEncoder due to + // encoding logic but it flushes "often enough" for our usage + assertEquals("{\"k\":\"va", bounded.toString("UTF-8")); + + boundedGenerator.writeEnd(); + defaultGenerator.writeEnd(); + assertEquals(0, defaultOut.size()); + assertEquals(9, bounded.size()); + } + + assertArrayEquals(bounded.toByteArray(), defaultOut.toByteArray()); + assertEquals("{\"k\":\"val\"}", bounded.toString("UTF-8")); + } +} diff --git a/johnzon-core/src/test/java/org/apache/johnzon/core/io/BoundedOutputStreamWriterTest.java b/johnzon-core/src/test/java/org/apache/johnzon/core/io/BoundedOutputStreamWriterTest.java new file mode 100644 index 00000000..2b8f6f6c --- /dev/null +++ b/johnzon-core/src/test/java/org/apache/johnzon/core/io/BoundedOutputStreamWriterTest.java @@ -0,0 +1,64 @@ +/* + * 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.io; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +public class BoundedOutputStreamWriterTest { + // sanity check + @Test + public void write() throws IOException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (final BoundedOutputStreamWriter writer = new BoundedOutputStreamWriter(outputStream, UTF_8, 10)) { + writer.write("ok"); + writer.write('1'); + } + assertEquals("ok1", outputStream.toString("UTF-8")); + } + + // enables to check buffer size respects + @Test + public void sizeLimit() throws IOException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (final BoundedOutputStreamWriter writer = new BoundedOutputStreamWriter(outputStream, UTF_8, 10)) { + writer.write("1234567890"); + assertEquals(0, outputStream.size()); // was not yet written since it matches buffer size + writer.write('1'); + assertEquals(10, outputStream.size()); // was written + } + assertEquals("12345678901", outputStream.toString("UTF-8")); + } + + // enables to check a small buffer size enables to have a faster outputstream feedback + @Test + public void sizeLimit2() throws IOException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (final BoundedOutputStreamWriter writer = new BoundedOutputStreamWriter(outputStream, UTF_8, 2)) { + writer.write("1234567890"); + writer.write('1'); + } + assertEquals("12345678901", outputStream.toString("UTF-8")); + } +} diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index f97517b5..3f7a4a0b 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -52,6 +52,17 @@ You'll surely want to add the API as dependency too: </dependency> ]]></pre> +#### Johnzon Factory Configurations + +##### JsonGeneratorFactory + +The generator factory supports the standard properties (pretty one for example) but also: + +* `org.apache.johnzon.encoding`: encoding to use for the generator when converting an OutputStream to a Writer. +* `org.apache.johnzon.buffer-strategy`: how to get buffers (char buffer), default strategy is a queue/pool based one but you can switch it to a `THREAD_LOCAL` one. `BY_INSTANCE` (per call/prototype) and `SINGLETON` (single instance) are also supported but first one is generally slower and last one does not enable overflows. +* `org.apache.johnzon.default-char-buffer-generator` (int): buffer size of the generator, it enables to work in memory to flush less often (for performances). +* `org.apache.johnzon.boundedoutputstreamwriter` (int): when converting an `OuputStream` to a `Writer` it defines the buffer size (if > 0) +- 2 charaters (for the encoding logic). It enables a faster flushing to the actual underlying output stream combined with `org.apache.johnzon.default-char-buffer-generator`. + ### JSON-P Strict Compliance (stable) <pre class="prettyprint linenums"><![CDATA[