blob: 06bc005301a22f128e9af39f097d4d56cd0d6b66 [file] [log] [blame]
/*
* 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 javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonGeneratorFactory;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
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;
/**
* 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.
*
* This constructor must not be used in Johnzon project. It is only here
* for convenience for integrators. Using it disables several Johnzon
* features.
*
* @param max the maximum length of the serialized json produced via of()
*/
public Snippet(final int max) {
this(max, Json.createGeneratorFactory(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) {
try (final Buffer buffer = new Buffer()) {
buffer.write(value);
return buffer.get();
}
}
/**
* 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 SnippetWriter snippet;
private Runnable flush;
private Buffer() {
this.snippet = new SnippetWriter(max);
this.generator = generatorFactory.createGenerator(snippet);
this.flush = generator::flush;
}
private void write(final JsonValue value) {
if (terminate()) {
return;
}
switch (value.getValueType()) {
case ARRAY: {
write(value.asJsonArray());
break;
}
case OBJECT: {
write(value.asJsonObject());
break;
}
default: {
generator.write(value);
}
}
}
private void write(final JsonArray array) {
if (array.isEmpty()) {
generator.write(array);
return;
}
generator.writeStartArray();
for (final JsonValue jsonValue : array) {
if (terminate()) {
break;
}
write(jsonValue);
}
generator.writeEnd();
}
private void write(final JsonObject object) {
if (object.isEmpty()) {
generator.write(object);
return;
}
generator.writeStartObject();
for (final Map.Entry<String, JsonValue> entry : object.entrySet()) {
if (terminate()) {
break;
}
write(entry.getKey(), entry.getValue());
}
generator.writeEnd();
}
private void write(final String name, final JsonValue value) {
switch (value.getValueType()) {
case ARRAY:
generator.writeStartArray(name);
final JsonArray array = value.asJsonArray();
for (final JsonValue jsonValue : array) {
if (terminate()) {
break;
}
write(jsonValue);
}
generator.writeEnd();
break;
case OBJECT:
generator.writeStartObject(name);
final JsonObject object = value.asJsonObject();
for (final Map.Entry<String, JsonValue> keyval : object.entrySet()) {
if (terminate()) {
break;
}
write(keyval.getKey(), keyval.getValue());
}
generator.writeEnd();
break;
default: {
generator.write(name, value);
}
}
}
private boolean terminate() {
flush.run();
return snippet.terminate();
}
private String get() {
generator.flush();
return snippet.isTruncated() ? snippet.get() + "..." : snippet.get();
}
@Override
public void close() {
generator.close();
}
/**
* Specialized Writer 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.
*/
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 = () -> {
// no-op
};
// future calls can just return the size
bufferSize = () -> size;
return size;
};
}
public String get() {
return buffer.toString();
}
@Override
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 {
// no-op
}
@Override
public void close() throws IOException {
// no-op
}
}
class Writing extends Mode {
private final int max;
private int count;
private final Writer writer;
public Writing(final int max, final Writer writer) {
this.max = max;
this.writer = writer;
}
@Override
public void write(final char[] cbuf, final int off, final int len) throws IOException {
final int remaining = max - count;
if (remaining <= 0) {
maxReached(new Truncated());
} else if (len == remaining) {
count += len;
writer.write(cbuf, off, remaining);
maxReached(new Completed());
} else if (len > remaining) {
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();
}
}
/**
* Signifies the last write was fully written, but there is
* no more space for future writes.
*/
class Completed extends Mode {
@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.
*/
class Truncated extends Mode {
@Override
public void write(final char[] cbuf, final int off, final int len) throws IOException {
// no-op
}
}
}
}
}