GsonMessageReader.java

  1. /*
  2.  * Copyright 2020 Providence Authors
  3.  *
  4.  * Licensed to the Apache Software Foundation (ASF) under one
  5.  * or more contributor license agreements. See the NOTICE file
  6.  * distributed with this work for additional information
  7.  * regarding copyright ownership. The ASF licenses this file
  8.  * to you under the Apache License, Version 2.0 (the
  9.  * "License"); you may not use this file except in compliance
  10.  * with the License. You may obtain a copy of the License at
  11.  *
  12.  *   http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing,
  15.  * software distributed under the License is distributed on an
  16.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17.  * KIND, either express or implied. See the License for the
  18.  * specific language governing permissions and limitations
  19.  * under the License.
  20.  */
  21. package net.morimekta.proto.gson.sio;

  22. import com.google.gson.JsonParseException;
  23. import com.google.gson.stream.JsonReader;
  24. import com.google.gson.stream.JsonToken;
  25. import com.google.protobuf.Any;
  26. import com.google.protobuf.ByteString;
  27. import com.google.protobuf.Descriptors;
  28. import com.google.protobuf.Duration;
  29. import com.google.protobuf.Message;
  30. import com.google.protobuf.Timestamp;
  31. import net.morimekta.collect.ListBuilder;
  32. import net.morimekta.collect.MapBuilder;
  33. import net.morimekta.collect.UnmodifiableList;
  34. import net.morimekta.collect.UnmodifiableMap;
  35. import net.morimekta.proto.MorimektaOptions;
  36. import net.morimekta.proto.ProtoEnum;
  37. import net.morimekta.proto.ProtoMessageBuilder;
  38. import net.morimekta.proto.gson.ProtoTypeOptions;
  39. import net.morimekta.proto.sio.MessageReader;
  40. import net.morimekta.proto.utils.GoogleTypesUtil;
  41. import net.morimekta.strings.io.LineBufferedReader;

  42. import java.io.Closeable;
  43. import java.io.IOException;
  44. import java.io.Reader;
  45. import java.time.Instant;
  46. import java.util.Base64;
  47. import java.util.List;
  48. import java.util.Map;

  49. import static java.lang.Boolean.parseBoolean;
  50. import static java.lang.Integer.parseInt;
  51. import static java.lang.Long.parseLong;
  52. import static net.morimekta.proto.gson.ProtoTypeOptions.Option.FAIL_ON_NULL_VALUE;
  53. import static net.morimekta.proto.gson.ProtoTypeOptions.Option.FAIL_ON_UNKNOWN_FIELD;
  54. import static net.morimekta.proto.gson.ProtoTypeOptions.Option.IGNORE_UNKNOWN_ANY_TYPE;
  55. import static net.morimekta.proto.gson.ProtoTypeOptions.Option.LENIENT_READER;
  56. import static net.morimekta.proto.gson.ProtoTypeOptions.Value.ANY_TYPE_FIELD_NAME;
  57. import static net.morimekta.proto.utils.FieldUtil.getMapKeyDescriptor;
  58. import static net.morimekta.proto.utils.FieldUtil.getMapValueDescriptor;
  59. import static net.morimekta.proto.utils.JsonNameUtil.ANY_TYPE_FIELDS;
  60. import static net.morimekta.proto.utils.JsonNameUtil.NUMERIC_FIELD_ID;
  61. import static net.morimekta.proto.utils.JsonNameUtil.getJsonFieldMap;
  62. import static net.morimekta.proto.utils.ProtoTypeRegistry.getTypeNameFromTypeUrl;
  63. import static net.morimekta.proto.utils.ProtoTypeRegistry.getTypeUrl;

  64. /**
  65.  * Reader for reading a JSON message from a stream.
  66.  */
  67. public class GsonMessageReader implements MessageReader, Closeable {
  68.     private final JsonReader       reader;
  69.     private final ProtoTypeOptions options;

  70.     /**
  71.      * @param in      Reader to read from.
  72.      * @param options Proto options.
  73.      */
  74.     public GsonMessageReader(Reader in, ProtoTypeOptions options) {
  75.         this(makeJsonReader(in, options.isEnabled(LENIENT_READER)), options);
  76.     }

  77.     /**
  78.      * @param reader  JSON Reader to read from.
  79.      * @param options Proto options.
  80.      */
  81.     public GsonMessageReader(JsonReader reader, ProtoTypeOptions options) {
  82.         this.reader = reader;
  83.         this.options = options;
  84.     }

  85.     @Override
  86.     public void close() throws IOException {
  87.         reader.close();
  88.     }

  89.     @Override
  90.     public Message read(Descriptors.Descriptor descriptor) throws IOException {
  91.         if (reader.peek() == JsonToken.NULL) {
  92.             reader.nextNull();
  93.             return null;
  94.         }
  95.         if (reader.peek() == JsonToken.BEGIN_ARRAY) {
  96.             if (!descriptor.getOptions().getExtension(MorimektaOptions.compact)) {
  97.                 throw new JsonParseException("Compact format not allowed for " + descriptor.getFullName() + " at " + reader.getPath());
  98.             }
  99.             ProtoMessageBuilder builder = new ProtoMessageBuilder(descriptor);
  100.             List<Descriptors.FieldDescriptor> fields = descriptor.getFields();

  101.             reader.beginArray();
  102.             int idx = 0;
  103.             while (reader.peek() != JsonToken.END_ARRAY) {
  104.                 if (idx >= fields.size()) {
  105.                     if (options.isEnabled(FAIL_ON_NULL_VALUE)) {
  106.                         throw new JsonParseException("Data after last field at " + reader.getPath());
  107.                     }
  108.                     reader.skipValue();
  109.                 } else if (reader.peek() == JsonToken.NULL) {
  110.                     reader.nextNull();
  111.                 } else {
  112.                     Descriptors.FieldDescriptor field = fields.get(idx);
  113.                     builder.set(field, readSingleValue(field));
  114.                 }
  115.                 idx++;
  116.             }
  117.             reader.endArray();
  118.             return builder.getMessage().build();
  119.         } else if (reader.peek() == JsonToken.BEGIN_OBJECT) {
  120.             ProtoMessageBuilder builder = new ProtoMessageBuilder(descriptor);
  121.             Map<String, Descriptors.FieldDescriptor> jsonFields = getJsonFieldMap(descriptor);

  122.             reader.beginObject();
  123.             boolean first = true;
  124.             while (reader.peek() != JsonToken.END_OBJECT) {
  125.                 String name = reader.nextName();
  126.                 if (first) {
  127.                     first = false;
  128.                     if (descriptor.equals(Any.getDescriptor()) &&
  129.                         (ANY_TYPE_FIELDS.contains(name) || options.getValue(ANY_TYPE_FIELD_NAME).equals(name))) {
  130.                         var anyBuilder = (Any.Builder) builder.getMessage();
  131.                         var typeUrl = reader.nextString();
  132.                         var packedType = options.getRegistry().messageTypeByTypeUrl(typeUrl);
  133.                         if (packedType != null) {
  134.                             var packedJsonFields = getJsonFieldMap(packedType);
  135.                             var packedBuilder = new ProtoMessageBuilder(packedType);
  136.                             while (reader.peek() != JsonToken.END_OBJECT) {
  137.                                 findFieldAndReadValue(packedBuilder, reader.nextName(), packedType, packedJsonFields);
  138.                             }
  139.                             anyBuilder.setTypeUrl(getTypeUrl(packedType));
  140.                             anyBuilder.setValue(packedBuilder.getMessage().build().toByteString());
  141.                             break;
  142.                         } else if (options.isEnabled(IGNORE_UNKNOWN_ANY_TYPE)) {
  143.                             anyBuilder.setTypeUrl("type.googleapis.com/" + getTypeNameFromTypeUrl(typeUrl));
  144.                             // ignore entire struct.
  145.                             while (reader.peek() != JsonToken.END_OBJECT) {
  146.                                 reader.nextName();
  147.                                 reader.skipValue();
  148.                             }
  149.                         } else {
  150.                             throw new JsonParseException("Unknown type " + typeUrl + " at " + reader.getPreviousPath());
  151.                         }
  152.                         break;
  153.                     }
  154.                 }
  155.                 findFieldAndReadValue(builder, name, descriptor, jsonFields);
  156.             }
  157.             reader.endObject();
  158.             return builder.getMessage().build();
  159.         } else {
  160.             if (Timestamp.getDescriptor().equals(descriptor)) {
  161.                 var builder = Timestamp.newBuilder();
  162.                 if (reader.peek() == JsonToken.NUMBER) {
  163.                     // timestamp as fractional seconds since epoch.
  164.                     var timestampDbl = reader.nextDouble();
  165.                     var timestampS = (long) timestampDbl;
  166.                     var micros = (int) Math.round((timestampDbl - timestampS) * 1_000_000L);
  167.                     builder.setSeconds(timestampS);
  168.                     builder.setNanos(micros * 1000);
  169.                 } else if (reader.peek() == JsonToken.STRING) {
  170.                     // ISO date string.
  171.                     var instant = parseIsoDateTime(reader, reader.nextString());
  172.                     builder.setSeconds(instant.getEpochSecond());
  173.                     builder.setNanos(instant.getNano());
  174.                 } else {
  175.                     throw new JsonParseException("Expected '{' or '[' or unix timestamp or ISO date or null, but found " + reader.peek() + " at " + reader.getPath());
  176.                 }
  177.                 return builder.build();
  178.             } else if (Duration.getDescriptor().equals(descriptor)) {
  179.                 var builder = Duration.newBuilder();
  180.                 if (reader.peek() == JsonToken.NUMBER) {
  181.                     // duration as seconds.
  182.                     var durationDbl = reader.nextDouble();
  183.                     var durationS = (long) durationDbl;
  184.                     var micros = (int) Math.round((durationDbl - durationS) * 1_000_000L);
  185.                     builder.setSeconds(durationS);
  186.                     builder.setNanos(micros * 1000);
  187.                 } else if (reader.peek() == JsonToken.STRING) {
  188.                     var duration = parseDuration(reader, reader.nextString());
  189.                     builder.setSeconds(duration.getSeconds());
  190.                     builder.setNanos(duration.getNano());
  191.                 } else {
  192.                     throw new JsonParseException("Expected '{' or '[', duration number or string or null, but found " + reader.peek() + " at " + reader.getPath());
  193.                 }
  194.                 return builder.build();
  195.             }
  196.             throw new JsonParseException("Expected '{' or '[' or null, but found: " + reader.peek() + " at " + reader.getPath());
  197.         }
  198.     }

  199.     private void findFieldAndReadValue(ProtoMessageBuilder builder,
  200.                                        String fieldName,
  201.                                        Descriptors.Descriptor descriptor,
  202.                                        Map<String, Descriptors.FieldDescriptor> jsonFields) throws IOException {
  203.         Descriptors.FieldDescriptor field;
  204.         if (NUMERIC_FIELD_ID.matcher(fieldName).matches()) {
  205.             int number = parseInt(fieldName);
  206.             field = descriptor.findFieldByNumber(number);
  207.             if (field == null) {
  208.                 field = options.getRegistry().extensionByScopeAndNumber(descriptor, number);
  209.             }
  210.         } else {
  211.             field = jsonFields.get(fieldName);
  212.             if (field == null) {
  213.                 field = options.getRegistry().extensionByScopeAndName(descriptor, fieldName);
  214.             }
  215.         }
  216.         if (field == null) {
  217.             if (options.isEnabled(FAIL_ON_UNKNOWN_FIELD)) {
  218.                 throw new JsonParseException("Unknown field at " + reader.getPath());
  219.             }
  220.             reader.skipValue();
  221.         } else {
  222.             Object o = readValue(field);
  223.             if (o != null) {
  224.                 builder.set(field, o);
  225.             } else if (options.isEnabled(FAIL_ON_NULL_VALUE)) {
  226.                 throw new JsonParseException("Null value at " + reader.getPath());
  227.             }
  228.         }
  229.     }

  230.     private Object readValue(Descriptors.FieldDescriptor descriptor) throws IOException {
  231.         if (reader.peek() == JsonToken.NULL) {
  232.             reader.nextNull();
  233.             return null;
  234.         }
  235.         if (descriptor.isRepeated()) {
  236.             if (descriptor.isMapField()) {
  237.                 var keyDescriptor = getMapKeyDescriptor(descriptor);
  238.                 var valueDescriptor = getMapValueDescriptor(descriptor);

  239.                 MapBuilder<Object, Object> map = UnmodifiableMap.newBuilder();
  240.                 reader.beginObject();
  241.                 while (reader.peek() != JsonToken.END_OBJECT) {
  242.                     var key = readMapKey(keyDescriptor);
  243.                     var value = readSingleValue(valueDescriptor);
  244.                     if (value != null) {
  245.                         map.put(key, value);
  246.                     } else if (options.isEnabled(FAIL_ON_NULL_VALUE)) {
  247.                         throw new JsonParseException("Null value in map at " + reader.getPreviousPath());
  248.                     }
  249.                 }
  250.                 reader.endObject();
  251.                 return map.build();
  252.             } else {
  253.                 ListBuilder<Object> list = UnmodifiableList.newBuilder();
  254.                 reader.beginArray();
  255.                 while (reader.peek() != JsonToken.END_ARRAY) {
  256.                     list.add(readSingleValue(descriptor));
  257.                 }
  258.                 reader.endArray();
  259.                 return list.build();
  260.             }
  261.         } else {
  262.             return readSingleValue(descriptor);
  263.         }
  264.     }

  265.     private Object readMapKey(Descriptors.FieldDescriptor descriptor) throws IOException {
  266.         var name = reader.nextName();
  267.         switch (descriptor.getType().getJavaType()) {
  268.             case BOOLEAN: {
  269.                 return parseBoolean(name);
  270.             }
  271.             case INT: {
  272.                 return parseInt(name);
  273.             }
  274.             case LONG: {
  275.                 return parseLong(name);
  276.             }
  277.             case STRING: {
  278.                 return name;
  279.             }
  280.             default: {
  281.                 // Usually not testable.
  282.                 throw new IOException("Unhandled map key type: " + descriptor.getType());
  283.             }
  284.         }
  285.     }

  286.     private Object readSingleValue(Descriptors.FieldDescriptor descriptor) throws IOException {
  287.         if (reader.peek() == JsonToken.NULL) {
  288.             return null;
  289.         }
  290.         switch (descriptor.getType().getJavaType()) {
  291.             case BOOLEAN: {
  292.                 return reader.nextBoolean();
  293.             }
  294.             case INT: {
  295.                 try {
  296.                     return reader.nextInt();
  297.                 } catch (NumberFormatException e) {
  298.                     throw new JsonParseException("Invalid int value \"" + reader.nextString() + "\" at " + reader.getPath());
  299.                 }
  300.             }
  301.             case LONG: {
  302.                 try {
  303.                     return reader.nextLong();
  304.                 } catch (NumberFormatException e) {
  305.                     throw new JsonParseException("Invalid long value \"" + reader.nextString() + "\" at " + reader.getPath());
  306.                 }
  307.             }
  308.             case FLOAT: {
  309.                 try {
  310.                     return (float) reader.nextDouble();
  311.                 } catch (NumberFormatException e) {
  312.                     throw new JsonParseException("Invalid float value \"" + reader.nextString() + "\" at " + reader.getPath());
  313.                 }
  314.             }
  315.             case DOUBLE: {
  316.                 try {
  317.                     return reader.nextDouble();
  318.                 } catch (NumberFormatException e) {
  319.                     throw new JsonParseException("Invalid double value \"" + reader.nextString() + "\" at " + reader.getPath());
  320.                 }
  321.             }
  322.             case STRING: {
  323.                 return reader.nextString();
  324.             }
  325.             case BYTE_STRING: {
  326.                 return parseBase64(reader.nextString());
  327.             }
  328.             case ENUM: {
  329.                 return readEnum(descriptor.getEnumType());
  330.             }
  331.             case MESSAGE: {
  332.                 return read(descriptor.getMessageType());
  333.             }
  334.             default: {
  335.                 // Usually not testable.
  336.                 throw new JsonParseException("Unhandled value type: " + descriptor.getType());
  337.             }
  338.         }
  339.     }

  340.     private ByteString parseBase64(String value) {
  341.         try {
  342.             return ByteString.copyFrom(Base64.getDecoder().decode(value));
  343.         } catch (IllegalArgumentException e) {
  344.             throw new JsonParseException(e.getMessage() + " at " + reader.getPreviousPath(), e);
  345.         }
  346.     }

  347.     private Object readEnum(Descriptors.EnumDescriptor descriptor) throws IOException {
  348.         ProtoEnum<?> type = ProtoEnum.getEnumDescriptor(descriptor);
  349.         if (reader.peek() == JsonToken.NUMBER) {
  350.             var num = reader.nextInt();
  351.             return type.valueForNumber(num);
  352.         } else {
  353.             var name = reader.nextString();
  354.             return type.valueForName(name);
  355.         }
  356.     }

  357.     private static JsonReader makeJsonReader(Reader in, boolean lenient) {
  358.         JsonReader reader = new JsonReader(new LineBufferedReader(in));
  359.         if (lenient) {
  360.             reader.setLenient(true);
  361.         }
  362.         return reader;
  363.     }

  364.     private static java.time.Duration parseDuration(JsonReader reader, String value) {
  365.         try {
  366.             return GoogleTypesUtil.parseJavaDurationString(value);
  367.         } catch (IllegalArgumentException e) {
  368.             throw new JsonParseException(e.getMessage() + " at " + reader.getPreviousPath(), e);
  369.         }
  370.     }

  371.     private static Instant parseIsoDateTime(JsonReader reader, String value) {
  372.         try {
  373.             return GoogleTypesUtil.parseJavaTimestampString(value);
  374.         } catch (IllegalArgumentException e) {
  375.             throw new JsonParseException(e.getMessage() + " at " + reader.getPreviousPath());
  376.         }
  377.     }
  378. }