zipkin2.internal.JsonCodec Maven / Gradle / Ivy
/*
* Copyright The OpenZipkin Authors
* SPDX-License-Identifier: Apache-2.0
*/
package zipkin2.internal;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static com.google.gson.stream.JsonToken.BOOLEAN;
import static com.google.gson.stream.JsonToken.NULL;
import static com.google.gson.stream.JsonToken.STRING;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* This explicitly constructs instances of model classes via manual parsing for a number of
* reasons.
*
*
* - Eliminates the need to keep separate model classes for proto3 vs json
*
- Avoids magic field initialization which, can miss constructor guards
*
- Allows us to safely re-use the json form in toString methods
*
- Encourages logic to be based on the json shape of objects
*
- Ensures the order and naming of the fields in json is stable
*
*
* There is the up-front cost of creating this, and maintenance of this to consider. However,
* this should be easy to justify as these objects don't change much at all.
*/
public final class JsonCodec {
// Hides gson types for internal use in other submodules
public static final class JsonReader {
final com.google.gson.stream.JsonReader delegate;
JsonReader(ReadBuffer buffer) {
delegate = new com.google.gson.stream.JsonReader(new InputStreamReader(buffer, UTF_8));
}
public void beginArray() throws IOException {
delegate.beginArray();
}
public boolean hasNext() throws IOException {
return delegate.hasNext();
}
public void endArray() throws IOException {
delegate.endArray();
}
public void beginObject() throws IOException {
delegate.beginObject();
}
public void endObject() throws IOException {
delegate.endObject();
}
public String nextName() throws IOException {
return delegate.nextName();
}
public String nextString() throws IOException {
return delegate.nextString();
}
public void skipValue() throws IOException {
delegate.skipValue();
}
public long nextLong() throws IOException {
return delegate.nextLong();
}
public String getPath() {
return delegate.getPath();
}
public boolean nextBoolean() throws IOException {
return delegate.nextBoolean();
}
public int nextInt() throws IOException {
return delegate.nextInt();
}
public boolean peekString() throws IOException {
return delegate.peek() == STRING;
}
public boolean peekBoolean() throws IOException {
return delegate.peek() == BOOLEAN;
}
public boolean peekNull() throws IOException {
return delegate.peek() == NULL;
}
@Override public String toString() {
return delegate.toString();
}
}
public interface JsonReaderAdapter {
T fromJson(JsonReader reader) throws IOException;
}
public static boolean read(
JsonReaderAdapter adapter, ReadBuffer buffer, Collection out) {
if (buffer.available() == 0) return false;
try {
out.add(adapter.fromJson(new JsonReader(buffer)));
return true;
} catch (Exception e) {
throw exceptionReading(adapter.toString(), e);
}
}
public static @Nullable T readOne(JsonReaderAdapter adapter, ReadBuffer buffer) {
List out = new ArrayList<>(1); // TODO: could make single-element list w/o array
if (!read(adapter, buffer, out)) return null;
return out.get(0);
}
public static boolean readList(
JsonReaderAdapter adapter, ReadBuffer buffer, Collection out) {
if (buffer.available() == 0) return false;
JsonReader reader = new JsonReader(buffer);
try {
reader.beginArray();
if (!reader.hasNext()) return false;
while (reader.hasNext()) out.add(adapter.fromJson(reader));
reader.endArray();
return true;
} catch (Exception e) {
throw exceptionReading("List<" + adapter + ">", e);
}
}
static int sizeInBytes(WriteBuffer.Writer writer, List value) {
int length = value.size();
int sizeInBytes = 2; // []
if (length > 1) sizeInBytes += length - 1; // comma to join elements
for (T t : value) {
sizeInBytes += writer.sizeInBytes(t);
}
return sizeInBytes;
}
/** Inability to encode is a programming bug. */
public static byte[] write(WriteBuffer.Writer writer, T value) {
byte[] result = new byte[writer.sizeInBytes(value)];
WriteBuffer b = WriteBuffer.wrap(result);
try {
writer.write(value, b);
} catch (RuntimeException e) {
int lengthWritten = result.length;
for (int i = 0; i < result.length; i++) {
if (result[i] == 0) {
lengthWritten = i;
break;
}
}
// Don't use value directly in the message, as its toString might be implemented using this
// method. If that's the case, we'd stack overflow. Instead, emit what we've written so far.
String message =
format(
"Bug found using %s to write %s as json. Wrote %s/%s bytes: %s",
writer.getClass().getSimpleName(),
value.getClass().getSimpleName(),
lengthWritten,
result.length,
new String(result, 0, lengthWritten, UTF_8));
throw new AssertionError(message, e);
}
return result;
}
public static byte[] writeList(WriteBuffer.Writer writer, List value) {
if (value.isEmpty()) return new byte[] {'[', ']'};
byte[] result = new byte[sizeInBytes(writer, value)];
writeList(writer, value, WriteBuffer.wrap(result));
return result;
}
public static int writeList(WriteBuffer.Writer writer, List value, byte[] out,
int pos) {
if (value.isEmpty()) {
out[pos++] = '[';
out[pos++] = ']';
return 2;
}
int initialPos = pos;
WriteBuffer result = WriteBuffer.wrap(out, pos);
writeList(writer, value, result);
return result.pos() - initialPos;
}
public static void writeList(WriteBuffer.Writer writer, List value, WriteBuffer b) {
b.writeByte('[');
for (int i = 0, length = value.size(); i < length; ) {
writer.write(value.get(i++), b);
if (i < length) b.writeByte(',');
}
b.writeByte(']');
}
static IllegalArgumentException exceptionReading(String type, Exception e) {
String cause = e.getMessage() == null ? "Error" : e.getMessage();
if (cause.contains("Expected BEGIN_OBJECT")
|| cause.contains("Expected BEGIN_ARRAY")
|| cause.contains("malformed")) {
cause = "Malformed";
}
String message = format("%s reading %s from json", cause, type);
throw new IllegalArgumentException(message, e);
}
}