/**
* Copyright 2015-2017 The OpenZipkin Authors
*
* Licensed 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 zipkin.internal;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.MalformedJsonException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import zipkin.Annotation;
import zipkin.BinaryAnnotation;
import zipkin.BinaryAnnotation.Type;
import zipkin.Codec;
import zipkin.DependencyLink;
import zipkin.Endpoint;
import zipkin.Span;
import static java.lang.Double.doubleToRawLongBits;
import static zipkin.internal.Buffer.asciiSizeInBytes;
import static zipkin.internal.Buffer.base64UrlSizeInBytes;
import static zipkin.internal.Buffer.ipv6SizeInBytes;
import static zipkin.internal.Buffer.jsonEscapedSizeInBytes;
import static zipkin.internal.Buffer.utf8SizeInBytes;
import static zipkin.internal.Util.UTF_8;
import static zipkin.internal.Util.assertionError;
import static zipkin.internal.Util.checkArgument;
import static zipkin.internal.Util.lowerHexToUnsignedLong;
/**
* This explicitly constructs instances of model classes via manual parsing for a number of
* reasons.
*
* <ul>
* <li>Eliminates the need to keep separate model classes for thrift vs json</li>
* <li>Avoids magic field initialization which, can miss constructor guards</li>
* <li>Allows us to safely re-use the json form in toString methods</li>
* <li>Encourages logic to be based on the thrift shape of objects</li>
* <li>Ensures the order and naming of the fields in json is stable</li>
* </ul>
*
* <p> 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 implements Codec {
static final long MAX_SAFE_INTEGER = 9007199254740991L; // 53 bits
static final String ENDPOINT_HEADER = ",\"endpoint\":";
static final JsonAdapter<Endpoint> ENDPOINT_ADAPTER = new JsonAdapter<Endpoint>() {
@Override
public Endpoint fromJson(JsonReader reader) throws IOException {
Endpoint.Builder result = Endpoint.builder();
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
if (nextName.equals("serviceName")) {
result.serviceName(reader.nextString());
} else if (nextName.equals("ipv4") || nextName.equals("ipv6")) {
result.parseIp(reader.nextString());
} else if (nextName.equals("port")) {
result.port(reader.nextInt());
} else {
reader.skipValue();
}
}
reader.endObject();
return result.build();
}
@Override public int sizeInBytes(Endpoint value) {
int sizeInBytes = 0;
sizeInBytes += asciiSizeInBytes("{\"serviceName\":\"");
sizeInBytes += jsonEscapedSizeInBytes(value.serviceName) + 1; // for end quote
if (value.ipv4 != 0) {
sizeInBytes += asciiSizeInBytes(",\"ipv4\":\"");
sizeInBytes += asciiSizeInBytes(value.ipv4 >> 24 & 0xff) + 1; // for dot
sizeInBytes += asciiSizeInBytes(value.ipv4 >> 16 & 0xff) + 1; // for dot
sizeInBytes += asciiSizeInBytes(value.ipv4 >> 8 & 0xff) + 1; // for dot
sizeInBytes += asciiSizeInBytes(value.ipv4 & 0xff) + 1; // for end quote
}
if (value.port != null && value.port != 0) {
sizeInBytes += asciiSizeInBytes(",\"port\":") + asciiSizeInBytes(value.port & 0xffff);
}
if (value.ipv6 != null) {
sizeInBytes += asciiSizeInBytes(",\"ipv6\":\"") + ipv6SizeInBytes(value.ipv6) + 1;
}
return ++sizeInBytes;// end curly-brace
}
@Override public void write(Endpoint value, Buffer b) {
b.writeAscii("{\"serviceName\":\"");
b.writeJsonEscaped(value.serviceName).writeByte('"');
if (value.ipv4 != 0) {
b.writeAscii(",\"ipv4\":\"");
b.writeAscii(value.ipv4 >> 24 & 0xff).writeByte('.');
b.writeAscii(value.ipv4 >> 16 & 0xff).writeByte('.');
b.writeAscii(value.ipv4 >> 8 & 0xff).writeByte('.');
b.writeAscii(value.ipv4 & 0xff).writeByte('"');
}
if (value.port != null && value.port != 0) {
b.writeAscii(",\"port\":").writeAscii(value.port & 0xffff);
}
if (value.ipv6 != null) {
b.writeAscii(",\"ipv6\":\"").writeIpV6(value.ipv6).writeByte('"');
}
b.writeByte('}');
}
};
static final JsonAdapter<Annotation> ANNOTATION_ADAPTER = new JsonAdapter<Annotation>() {
@Override
public Annotation fromJson(JsonReader reader) throws IOException {
Annotation.Builder result = Annotation.builder();
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
if (nextName.equals("timestamp")) {
result.timestamp(reader.nextLong());
} else if (nextName.equals("value")) {
result.value(reader.nextString());
} else if (nextName.equals("endpoint") && reader.peek() != JsonToken.NULL) {
result.endpoint(ENDPOINT_ADAPTER.fromJson(reader));
} else {
reader.skipValue();
}
}
reader.endObject();
return result.build();
}
@Override public int sizeInBytes(Annotation value) {
int sizeInBytes = 0;
sizeInBytes += asciiSizeInBytes("{\"timestamp\":") + asciiSizeInBytes(value.timestamp);
sizeInBytes += asciiSizeInBytes(",\"value\":\"") + jsonEscapedSizeInBytes(value.value) + 1;
if (value.endpoint != null) {
sizeInBytes += ENDPOINT_HEADER.length() + ENDPOINT_ADAPTER.sizeInBytes(value.endpoint);
}
return ++sizeInBytes;// end curly-brace
}
@Override public void write(Annotation value, Buffer b) {
b.writeAscii("{\"timestamp\":").writeAscii(value.timestamp);
b.writeAscii(",\"value\":\"").writeJsonEscaped(value.value).writeByte('"');
if (value.endpoint != null) {
b.writeAscii(ENDPOINT_HEADER);
ENDPOINT_ADAPTER.write(value.endpoint, b);
}
b.writeByte('}');
}
};
static final JsonAdapter<BinaryAnnotation> BINARY_ANNOTATION_ADAPTER = new JsonAdapter<BinaryAnnotation>() {
@Override
public BinaryAnnotation fromJson(JsonReader reader) throws IOException {
BinaryAnnotation.Builder result = BinaryAnnotation.builder();
String key = null;
Type type = Type.STRING;
boolean valueSet = false;
String number = null;
String string = null;
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
if (nextName.equals("key")) {
result.key(key = reader.nextString());
} else if (nextName.equals("value")) {
valueSet = true;
switch (reader.peek()) {
case BOOLEAN:
type = Type.BOOL;
result.value(reader.nextBoolean() ? new byte[] {1} : new byte[] {0});
break;
case STRING:
string = reader.nextString();
break;
case NUMBER:
number = reader.nextString();
break;
default:
throw new MalformedJsonException(
"Expected value to be a boolean, string or number but was " + reader.peek()
+ " at path " + reader.getPath());
}
} else if (nextName.equals("type")) {
type = Type.valueOf(reader.nextString());
} else if (nextName.equals("endpoint") && reader.peek() != JsonToken.NULL) {
result.endpoint(ENDPOINT_ADAPTER.fromJson(reader));
} else {
reader.skipValue();
}
}
if (key == null) {
throw new MalformedJsonException("No key at " + reader.getPath());
} else if (!valueSet) {
throw new MalformedJsonException("No value for key " + key + " at " + reader.getPath());
}
reader.endObject();
result.type(type);
switch (type) {
case BOOL:
return result.build();
case STRING:
return result.value(string.getBytes(UTF_8)).build();
case BYTES:
return result.value(Base64.decode(string)).build();
default:
break;
}
final byte[] value;
if (type == Type.I16) {
short v = Short.parseShort(number);
value = ByteBuffer.allocate(2).putShort(0, v).array();
} else if (type == Type.I32) {
int v = Integer.parseInt(number);
value = ByteBuffer.allocate(4).putInt(0, v).array();
} else if (type == Type.I64 || type == Type.DOUBLE) {
if (number == null) number = string;
long v = type == Type.I64
? Long.parseLong(number)
: doubleToRawLongBits(Double.parseDouble(number));
value = ByteBuffer.allocate(8).putLong(0, v).array();
} else {
throw new AssertionError("BinaryAnnotationType " + type + " was added, but not handled");
}
return result.value(value).build();
}
@Override public int sizeInBytes(BinaryAnnotation value) {
int sizeInBytes = 0;
sizeInBytes += asciiSizeInBytes("{\"key\":\"") + jsonEscapedSizeInBytes(value.key);
sizeInBytes += asciiSizeInBytes("\",\"value\":");
switch (value.type) {
case BOOL:
sizeInBytes += asciiSizeInBytes(value.value[0] == 1 ? "true" : "false");
break;
case STRING:
sizeInBytes += jsonEscapedSizeInBytes(value.value) + 2; //for quotes
break;
case BYTES:
sizeInBytes += base64UrlSizeInBytes(value.value) +2; //for quotes
break;
case I16:
sizeInBytes += asciiSizeInBytes(ByteBuffer.wrap(value.value).getShort());
break;
case I32:
sizeInBytes += asciiSizeInBytes(ByteBuffer.wrap(value.value).getInt());
break;
case I64:
long number = ByteBuffer.wrap(value.value).getLong();
sizeInBytes += asciiSizeInBytes(number);
if (number > MAX_SAFE_INTEGER) sizeInBytes += 2; //for quotes
break;
case DOUBLE:
double wrapped = Double.longBitsToDouble(ByteBuffer.wrap(value.value).getLong());
sizeInBytes += asciiSizeInBytes(Double.toString(wrapped));
break;
default:
}
if (value.type != BinaryAnnotation.Type.STRING && value.type != BinaryAnnotation.Type.BOOL) {
sizeInBytes += asciiSizeInBytes(",\"type\":\"") + utf8SizeInBytes(value.type.name()) + 1;
}
if (value.endpoint != null) {
sizeInBytes += ENDPOINT_HEADER.length() + ENDPOINT_ADAPTER.sizeInBytes(value.endpoint);
}
return ++sizeInBytes;// end curly-brace
}
@Override public void write(BinaryAnnotation value, Buffer b) {
b.writeAscii("{\"key\":\"").writeJsonEscaped(value.key);
b.writeAscii("\",\"value\":");
switch (value.type) {
case BOOL:
b.writeAscii(value.value[0] == 1 ? "true" : "false");
break;
case STRING:
b.writeByte('"').writeJsonEscaped(value.value).writeByte('"');
break;
case BYTES:
b.writeByte('"').writeBase64Url(value.value).writeByte('"');
break;
case I16:
b.writeAscii(ByteBuffer.wrap(value.value).getShort());
break;
case I32:
b.writeAscii(ByteBuffer.wrap(value.value).getInt());
break;
case I64:
long number = ByteBuffer.wrap(value.value).getLong();
if (number > MAX_SAFE_INTEGER) b.writeByte('"');
b.writeAscii(number);
if (number > MAX_SAFE_INTEGER) b.writeByte('"');
break;
case DOUBLE:
double wrapped = Double.longBitsToDouble(ByteBuffer.wrap(value.value).getLong());
b.writeAscii(Double.toString(wrapped));
break;
default:
}
if (value.type != BinaryAnnotation.Type.STRING && value.type != BinaryAnnotation.Type.BOOL) {
b.writeAscii(",\"type\":\"").writeAscii(value.type.name()).writeByte('"');
}
if (value.endpoint != null) {
b.writeAscii(ENDPOINT_HEADER);
ENDPOINT_ADAPTER.write(value.endpoint, b);
}
b.writeByte('}');
}
};
static final JsonAdapter<Span> SPAN_ADAPTER = new JsonAdapter<Span>() {
@Override
public Span fromJson(JsonReader reader) throws IOException {
Span.Builder result = Span.builder();
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
if (nextName.equals("traceId")) {
String traceId = reader.nextString();
if (traceId.length() == 32) {
result.traceIdHigh(lowerHexToUnsignedLong(traceId, 0));
}
result.traceId(lowerHexToUnsignedLong(traceId));
} else if (nextName.equals("name")) {
result.name(reader.nextString());
} else if (nextName.equals("id")) {
result.id(lowerHexToUnsignedLong(reader.nextString()));
} else if (nextName.equals("parentId") && reader.peek() != JsonToken.NULL) {
result.parentId(lowerHexToUnsignedLong(reader.nextString()));
} else if (nextName.equals("timestamp") && reader.peek() != JsonToken.NULL) {
result.timestamp(reader.nextLong());
} else if (nextName.equals("duration") && reader.peek() != JsonToken.NULL) {
result.duration(reader.nextLong());
} else if (nextName.equals("annotations")) {
reader.beginArray();
while (reader.hasNext()) {
result.addAnnotation(ANNOTATION_ADAPTER.fromJson(reader));
}
reader.endArray();
} else if (nextName.equals("binaryAnnotations")) {
reader.beginArray();
while (reader.hasNext()) {
result.addBinaryAnnotation(BINARY_ANNOTATION_ADAPTER.fromJson(reader));
}
reader.endArray();
} else if (nextName.equals("debug") && reader.peek() != JsonToken.NULL) {
if (reader.nextBoolean()) result.debug(true);
} else {
reader.skipValue();
}
}
reader.endObject();
return result.build();
}
@Override public int sizeInBytes(Span value) {
int sizeInBytes = 0;
if (value.traceIdHigh != 0) sizeInBytes += 16;
sizeInBytes += asciiSizeInBytes("{\"traceId\":\"") + 16; // fixed-width hex
sizeInBytes += asciiSizeInBytes("\",\"id\":\"") + 16;
sizeInBytes += asciiSizeInBytes("\",\"name\":\"") + jsonEscapedSizeInBytes(value.name) + 1;
if (value.parentId != null) {
sizeInBytes += asciiSizeInBytes(",\"parentId\":\"") + 16 + 1;
}
if (value.timestamp != null) {
sizeInBytes += asciiSizeInBytes(",\"timestamp\":") + asciiSizeInBytes(value.timestamp);
}
if (value.duration != null) {
sizeInBytes += asciiSizeInBytes(",\"duration\":") + asciiSizeInBytes(value.duration);
}
if (!value.annotations.isEmpty()) {
sizeInBytes += asciiSizeInBytes(",\"annotations\":");
sizeInBytes += JsonCodec.sizeInBytes(ANNOTATION_ADAPTER, value.annotations);
}
if (!value.binaryAnnotations.isEmpty()) {
sizeInBytes += asciiSizeInBytes(",\"binaryAnnotations\":");
sizeInBytes += JsonCodec.sizeInBytes(BINARY_ANNOTATION_ADAPTER, value.binaryAnnotations);
}
if (value.debug != null && value.debug) {
sizeInBytes += asciiSizeInBytes(",\"debug\":true");
}
return ++sizeInBytes;// end curly-brace
}
@Override public void write(Span value, Buffer b) {
b.writeAscii("{\"traceId\":\"");
if (value.traceIdHigh != 0) {
b.writeLowerHex(value.traceIdHigh);
}
b.writeLowerHex(value.traceId);
b.writeAscii("\",\"id\":\"").writeLowerHex(value.id);
b.writeAscii("\",\"name\":\"").writeJsonEscaped(value.name).writeByte('"');
if (value.parentId != null) {
b.writeAscii(",\"parentId\":\"").writeLowerHex(value.parentId).writeByte('"');
}
if (value.timestamp != null) {
b.writeAscii(",\"timestamp\":").writeAscii(value.timestamp);
}
if (value.duration != null) {
b.writeAscii(",\"duration\":").writeAscii(value.duration);
}
if (!value.annotations.isEmpty()) {
b.writeAscii(",\"annotations\":");
writeList(ANNOTATION_ADAPTER, value.annotations, b);
}
if (!value.binaryAnnotations.isEmpty()) {
b.writeAscii(",\"binaryAnnotations\":");
writeList(BINARY_ANNOTATION_ADAPTER, value.binaryAnnotations, b);
}
if (value.debug != null && value.debug) {
b.writeAscii(",\"debug\":true");
}
b.writeByte('}');
}
@Override public String toString() {
return "Span";
}
};
@Override
public Span readSpan(byte[] bytes) {
checkArgument(bytes.length > 0, "Empty input reading Span");
try {
return SPAN_ADAPTER.fromJson(jsonReader(bytes));
} catch (Exception e) {
throw exceptionReading("Span", bytes, e);
}
}
@Override public int sizeInBytes(Span value) {
return SPAN_ADAPTER.sizeInBytes(value);
}
@Override
public byte[] writeSpan(Span value) {
return write(SPAN_ADAPTER, value);
}
/** Exposed for {@link Endpoint#toString()} */
public static byte[] writeEndpoint(Endpoint value) {
return write(ENDPOINT_ADAPTER, value);
}
@Override
public List<Span> readSpans(byte[] bytes) {
checkArgument(bytes.length > 0, "Empty input reading List<Span>");
return readList(SPAN_ADAPTER, bytes);
}
@Override
public byte[] writeSpans(List<Span> value) {
if (value.isEmpty()) return new byte[] {'[', ']'};
Buffer result = new Buffer(sizeInBytes(SPAN_ADAPTER, value));
writeList(SPAN_ADAPTER, value, result);
return result.toByteArray();
}
@Override
public byte[] writeTraces(List<List<Span>> traces) {
// Get the encoded size of the nested list so that we don't need to grow the buffer
int sizeInBytes = overheadInBytes(traces);
for (int i = 0; i < traces.size(); i++) {
List<Span> spans = traces.get(i);
sizeInBytes += overheadInBytes(spans);
for (int j = 0; j < spans.size(); j++) {
sizeInBytes += SPAN_ADAPTER.sizeInBytes(spans.get(j));
}
}
Buffer out = new Buffer(sizeInBytes);
out.writeByte('['); // start list of traces
for (Iterator<List<Span>> trace = traces.iterator(); trace.hasNext(); ) {
writeList(SPAN_ADAPTER, trace.next(), out);
if (trace.hasNext()) out.writeByte(',');
}
out.writeByte(']'); // stop list of traces
return out.toByteArray();
}
public List<List<Span>> readTraces(byte[] bytes) {
JsonReader reader = jsonReader(bytes);
List<List<Span>> result = new LinkedList<>(); // cause we don't know how long it will be
try {
reader.beginArray();
while (reader.hasNext()) {
reader.beginArray();
List<Span> trace = new LinkedList<>(); // cause we don't know how long it will be
while (reader.hasNext()) {
trace.add(SPAN_ADAPTER.fromJson(reader));
}
reader.endArray();
result.add(trace);
}
reader.endArray();
return result;
} catch (Exception e) {
throw exceptionReading("List<List<Span>>", bytes, e);
}
}
static final JsonAdapter<DependencyLink> DEPENDENCY_LINK_ADAPTER = new JsonAdapter<DependencyLink>() {
@Override
public DependencyLink fromJson(JsonReader reader) throws IOException {
DependencyLink.Builder result = DependencyLink.builder();
reader.beginObject();
while (reader.hasNext()) {
String nextName = reader.nextName();
if (nextName.equals("parent")) {
result.parent(reader.nextString());
} else if (nextName.equals("child")) {
result.child(reader.nextString());
} else if (nextName.equals("callCount")) {
result.callCount(reader.nextLong());
} else {
reader.skipValue();
}
}
reader.endObject();
return result.build();
}
@Override public int sizeInBytes(DependencyLink value) {
int sizeInBytes = 0;
sizeInBytes += asciiSizeInBytes("{\"parent\":\"") + jsonEscapedSizeInBytes(value.parent);
sizeInBytes += asciiSizeInBytes("\",\"child\":\"") + jsonEscapedSizeInBytes(value.child);
sizeInBytes += asciiSizeInBytes("\",\"callCount\":") + asciiSizeInBytes(value.callCount);
return ++sizeInBytes;// end curly-brace
}
@Override public void write(DependencyLink value, Buffer b) {
b.writeAscii("{\"parent\":\"").writeJsonEscaped(value.parent);
b.writeAscii("\",\"child\":\"").writeJsonEscaped(value.child);
b.writeAscii("\",\"callCount\":").writeAscii(value.callCount).writeByte('}');
}
@Override public String toString() {
return "DependencyLink";
}
};
@Override
public DependencyLink readDependencyLink(byte[] bytes) {
checkArgument(bytes.length > 0, "Empty input reading DependencyLink");
try {
return DEPENDENCY_LINK_ADAPTER.fromJson(jsonReader(bytes));
} catch (Exception e) {
throw exceptionReading("Span", bytes, e);
}
}
@Override
public byte[] writeDependencyLink(DependencyLink value) {
return write(DEPENDENCY_LINK_ADAPTER, value);
}
@Override
public List<DependencyLink> readDependencyLinks(byte[] bytes) {
checkArgument(bytes.length > 0, "Empty input reading List<DependencyLink>");
return readList(DEPENDENCY_LINK_ADAPTER, bytes);
}
@Override
public byte[] writeDependencyLinks(List<DependencyLink> value) {
Buffer result = new Buffer(sizeInBytes(DEPENDENCY_LINK_ADAPTER, value));
writeList(DEPENDENCY_LINK_ADAPTER, value, result);
return result.toByteArray();
}
static final JsonAdapter<String> STRING_ADAPTER = new JsonAdapter<String>() {
@Override
public String fromJson(JsonReader reader) throws IOException {
return reader.nextString();
}
@Override public int sizeInBytes(String value) {
return jsonEscapedSizeInBytes(value) + 2; // For quotes
}
@Override public void write(String value, Buffer buffer) {
buffer.writeByte('"').writeJsonEscaped(value).writeByte('"');
}
};
public List<String> readStrings(byte[] bytes) {
checkArgument(bytes.length > 0, "Empty input reading List<String>");
return readList(STRING_ADAPTER, bytes);
}
public byte[] writeStrings(List<String> value) {
Buffer result = new Buffer(sizeInBytes(STRING_ADAPTER, value));
writeList(STRING_ADAPTER, value, result);
return result.toByteArray();
}
static <T> List<T> readList(JsonAdapter<T> adapter, byte[] bytes) {
JsonReader reader = jsonReader(bytes);
List<T> result;
try {
reader.beginArray();
if (reader.hasNext()) {
result = new LinkedList<>(); // cause we don't know how long it will be
} else {
result = Collections.emptyList();
}
while (reader.hasNext()) {
result.add(adapter.fromJson(reader));
}
reader.endArray();
return result;
} catch (Exception e) {
throw exceptionReading("List<" + adapter + ">", bytes, e);
}
}
private static JsonReader jsonReader(byte[] bytes) {
return new JsonReader(new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8));
}
/** Inability to encode is a programming bug. */
static <T> byte[] write(Buffer.Writer<T> writer, T value) {
Buffer b = new Buffer(writer.sizeInBytes(value));
try {
writer.write(value, b);
} catch (RuntimeException e) {
byte[] bytes = b.toByteArray();
int lengthWritten = bytes.length;
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] == 0) {
lengthWritten = i;
break;
}
}
final byte[] bytesWritten;
if (lengthWritten == bytes.length) {
bytesWritten = bytes;
} else {
bytesWritten = new byte[lengthWritten];
System.arraycopy(bytes, 0, bytesWritten, 0, lengthWritten);
}
String written = new String(bytesWritten, UTF_8);
// 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 = String.format(
"Bug found using %s to write %s as json. Wrote %s/%s bytes: %s",
writer.getClass().getSimpleName(), value.getClass().getSimpleName(), lengthWritten,
bytes.length, written);
throw assertionError(message, e);
}
return b.toByteArray();
}
static <T> int sizeInBytes(Buffer.Writer<T> writer, List<T> value) {
int sizeInBytes = overheadInBytes(value);
for (int i = 0, length = value.size(); i < length; i++) {
sizeInBytes += writer.sizeInBytes(value.get(i));
}
return sizeInBytes;
}
static <T> int overheadInBytes(List<T> value) {
int sizeInBytes = 2; // brackets
if (value.size() > 1) sizeInBytes += value.size() - 1; // comma to join elements
return sizeInBytes;
}
static <T> void writeList(Buffer.Writer<T> writer, List<T> value, Buffer 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, byte[] bytes, Exception e) {
String cause = e.getMessage() == null ? "Error" : e.getMessage();
if (cause.indexOf("malformed") != -1) cause = "Malformed";
String message = String.format("%s reading %s from json: %s", cause, type, new String(bytes, UTF_8));
throw new IllegalArgumentException(message, e);
}
static abstract class JsonAdapter<T> implements Buffer.Writer<T> {
abstract T fromJson(JsonReader reader) throws IOException;
}
}