blob: a0dad930c93de4fd7072003819bf8d7b901e5128 [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.logging.log4j.layout.json.template.util;
import org.apache.logging.log4j.util.BiConsumer;
import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import org.apache.logging.log4j.util.StringMap;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A simple JSON writer with support for common Java data types.
* <p>
* The following types have specific handlers:
* <p>
* <ul>
* <li> <tt>null</tt> input
* <li>{@link Map}, {@link IndexedReadOnlyStringMap}, {@link StringMap}
* <li>{@link Collection} and {@link List}
* <li>{@link Number} ({@link BigDecimal}, {@link BigInteger}, {@link Float},
* {@link Double}, {@link Byte}, {@link Short}, {@link Integer}, and
* {@link Long})
* <li>{@link Boolean}
* <li>{@link StringBuilderFormattable}
* <li>arrays of primitve types
* <tt>char/boolean/byte/short/int/long/float/double</tt> and {@link Object}
* <li>{@link CharSequence} and <tt>char[]</tt> with necessary escaping
* </ul>
* <p>
* JSON standard quoting routines are borrowed from
* <a href="https://github.com/FasterXML/jackson-core">Jackson</a>.
*/
public final class JsonWriter implements AutoCloseable, Cloneable {
private final static char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
/**
* Lookup table used for determining which output characters in 7-bit ASCII
* range (i.e., first 128 Unicode code points, single-byte UTF-8 characters)
* need to be quoted.
*<p>
* Value of 0 means "no escaping"; other positive values, that value is
* character to use after backslash; and negative values, that generic
* (backslash - u) escaping is to be used.
*/
private final static int[] ESC_CODES;
static {
int[] table = new int[128];
// Control chars need generic escape sequence
for (int i = 0; i < 32; ++i) {
// 04-Mar-2011, tatu: Used to use "-(i + 1)", replaced with constant
table[i] = -1;
}
// Others (and some within that range too) have explicit shorter sequences
table['"'] = '"';
table['\\'] = '\\';
// Escaping of slash is optional, so let's not add it
table[0x08] = 'b';
table[0x09] = 't';
table[0x0C] = 'f';
table[0x0A] = 'n';
table[0x0D] = 'r';
ESC_CODES = table;
}
private final char[] quoteBuffer;
private final StringBuilder stringBuilder;
private final StringBuilder formattableBuffer;
private final int maxStringLength;
private final String truncatedStringSuffix;
private final String quotedTruncatedStringSuffix;
private JsonWriter(final Builder builder) {
this.quoteBuffer = new char[]{'\\', '-', '0', '0', '-', '-'};
this.stringBuilder = new StringBuilder();
this.formattableBuffer = new StringBuilder();
this.maxStringLength = builder.maxStringLength;
this.truncatedStringSuffix = builder.truncatedStringSuffix;
this.quotedTruncatedStringSuffix = quoteString(builder.truncatedStringSuffix);
}
private String quoteString(final String string) {
final int startIndex = stringBuilder.length();
quoteString(string, 0, string.length());
final StringBuilder quotedStringBuilder = new StringBuilder();
quotedStringBuilder.append(stringBuilder, startIndex, stringBuilder.length());
final String quotedString = quotedStringBuilder.toString();
stringBuilder.setLength(startIndex);
return quotedString;
}
public String use(Runnable runnable) {
final int startIndex = stringBuilder.length();
runnable.run();
final StringBuilder sliceStringBuilder = new StringBuilder();
sliceStringBuilder.append(stringBuilder, startIndex, stringBuilder.length());
stringBuilder.setLength(startIndex);
return sliceStringBuilder.toString();
}
public StringBuilder getStringBuilder() {
return stringBuilder;
}
public int getMaxStringLength() {
return maxStringLength;
}
public String getTruncatedStringSuffix() {
return truncatedStringSuffix;
}
public void writeValue(final Object value) {
// null
if (value == null) {
writeNull();
}
// map
else if (value instanceof IndexedReadOnlyStringMap) {
final IndexedReadOnlyStringMap map = (IndexedReadOnlyStringMap) value;
writeObject(map);
} else if (value instanceof StringMap) {
final StringMap map = (StringMap) value;
writeObject(map);
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
final Map<String, Object> map = (Map<String, Object>) value;
writeObject(map);
}
// list & collection
else if (value instanceof List) {
@SuppressWarnings("unchecked")
final List<Object> list = (List<Object>) value;
writeArray(list);
} else if (value instanceof Collection) {
@SuppressWarnings("unchecked")
final Collection<Object> collection = (Collection<Object>) value;
writeArray(collection);
}
// number & boolean
else if (value instanceof Number) {
final Number number = (Number) value;
writeNumber(number);
} else if (value instanceof Boolean) {
final boolean booleanValue = (boolean) value;
writeBoolean(booleanValue);
}
// formattable
else if (value instanceof StringBuilderFormattable) {
final StringBuilderFormattable formattable = (StringBuilderFormattable) value;
writeString(formattable);
}
// arrays
else if (value instanceof char[]) {
final char[] charValues = (char[]) value;
writeArray(charValues);
} else if (value instanceof boolean[]) {
final boolean[] booleanValues = (boolean[]) value;
writeArray(booleanValues);
} else if (value instanceof byte[]) {
final byte[] byteValues = (byte[]) value;
writeArray(byteValues);
} else if (value instanceof short[]) {
final short[] shortValues = (short[]) value;
writeArray(shortValues);
} else if (value instanceof int[]) {
final int[] intValues = (int[]) value;
writeArray(intValues);
} else if (value instanceof long[]) {
final long[] longValues = (long[]) value;
writeArray(longValues);
} else if (value instanceof float[]) {
final float[] floatValues = (float[]) value;
writeArray(floatValues);
} else if (value instanceof double[]) {
final double[] doubleValues = (double[]) value;
writeArray(doubleValues);
} else if (value instanceof Object[]) {
final Object[] values = (Object[]) value;
writeArray(values);
}
// string
else {
final String stringValue = value instanceof String
? (String) value
: String.valueOf(value);
writeString(stringValue);
}
}
public void writeObject(final StringMap map) {
if (map == null) {
writeNull();
} else {
writeObjectStart();
final boolean[] firstEntry = {true};
map.forEach((final String key, final Object value) -> {
if (key == null) {
throw new IllegalArgumentException("null keys are not allowed");
}
if (firstEntry[0]) {
firstEntry[0] = false;
} else {
writeSeparator();
}
writeObjectKey(key);
writeValue(value);
});
writeObjectEnd();
}
}
public void writeObject(final IndexedReadOnlyStringMap map) {
if (map == null) {
writeNull();
} else {
writeObjectStart();
for (int entryIndex = 0; entryIndex < map.size(); entryIndex++) {
final String key = map.getKeyAt(entryIndex);
final Object value = map.getValueAt(entryIndex);
if (entryIndex > 0) {
writeSeparator();
}
writeObjectKey(key);
writeValue(value);
}
writeObjectEnd();
}
}
public void writeObject(final Map<String, Object> map) {
if (map == null) {
writeNull();
} else {
writeObjectStart();
final boolean[] firstEntry = {true};
map.forEach((final String key, final Object value) -> {
if (key == null) {
throw new IllegalArgumentException("null keys are not allowed");
}
if (firstEntry[0]) {
firstEntry[0] = false;
} else {
writeSeparator();
}
writeObjectKey(key);
writeValue(value);
});
writeObjectEnd();
}
}
public void writeObjectStart() {
stringBuilder.append('{');
}
public void writeObjectEnd() {
stringBuilder.append('}');
}
public void writeObjectKey(final CharSequence key) {
writeString(key);
stringBuilder.append(':');
}
public void writeArray(final List<Object> items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final Object item = items.get(itemIndex);
writeValue(item);
}
writeArrayEnd();
}
}
public void writeArray(final Collection<Object> items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
final boolean[] firstItem = {true};
items.forEach((final Object item) -> {
if (firstItem[0]) {
firstItem[0] = false;
} else {
writeSeparator();
}
writeValue(item);
});
writeArrayEnd();
}
}
public void writeArray(final char[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
stringBuilder.append('"');
quoteString(items, itemIndex, 1);
stringBuilder.append('"');
}
writeArrayEnd();
}
}
public void writeArray(final boolean[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final boolean item = items[itemIndex];
writeBoolean(item);
}
writeArrayEnd();
}
}
public void writeArray(final byte[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final byte item = items[itemIndex];
writeNumber(item);
}
writeArrayEnd();
}
}
public void writeArray(final short[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final short item = items[itemIndex];
writeNumber(item);
}
writeArrayEnd();
}
}
public void writeArray(final int[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final int item = items[itemIndex];
writeNumber(item);
}
writeArrayEnd();
}
}
public void writeArray(final long[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final long item = items[itemIndex];
writeNumber(item);
}
writeArrayEnd();
}
}
public void writeArray(final float[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final float item = items[itemIndex];
writeNumber(item);
}
writeArrayEnd();
}
}
public void writeArray(final double[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final double item = items[itemIndex];
writeNumber(item);
}
writeArrayEnd();
}
}
public void writeArray(final Object[] items) {
if (items == null) {
writeNull();
} else {
writeArrayStart();
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
if (itemIndex > 0) {
writeSeparator();
}
final Object item = items[itemIndex];
writeValue(item);
}
writeArrayEnd();
}
}
public void writeArrayStart() {
stringBuilder.append('[');
}
public void writeArrayEnd() {
stringBuilder.append(']');
}
public void writeSeparator() {
stringBuilder.append(',');
}
public <S> void writeString(
final BiConsumer<StringBuilder, S> emitter,
final S state) {
Objects.requireNonNull(emitter, "emitter");
stringBuilder.append('"');
formattableBuffer.setLength(0);
emitter.accept(formattableBuffer, state);
final int length = formattableBuffer.length();
// Handle max. string length complying input.
if (length <= maxStringLength) {
quoteString(formattableBuffer, 0, length);
}
// Handle max. string length violating input.
else {
quoteString(formattableBuffer, 0, maxStringLength);
stringBuilder.append(quotedTruncatedStringSuffix);
}
stringBuilder.append('"');
}
public void writeString(final StringBuilderFormattable formattable) {
if (formattable == null) {
writeNull();
} else {
stringBuilder.append('"');
formattableBuffer.setLength(0);
formattable.formatTo(formattableBuffer);
final int length = formattableBuffer.length();
// Handle max. string length complying input.
if (length <= maxStringLength) {
quoteString(formattableBuffer, 0, length);
}
// Handle max. string length violating input.
else {
quoteString(formattableBuffer, 0, maxStringLength);
stringBuilder.append(quotedTruncatedStringSuffix);
}
stringBuilder.append('"');
}
}
public void writeString(final CharSequence seq) {
if (seq == null) {
writeNull();
} else {
writeString(seq, 0, seq.length());
}
}
public void writeString(
final CharSequence seq,
final int offset,
final int length) {
// Handle null input.
if (seq == null) {
writeNull();
return;
}
// Check arguments.
if (offset < 0) {
throw new IllegalArgumentException(
"was expecting a positive offset: " + offset);
}
if (length < 0) {
throw new IllegalArgumentException(
"was expecting a positive length: " + length);
}
stringBuilder.append('"');
// Handle max. string length complying input.
if (length <= maxStringLength) {
quoteString(seq, offset, length);
}
// Handle max. string length violating input.
else {
quoteString(seq, offset, maxStringLength);
stringBuilder.append(quotedTruncatedStringSuffix);
}
stringBuilder.append('"');
}
/**
* Quote text contents using JSON standard quoting.
*/
private void quoteString(
final CharSequence seq,
final int offset,
final int length) {
final int limit = offset + length;
int i = offset;
outer:
while (i < limit) {
while (true) {
final char c = seq.charAt(i);
if (c < ESC_CODES.length && ESC_CODES[c] != 0) {
break;
}
stringBuilder.append(c);
if (++i >= limit) {
break outer;
}
}
final char d = seq.charAt(i++);
final int escCode = ESC_CODES[d];
final int quoteBufferLength = escCode < 0
? quoteNumeric(d)
: quoteNamed(escCode);
stringBuilder.append(quoteBuffer, 0, quoteBufferLength);
}
}
public void writeString(final char[] buffer) {
if (buffer == null) {
writeNull();
} else {
writeString(buffer, 0, buffer.length);
}
}
public void writeString(
final char[] buffer,
final int offset,
final int length) {
// Handle null input.
if (buffer == null) {
writeNull();
return;
}
// Check arguments.
if (offset < 0) {
throw new IllegalArgumentException(
"was expecting a positive offset: " + offset);
}
if (length < 0) {
throw new IllegalArgumentException(
"was expecting a positive length: " + length);
}
stringBuilder.append('"');
// Handle max. string length complying input.
if (length <= maxStringLength) {
quoteString(buffer, offset, length);
}
// Handle max. string length violating input.
else {
quoteString(buffer, offset, maxStringLength);
stringBuilder.append(quotedTruncatedStringSuffix);
}
stringBuilder.append('"');
}
/**
* Quote text contents using JSON standard quoting.
*/
private void quoteString(
final char[] buffer,
final int offset,
final int length) {
final int limit = offset + length;
int i = offset;
outer:
while (i < limit) {
while (true) {
final char c = buffer[i];
if (c < ESC_CODES.length && ESC_CODES[c] != 0) {
break;
}
stringBuilder.append(c);
if (++i >= limit) {
break outer;
}
}
final char d = buffer[i++];
final int escCode = ESC_CODES[d];
final int quoteBufferLength = escCode < 0
? quoteNumeric(d)
: quoteNamed(escCode);
stringBuilder.append(quoteBuffer, 0, quoteBufferLength);
}
}
private int quoteNumeric(final int value) {
quoteBuffer[1] = 'u';
// We know it's a control char, so only the last 2 chars are non-0
quoteBuffer[4] = HEX_CHARS[value >> 4];
quoteBuffer[5] = HEX_CHARS[value & 0xF];
return 6;
}
private int quoteNamed(final int esc) {
quoteBuffer[1] = (char) esc;
return 2;
}
private void writeNumber(final Number number) {
if (number instanceof BigDecimal) {
final BigDecimal decimalNumber = (BigDecimal) number;
writeNumber(decimalNumber);
} else if (number instanceof BigInteger) {
final BigInteger integerNumber = (BigInteger) number;
writeNumber(integerNumber);
} else if (number instanceof Double) {
final double doubleNumber = (Double) number;
writeNumber(doubleNumber);
} else if (number instanceof Float) {
final float floatNumber = (float) number;
writeNumber(floatNumber);
} else if (number instanceof Byte ||
number instanceof Short ||
number instanceof Integer ||
number instanceof Long) {
final long longNumber = number.longValue();
writeNumber(longNumber);
} else {
final long longNumber = number.longValue();
final double doubleValue = number.doubleValue();
if (Double.compare(longNumber, doubleValue) == 0) {
writeNumber(longNumber);
} else {
writeNumber(doubleValue);
}
}
}
public void writeNumber(final BigDecimal number) {
if (number == null) {
writeNull();
} else {
stringBuilder.append(number);
}
}
public void writeNumber(final BigInteger number) {
if (number == null) {
writeNull();
} else {
stringBuilder.append(number);
}
}
public void writeNumber(final float number) {
stringBuilder.append(number);
}
public void writeNumber(final double number) {
stringBuilder.append(number);
}
public void writeNumber(final short number) {
stringBuilder.append(number);
}
public void writeNumber(final int number) {
stringBuilder.append(number);
}
public void writeNumber(final long number) {
stringBuilder.append(number);
}
public void writeNumber(final long integralPart, final long fractionalPart) {
if (fractionalPart < 0) {
throw new IllegalArgumentException(
"was expecting a positive fraction: " + fractionalPart);
}
stringBuilder.append(integralPart);
if (fractionalPart != 0) {
stringBuilder.append('.');
stringBuilder.append(fractionalPart);
}
}
public void writeBoolean(final boolean value) {
writeRawString(value ? "true" : "false");
}
public void writeNull() {
writeRawString("null");
}
public void writeRawString(final CharSequence seq) {
Objects.requireNonNull(seq, "seq");
writeRawString(seq, 0, seq.length());
}
public void writeRawString(
final CharSequence seq,
final int offset,
final int length) {
// Check arguments.
Objects.requireNonNull(seq, "seq");
if (offset < 0) {
throw new IllegalArgumentException(
"was expecting a positive offset: " + offset);
}
if (length < 0) {
throw new IllegalArgumentException(
"was expecting a positive length: " + length);
}
// Write characters.
final int limit = offset + length;
stringBuilder.append(seq, offset, limit);
}
public void writeRawString(final char[] buffer) {
Objects.requireNonNull(buffer, "buffer");
writeRawString(buffer, 0, buffer.length);
}
public void writeRawString(
final char[] buffer,
final int offset,
final int length) {
// Check arguments.
Objects.requireNonNull(buffer, "buffer");
if (offset < 0) {
throw new IllegalArgumentException(
"was expecting a positive offset: " + offset);
}
if (length < 0) {
throw new IllegalArgumentException(
"was expecting a positive length: " + length);
}
// Write characters.
stringBuilder.append(buffer, offset, length);
}
@Override
public void close() {
stringBuilder.setLength(0);
}
@Override
@SuppressWarnings("MethodDoesntCallSuperMethod")
public JsonWriter clone() {
final JsonWriter jsonWriter = newBuilder()
.setMaxStringLength(maxStringLength)
.setTruncatedStringSuffix(truncatedStringSuffix)
.build();
jsonWriter.stringBuilder.append(stringBuilder);
return jsonWriter;
}
public static Builder newBuilder() {
return new Builder();
}
public static final class Builder {
private int maxStringLength;
private String truncatedStringSuffix;
public int getMaxStringLength() {
return maxStringLength;
}
public Builder setMaxStringLength(final int maxStringLength) {
this.maxStringLength = maxStringLength;
return this;
}
public String getTruncatedStringSuffix() {
return truncatedStringSuffix;
}
public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) {
this.truncatedStringSuffix = truncatedStringSuffix;
return this;
}
public JsonWriter build() {
validate();
return new JsonWriter(this);
}
private void validate() {
if (maxStringLength <= 0) {
throw new IllegalArgumentException(
"was expecting maxStringLength > 0: " +
maxStringLength);
}
Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
}
}
}