GsonMessageReader.java
/*
* Copyright 2020 Providence Authors
*
* 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 net.morimekta.proto.gson.sio;
import com.google.gson.JsonParseException;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Duration;
import com.google.protobuf.Message;
import com.google.protobuf.Timestamp;
import net.morimekta.collect.ListBuilder;
import net.morimekta.collect.MapBuilder;
import net.morimekta.collect.UnmodifiableList;
import net.morimekta.collect.UnmodifiableMap;
import net.morimekta.proto.MorimektaOptions;
import net.morimekta.proto.ProtoEnum;
import net.morimekta.proto.ProtoMessageBuilder;
import net.morimekta.proto.gson.ProtoTypeOptions;
import net.morimekta.proto.sio.MessageReader;
import net.morimekta.proto.utils.GoogleTypesUtil;
import net.morimekta.strings.io.LineBufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import static java.lang.Boolean.parseBoolean;
import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;
import static net.morimekta.proto.gson.ProtoTypeOptions.Option.FAIL_ON_NULL_VALUE;
import static net.morimekta.proto.gson.ProtoTypeOptions.Option.FAIL_ON_UNKNOWN_FIELD;
import static net.morimekta.proto.gson.ProtoTypeOptions.Option.IGNORE_UNKNOWN_ANY_TYPE;
import static net.morimekta.proto.gson.ProtoTypeOptions.Option.LENIENT_READER;
import static net.morimekta.proto.gson.ProtoTypeOptions.Value.ANY_TYPE_FIELD_NAME;
import static net.morimekta.proto.utils.FieldUtil.getMapKeyDescriptor;
import static net.morimekta.proto.utils.FieldUtil.getMapValueDescriptor;
import static net.morimekta.proto.utils.JsonNameUtil.ANY_TYPE_FIELDS;
import static net.morimekta.proto.utils.JsonNameUtil.NUMERIC_FIELD_ID;
import static net.morimekta.proto.utils.JsonNameUtil.getJsonFieldMap;
import static net.morimekta.proto.utils.ProtoTypeRegistry.getTypeNameFromTypeUrl;
import static net.morimekta.proto.utils.ProtoTypeRegistry.getTypeUrl;
/**
* Reader for reading a JSON message from a stream.
*/
public class GsonMessageReader implements MessageReader, Closeable {
private final JsonReader reader;
private final ProtoTypeOptions options;
/**
* @param in Reader to read from.
* @param options Proto options.
*/
public GsonMessageReader(Reader in, ProtoTypeOptions options) {
this(makeJsonReader(in, options.isEnabled(LENIENT_READER)), options);
}
/**
* @param reader JSON Reader to read from.
* @param options Proto options.
*/
public GsonMessageReader(JsonReader reader, ProtoTypeOptions options) {
this.reader = reader;
this.options = options;
}
@Override
public void close() throws IOException {
reader.close();
}
@Override
public Message read(Descriptors.Descriptor descriptor) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
if (reader.peek() == JsonToken.BEGIN_ARRAY) {
if (!descriptor.getOptions().getExtension(MorimektaOptions.compact)) {
throw new JsonParseException("Compact format not allowed for " + descriptor.getFullName() + " at " + reader.getPath());
}
ProtoMessageBuilder builder = new ProtoMessageBuilder(descriptor);
List<Descriptors.FieldDescriptor> fields = descriptor.getFields();
reader.beginArray();
int idx = 0;
while (reader.peek() != JsonToken.END_ARRAY) {
if (idx >= fields.size()) {
if (options.isEnabled(FAIL_ON_NULL_VALUE)) {
throw new JsonParseException("Data after last field at " + reader.getPath());
}
reader.skipValue();
} else if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
} else {
Descriptors.FieldDescriptor field = fields.get(idx);
builder.set(field, readSingleValue(field));
}
idx++;
}
reader.endArray();
return builder.getMessage().build();
} else if (reader.peek() == JsonToken.BEGIN_OBJECT) {
ProtoMessageBuilder builder = new ProtoMessageBuilder(descriptor);
Map<String, Descriptors.FieldDescriptor> jsonFields = getJsonFieldMap(descriptor);
reader.beginObject();
boolean first = true;
while (reader.peek() != JsonToken.END_OBJECT) {
String name = reader.nextName();
if (first) {
first = false;
if (descriptor.equals(Any.getDescriptor()) &&
(ANY_TYPE_FIELDS.contains(name) || options.getValue(ANY_TYPE_FIELD_NAME).equals(name))) {
var anyBuilder = (Any.Builder) builder.getMessage();
var typeUrl = reader.nextString();
var packedType = options.getRegistry().messageTypeByTypeUrl(typeUrl);
if (packedType != null) {
var packedJsonFields = getJsonFieldMap(packedType);
var packedBuilder = new ProtoMessageBuilder(packedType);
while (reader.peek() != JsonToken.END_OBJECT) {
findFieldAndReadValue(packedBuilder, reader.nextName(), packedType, packedJsonFields);
}
anyBuilder.setTypeUrl(getTypeUrl(packedType));
anyBuilder.setValue(packedBuilder.getMessage().build().toByteString());
break;
} else if (options.isEnabled(IGNORE_UNKNOWN_ANY_TYPE)) {
anyBuilder.setTypeUrl("type.googleapis.com/" + getTypeNameFromTypeUrl(typeUrl));
// ignore entire struct.
while (reader.peek() != JsonToken.END_OBJECT) {
reader.nextName();
reader.skipValue();
}
} else {
throw new JsonParseException("Unknown type " + typeUrl + " at " + reader.getPreviousPath());
}
break;
}
}
findFieldAndReadValue(builder, name, descriptor, jsonFields);
}
reader.endObject();
return builder.getMessage().build();
} else {
if (Timestamp.getDescriptor().equals(descriptor)) {
var builder = Timestamp.newBuilder();
if (reader.peek() == JsonToken.NUMBER) {
// timestamp as fractional seconds since epoch.
var timestampDbl = reader.nextDouble();
var timestampS = (long) timestampDbl;
var micros = (int) Math.round((timestampDbl - timestampS) * 1_000_000L);
builder.setSeconds(timestampS);
builder.setNanos(micros * 1000);
} else if (reader.peek() == JsonToken.STRING) {
// ISO date string.
var instant = parseIsoDateTime(reader, reader.nextString());
builder.setSeconds(instant.getEpochSecond());
builder.setNanos(instant.getNano());
} else {
throw new JsonParseException("Expected '{' or '[' or unix timestamp or ISO date or null, but found " + reader.peek() + " at " + reader.getPath());
}
return builder.build();
} else if (Duration.getDescriptor().equals(descriptor)) {
var builder = Duration.newBuilder();
if (reader.peek() == JsonToken.NUMBER) {
// duration as seconds.
var durationDbl = reader.nextDouble();
var durationS = (long) durationDbl;
var micros = (int) Math.round((durationDbl - durationS) * 1_000_000L);
builder.setSeconds(durationS);
builder.setNanos(micros * 1000);
} else if (reader.peek() == JsonToken.STRING) {
var duration = parseDuration(reader, reader.nextString());
builder.setSeconds(duration.getSeconds());
builder.setNanos(duration.getNano());
} else {
throw new JsonParseException("Expected '{' or '[', duration number or string or null, but found " + reader.peek() + " at " + reader.getPath());
}
return builder.build();
}
throw new JsonParseException("Expected '{' or '[' or null, but found: " + reader.peek() + " at " + reader.getPath());
}
}
private void findFieldAndReadValue(ProtoMessageBuilder builder,
String fieldName,
Descriptors.Descriptor descriptor,
Map<String, Descriptors.FieldDescriptor> jsonFields) throws IOException {
Descriptors.FieldDescriptor field;
if (NUMERIC_FIELD_ID.matcher(fieldName).matches()) {
int number = parseInt(fieldName);
field = descriptor.findFieldByNumber(number);
if (field == null) {
field = options.getRegistry().extensionByScopeAndNumber(descriptor, number);
}
} else {
field = jsonFields.get(fieldName);
if (field == null) {
field = options.getRegistry().extensionByScopeAndName(descriptor, fieldName);
}
}
if (field == null) {
if (options.isEnabled(FAIL_ON_UNKNOWN_FIELD)) {
throw new JsonParseException("Unknown field at " + reader.getPath());
}
reader.skipValue();
} else {
Object o = readValue(field);
if (o != null) {
builder.set(field, o);
} else if (options.isEnabled(FAIL_ON_NULL_VALUE)) {
throw new JsonParseException("Null value at " + reader.getPath());
}
}
}
private Object readValue(Descriptors.FieldDescriptor descriptor) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
if (descriptor.isRepeated()) {
if (descriptor.isMapField()) {
var keyDescriptor = getMapKeyDescriptor(descriptor);
var valueDescriptor = getMapValueDescriptor(descriptor);
MapBuilder<Object, Object> map = UnmodifiableMap.newBuilder();
reader.beginObject();
while (reader.peek() != JsonToken.END_OBJECT) {
var key = readMapKey(keyDescriptor);
var value = readSingleValue(valueDescriptor);
if (value != null) {
map.put(key, value);
} else if (options.isEnabled(FAIL_ON_NULL_VALUE)) {
throw new JsonParseException("Null value in map at " + reader.getPreviousPath());
}
}
reader.endObject();
return map.build();
} else {
ListBuilder<Object> list = UnmodifiableList.newBuilder();
reader.beginArray();
while (reader.peek() != JsonToken.END_ARRAY) {
list.add(readSingleValue(descriptor));
}
reader.endArray();
return list.build();
}
} else {
return readSingleValue(descriptor);
}
}
private Object readMapKey(Descriptors.FieldDescriptor descriptor) throws IOException {
var name = reader.nextName();
switch (descriptor.getType().getJavaType()) {
case BOOLEAN: {
return parseBoolean(name);
}
case INT: {
return parseInt(name);
}
case LONG: {
return parseLong(name);
}
case STRING: {
return name;
}
default: {
// Usually not testable.
throw new IOException("Unhandled map key type: " + descriptor.getType());
}
}
}
private Object readSingleValue(Descriptors.FieldDescriptor descriptor) throws IOException {
if (reader.peek() == JsonToken.NULL) {
return null;
}
switch (descriptor.getType().getJavaType()) {
case BOOLEAN: {
return reader.nextBoolean();
}
case INT: {
try {
return reader.nextInt();
} catch (NumberFormatException e) {
throw new JsonParseException("Invalid int value \"" + reader.nextString() + "\" at " + reader.getPath());
}
}
case LONG: {
try {
return reader.nextLong();
} catch (NumberFormatException e) {
throw new JsonParseException("Invalid long value \"" + reader.nextString() + "\" at " + reader.getPath());
}
}
case FLOAT: {
try {
return (float) reader.nextDouble();
} catch (NumberFormatException e) {
throw new JsonParseException("Invalid float value \"" + reader.nextString() + "\" at " + reader.getPath());
}
}
case DOUBLE: {
try {
return reader.nextDouble();
} catch (NumberFormatException e) {
throw new JsonParseException("Invalid double value \"" + reader.nextString() + "\" at " + reader.getPath());
}
}
case STRING: {
return reader.nextString();
}
case BYTE_STRING: {
return parseBase64(reader.nextString());
}
case ENUM: {
return readEnum(descriptor.getEnumType());
}
case MESSAGE: {
return read(descriptor.getMessageType());
}
default: {
// Usually not testable.
throw new JsonParseException("Unhandled value type: " + descriptor.getType());
}
}
}
private ByteString parseBase64(String value) {
try {
return ByteString.copyFrom(Base64.getDecoder().decode(value));
} catch (IllegalArgumentException e) {
throw new JsonParseException(e.getMessage() + " at " + reader.getPreviousPath(), e);
}
}
private Object readEnum(Descriptors.EnumDescriptor descriptor) throws IOException {
ProtoEnum<?> type = ProtoEnum.getEnumDescriptor(descriptor);
if (reader.peek() == JsonToken.NUMBER) {
var num = reader.nextInt();
return type.valueForNumber(num);
} else {
var name = reader.nextString();
return type.valueForName(name);
}
}
private static JsonReader makeJsonReader(Reader in, boolean lenient) {
JsonReader reader = new JsonReader(new LineBufferedReader(in));
if (lenient) {
reader.setLenient(true);
}
return reader;
}
private static java.time.Duration parseDuration(JsonReader reader, String value) {
try {
return GoogleTypesUtil.parseJavaDurationString(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException(e.getMessage() + " at " + reader.getPreviousPath(), e);
}
}
private static Instant parseIsoDateTime(JsonReader reader, String value) {
try {
return GoogleTypesUtil.parseJavaTimestampString(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException(e.getMessage() + " at " + reader.getPreviousPath());
}
}
}