blob: e3b8ec9db7926c9d64c2a1d3324f3c6ba4147552 [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.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.OutputStream;
import java.io.UncheckedIOException;
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, 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";
case FALSE: return "false";
case NULL: return "null";
default: {
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 SnippetOutputStream snippet;
private Buffer() {
this.snippet = new SnippetOutputStream(max);
this.generator = generatorFactory.createGenerator(snippet);
}
private void write(final JsonValue value) {
if (snippet.terminate()) {
return;
}
switch (value.getValueType()) {
case ARRAY: {
write(value.asJsonArray());
break;
}
case OBJECT: {
write(value.asJsonObject());
break;
}
default: {
generator.write(value);
generator.flush();
}
}
}
private void write(final JsonArray array) {
if (snippet.terminate()) {
return;
}
if (array.isEmpty()) {
generator.write(array);
generator.flush();
return;
}
generator.writeStartArray();
generator.flush();
for (final JsonValue jsonValue : array) {
if (snippet.terminate()) {
break;
}
write(jsonValue);
}
generator.writeEnd();
generator.flush();
}
private void write(final JsonObject object) {
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.terminate()) {
break;
}
write(entry.getKey(), entry.getValue());
}
generator.writeEnd();
generator.flush();
}
private void write(final String name, final JsonValue value) {
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.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.terminate()) {
break;
}
write(keyval.getKey(), keyval.getValue());
}
generator.writeEnd();
generator.flush();
break;
default: {
generator.write(name, value);
generator.flush();
}
}
}
private String get() {
generator.flush();
return snippet.isTruncated() ? snippet.get() + "..." : snippet.get();
}
@Override
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.
*
* 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 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();
}
public void print(final String string) {
try {
mode.write(string.getBytes());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
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;
}
@Override
public void write(final int b) throws IOException {
if (++count < max) {
out.write(b);
} else {
maxReached(new Truncated());
}
}
@Override
public void write(final byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(final byte[] b, 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;
out.write(b, off, remaining);
maxReached(new Completed());
} else if (len > remaining) {
count += len;
out.write(b, off, remaining);
maxReached(new Truncated());
} else {
count += len;
out.write(b, off, len);
}
}
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 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 {
}
}
}
}