diff options
Diffstat (limited to 'java/util/src/main/java/com/google/protobuf/util/JsonFormat.java')
-rw-r--r-- | java/util/src/main/java/com/google/protobuf/util/JsonFormat.java | 1141 |
1 files changed, 654 insertions, 487 deletions
diff --git a/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java index d13ff0ed..7f69ee68 100644 --- a/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java +++ b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java @@ -30,10 +30,13 @@ package com.google.protobuf.util; +import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -48,6 +51,7 @@ import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; import com.google.protobuf.DynamicMessage; @@ -59,13 +63,13 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.ListValue; import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.NullValue; import com.google.protobuf.StringValue; import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; - import java.io.IOException; import java.io.Reader; import java.io.StringReader; @@ -91,46 +95,70 @@ import java.util.logging.Logger; * as well. */ public class JsonFormat { - private static final Logger logger = - Logger.getLogger(JsonFormat.class.getName()); + private static final Logger logger = Logger.getLogger(JsonFormat.class.getName()); private JsonFormat() {} - + /** * Creates a {@link Printer} with default configurations. */ public static Printer printer() { - return new Printer(TypeRegistry.getEmptyTypeRegistry(), false, false); + return new Printer( + TypeRegistry.getEmptyTypeRegistry(), false, Collections.<FieldDescriptor>emptySet(), + false, false, false); } - + /** * A Printer converts protobuf message to JSON format. */ public static class Printer { private final TypeRegistry registry; - private final boolean includingDefaultValueFields; + // NOTE: There are 3 states for these *defaultValueFields variables: + // 1) Default - alwaysOutput is false & including is empty set. Fields only output if they are + // set to non-default values. + // 2) No-args includingDefaultValueFields() called - alwaysOutput is true & including is + // irrelevant (but set to empty set). All fields are output regardless of their values. + // 3) includingDefaultValueFields(Set<FieldDescriptor>) called - alwaysOutput is false & + // including is set to the specified set. Fields in that set are always output & fields not + // in that set are only output if set to non-default values. + private boolean alwaysOutputDefaultValueFields; + private Set<FieldDescriptor> includingDefaultValueFields; private final boolean preservingProtoFieldNames; + private final boolean omittingInsignificantWhitespace; + private final boolean printingEnumsAsInts; private Printer( TypeRegistry registry, - boolean includingDefaultValueFields, - boolean preservingProtoFieldNames) { + boolean alwaysOutputDefaultValueFields, + Set<FieldDescriptor> includingDefaultValueFields, + boolean preservingProtoFieldNames, + boolean omittingInsignificantWhitespace, + boolean printingEnumsAsInts) { this.registry = registry; + this.alwaysOutputDefaultValueFields = alwaysOutputDefaultValueFields; this.includingDefaultValueFields = includingDefaultValueFields; this.preservingProtoFieldNames = preservingProtoFieldNames; + this.omittingInsignificantWhitespace = omittingInsignificantWhitespace; + this.printingEnumsAsInts = printingEnumsAsInts; } - + /** * Creates a new {@link Printer} using the given registry. The new Printer * clones all other configurations from the current {@link Printer}. - * + * * @throws IllegalArgumentException if a registry is already set. */ public Printer usingTypeRegistry(TypeRegistry registry) { if (this.registry != TypeRegistry.getEmptyTypeRegistry()) { throw new IllegalArgumentException("Only one registry is allowed."); } - return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames); + return new Printer( + registry, + alwaysOutputDefaultValueFields, + includingDefaultValueFields, + preservingProtoFieldNames, + omittingInsignificantWhitespace, + printingEnumsAsInts); } /** @@ -140,7 +168,66 @@ public class JsonFormat { * {@link Printer}. */ public Printer includingDefaultValueFields() { - return new Printer(registry, true, preservingProtoFieldNames); + checkUnsetIncludingDefaultValueFields(); + return new Printer( + registry, + true, + Collections.<FieldDescriptor>emptySet(), + preservingProtoFieldNames, + omittingInsignificantWhitespace, + printingEnumsAsInts); + } + + /** + * Creates a new {@link Printer} that will print enum field values as integers instead of as + * string. + * The new Printer clones all other configurations from the current + * {@link Printer}. + */ + public Printer printingEnumsAsInts() { + checkUnsetPrintingEnumsAsInts(); + return new Printer( + registry, + alwaysOutputDefaultValueFields, + Collections.<FieldDescriptor>emptySet(), + preservingProtoFieldNames, + omittingInsignificantWhitespace, + true); + } + + private void checkUnsetPrintingEnumsAsInts() { + if (printingEnumsAsInts) { + throw new IllegalStateException("JsonFormat printingEnumsAsInts has already been set."); + } + } + + /** + * Creates a new {@link Printer} that will also print default-valued fields if their + * FieldDescriptors are found in the supplied set. Empty repeated fields and map fields will be + * printed as well, if they match. The new Printer clones all other configurations from the + * current {@link Printer}. Call includingDefaultValueFields() with no args to unconditionally + * output all fields. + */ + public Printer includingDefaultValueFields(Set<FieldDescriptor> fieldsToAlwaysOutput) { + Preconditions.checkArgument( + null != fieldsToAlwaysOutput && !fieldsToAlwaysOutput.isEmpty(), + "Non-empty Set must be supplied for includingDefaultValueFields."); + + checkUnsetIncludingDefaultValueFields(); + return new Printer( + registry, + false, + fieldsToAlwaysOutput, + preservingProtoFieldNames, + omittingInsignificantWhitespace, + printingEnumsAsInts); + } + + private void checkUnsetIncludingDefaultValueFields() { + if (alwaysOutputDefaultValueFields || !includingDefaultValueFields.isEmpty()) { + throw new IllegalStateException( + "JsonFormat includingDefaultValueFields has already been set."); + } } /** @@ -150,30 +237,69 @@ public class JsonFormat { * current {@link Printer}. */ public Printer preservingProtoFieldNames() { - return new Printer(registry, includingDefaultValueFields, true); + return new Printer( + registry, + alwaysOutputDefaultValueFields, + includingDefaultValueFields, + true, + omittingInsignificantWhitespace, + printingEnumsAsInts); + } + + + /** + * Create a new {@link Printer} that will omit all insignificant whitespace in the JSON output. + * This new Printer clones all other configurations from the current Printer. Insignificant + * whitespace is defined by the JSON spec as whitespace that appear between JSON structural + * elements: + * + * <pre> + * ws = *( + * %x20 / ; Space + * %x09 / ; Horizontal tab + * %x0A / ; Line feed or New line + * %x0D ) ; Carriage return + * </pre> + * + * See <a href="https://tools.ietf.org/html/rfc7159">https://tools.ietf.org/html/rfc7159</a> + * current {@link Printer}. + */ + public Printer omittingInsignificantWhitespace() { + return new Printer( + registry, + alwaysOutputDefaultValueFields, + includingDefaultValueFields, + preservingProtoFieldNames, + true, + printingEnumsAsInts); } - + /** * Converts a protobuf message to JSON format. - * - * @throws InvalidProtocolBufferException if the message contains Any types - * that can't be resolved. + * + * @throws InvalidProtocolBufferException if the message contains Any types that can't be + * resolved. * @throws IOException if writing to the output fails. */ - public void appendTo(MessageOrBuilder message, Appendable output) - throws IOException { + public void appendTo(MessageOrBuilder message, Appendable output) throws IOException { // TODO(xiaofeng): Investigate the allocation overhead and optimize for // mobile. - new PrinterImpl(registry, includingDefaultValueFields, preservingProtoFieldNames, output) + new PrinterImpl( + registry, + alwaysOutputDefaultValueFields, + includingDefaultValueFields, + preservingProtoFieldNames, + output, + omittingInsignificantWhitespace, + printingEnumsAsInts) .print(message); } /** * Converts a protobuf message to JSON format. Throws exceptions if there - * are unknown Any types in the message. + * are unknown Any types in the message. */ - public String print(MessageOrBuilder message) - throws InvalidProtocolBufferException { + public String print(MessageOrBuilder message) throws InvalidProtocolBufferException { try { StringBuilder builder = new StringBuilder(); appendTo(message, builder); @@ -191,57 +317,75 @@ public class JsonFormat { * Creates a {@link Parser} with default configuration. */ public static Parser parser() { - return new Parser(TypeRegistry.getEmptyTypeRegistry()); + return new Parser(TypeRegistry.getEmptyTypeRegistry(), false, Parser.DEFAULT_RECURSION_LIMIT); } - + /** * A Parser parses JSON to protobuf message. */ public static class Parser { private final TypeRegistry registry; - - private Parser(TypeRegistry registry) { - this.registry = registry; + private final boolean ignoringUnknownFields; + private final int recursionLimit; + + // The default parsing recursion limit is aligned with the proto binary parser. + private static final int DEFAULT_RECURSION_LIMIT = 100; + + private Parser(TypeRegistry registry, boolean ignoreUnknownFields, int recursionLimit) { + this.registry = registry; + this.ignoringUnknownFields = ignoreUnknownFields; + this.recursionLimit = recursionLimit; } - + /** * Creates a new {@link Parser} using the given registry. The new Parser * clones all other configurations from this Parser. - * + * * @throws IllegalArgumentException if a registry is already set. */ public Parser usingTypeRegistry(TypeRegistry registry) { if (this.registry != TypeRegistry.getEmptyTypeRegistry()) { throw new IllegalArgumentException("Only one registry is allowed."); } - return new Parser(registry); + return new Parser(registry, ignoringUnknownFields, recursionLimit); + } + + /** + * Creates a new {@link Parser} configured to not throw an exception when an unknown field is + * encountered. The new Parser clones all other configurations from this Parser. + */ + public Parser ignoringUnknownFields() { + return new Parser(this.registry, true, recursionLimit); } - + /** * Parses from JSON into a protobuf message. - * + * * @throws InvalidProtocolBufferException if the input is not valid JSON * format or there are unknown fields in the input. */ - public void merge(String json, Message.Builder builder) - throws InvalidProtocolBufferException { + public void merge(String json, Message.Builder builder) throws InvalidProtocolBufferException { // TODO(xiaofeng): Investigate the allocation overhead and optimize for // mobile. - new ParserImpl(registry).merge(json, builder); + new ParserImpl(registry, ignoringUnknownFields, recursionLimit).merge(json, builder); } - + /** * Parses from JSON into a protobuf message. - * + * * @throws InvalidProtocolBufferException if the input is not valid JSON * format or there are unknown fields in the input. * @throws IOException if reading from the input throws. */ - public void merge(Reader json, Message.Builder builder) - throws IOException { + public void merge(Reader json, Message.Builder builder) throws IOException { // TODO(xiaofeng): Investigate the allocation overhead and optimize for // mobile. - new ParserImpl(registry).merge(json, builder); + new ParserImpl(registry, ignoringUnknownFields, recursionLimit).merge(json, builder); + } + + // For testing only. + Parser usingRecursionLimit(int recursionLimit) { + return new Parser(registry, ignoringUnknownFields, recursionLimit); } } @@ -254,8 +398,8 @@ public class JsonFormat { */ public static class TypeRegistry { private static class EmptyTypeRegistryHolder { - private static final TypeRegistry EMPTY = new TypeRegistry( - Collections.<String, Descriptor>emptyMap()); + private static final TypeRegistry EMPTY = + new TypeRegistry(Collections.<String, Descriptor>emptyMap()); } public static TypeRegistry getEmptyTypeRegistry() { @@ -292,8 +436,7 @@ public class JsonFormat { */ public Builder add(Descriptor messageType) { if (types == null) { - throw new IllegalStateException( - "A TypeRegistry.Builer can only be used once."); + throw new IllegalStateException("A TypeRegistry.Builer can only be used once."); } addFile(messageType.getFile()); return this; @@ -305,8 +448,7 @@ public class JsonFormat { */ public Builder add(Iterable<Descriptor> messageTypes) { if (types == null) { - throw new IllegalStateException( - "A TypeRegistry.Builer can only be used once."); + throw new IllegalStateException("A TypeRegistry.Builder can only be used once."); } for (Descriptor type : messageTypes) { addFile(type.getFile()); @@ -344,8 +486,7 @@ public class JsonFormat { } if (types.containsKey(message.getFullName())) { - logger.warning("Type " + message.getFullName() - + " is added multiple times."); + logger.warning("Type " + message.getFullName() + " is added multiple times."); return; } @@ -353,48 +494,80 @@ public class JsonFormat { } private final Set<String> files = new HashSet<String>(); - private Map<String, Descriptor> types = - new HashMap<String, Descriptor>(); + private Map<String, Descriptor> types = new HashMap<String, Descriptor>(); } } /** + * An interface for json formatting that can be used in + * combination with the omittingInsignificantWhitespace() method + */ + interface TextGenerator { + void indent(); + + void outdent(); + + void print(final CharSequence text) throws IOException; + } + + /** + * Format the json without indentation + */ + private static final class CompactTextGenerator implements TextGenerator { + private final Appendable output; + + private CompactTextGenerator(final Appendable output) { + this.output = output; + } + + /** ignored by compact printer */ + @Override + public void indent() {} + + /** ignored by compact printer */ + @Override + public void outdent() {} + + /** Print text to the output stream. */ + @Override + public void print(final CharSequence text) throws IOException { + output.append(text); + } + } + /** * A TextGenerator adds indentation when writing formatted text. */ - private static final class TextGenerator { + private static final class PrettyTextGenerator implements TextGenerator { private final Appendable output; private final StringBuilder indent = new StringBuilder(); private boolean atStartOfLine = true; - private TextGenerator(final Appendable output) { + private PrettyTextGenerator(final Appendable output) { this.output = output; } /** - * Indent text by two spaces. After calling Indent(), two spaces will be - * inserted at the beginning of each line of text. Indent() may be called - * multiple times to produce deeper indents. + * Indent text by two spaces. After calling Indent(), two spaces will be inserted at the + * beginning of each line of text. Indent() may be called multiple times to produce deeper + * indents. */ + @Override public void indent() { indent.append(" "); } - /** - * Reduces the current indent level by two spaces, or crashes if the indent - * level is zero. - */ + /** Reduces the current indent level by two spaces, or crashes if the indent level is zero. */ + @Override public void outdent() { final int length = indent.length(); if (length < 2) { - throw new IllegalArgumentException( - " Outdent() without matching Indent()."); + throw new IllegalArgumentException(" Outdent() without matching Indent()."); } indent.delete(length - 2, length); } - /** - * Print text to the output stream. - */ + /** Print text to the output stream. */ + @Override public void print(final CharSequence text) throws IOException { final int size = text.length(); int pos = 0; @@ -426,68 +599,82 @@ public class JsonFormat { */ private static final class PrinterImpl { private final TypeRegistry registry; - private final boolean includingDefaultValueFields; + private final boolean alwaysOutputDefaultValueFields; + private final Set<FieldDescriptor> includingDefaultValueFields; private final boolean preservingProtoFieldNames; + private final boolean printingEnumsAsInts; private final TextGenerator generator; // We use Gson to help handle string escapes. private final Gson gson; + private final CharSequence blankOrSpace; + private final CharSequence blankOrNewLine; private static class GsonHolder { - private static final Gson DEFAULT_GSON = new Gson(); + private static final Gson DEFAULT_GSON = new GsonBuilder().disableHtmlEscaping().create(); } PrinterImpl( TypeRegistry registry, - boolean includingDefaultValueFields, + boolean alwaysOutputDefaultValueFields, + Set<FieldDescriptor> includingDefaultValueFields, boolean preservingProtoFieldNames, - Appendable jsonOutput) { + Appendable jsonOutput, + boolean omittingInsignificantWhitespace, + boolean printingEnumsAsInts) { this.registry = registry; + this.alwaysOutputDefaultValueFields = alwaysOutputDefaultValueFields; this.includingDefaultValueFields = includingDefaultValueFields; this.preservingProtoFieldNames = preservingProtoFieldNames; - this.generator = new TextGenerator(jsonOutput); + this.printingEnumsAsInts = printingEnumsAsInts; this.gson = GsonHolder.DEFAULT_GSON; + // json format related properties, determined by printerType + if (omittingInsignificantWhitespace) { + this.generator = new CompactTextGenerator(jsonOutput); + this.blankOrSpace = ""; + this.blankOrNewLine = ""; + } else { + this.generator = new PrettyTextGenerator(jsonOutput); + this.blankOrSpace = " "; + this.blankOrNewLine = "\n"; + } } void print(MessageOrBuilder message) throws IOException { - WellKnownTypePrinter specialPrinter = wellKnownTypePrinters.get( - message.getDescriptorForType().getFullName()); + WellKnownTypePrinter specialPrinter = + wellKnownTypePrinters.get(message.getDescriptorForType().getFullName()); if (specialPrinter != null) { specialPrinter.print(this, message); return; } print(message, null); } - + private interface WellKnownTypePrinter { - void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException; - } - - private static final Map<String, WellKnownTypePrinter> - wellKnownTypePrinters = buildWellKnownTypePrinters(); - - private static Map<String, WellKnownTypePrinter> - buildWellKnownTypePrinters() { - Map<String, WellKnownTypePrinter> printers = - new HashMap<String, WellKnownTypePrinter>(); + void print(PrinterImpl printer, MessageOrBuilder message) throws IOException; + } + + private static final Map<String, WellKnownTypePrinter> wellKnownTypePrinters = + buildWellKnownTypePrinters(); + + private static Map<String, WellKnownTypePrinter> buildWellKnownTypePrinters() { + Map<String, WellKnownTypePrinter> printers = new HashMap<String, WellKnownTypePrinter>(); // Special-case Any. - printers.put(Any.getDescriptor().getFullName(), + printers.put( + Any.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printAny(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printAny(message); + } + }); // Special-case wrapper types. - WellKnownTypePrinter wrappersPrinter = new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printWrapper(message); - - } - }; + WellKnownTypePrinter wrappersPrinter = + new WellKnownTypePrinter() { + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printWrapper(message); + } + }; printers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter); printers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter); printers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter); @@ -498,70 +685,75 @@ public class JsonFormat { printers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter); printers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter); // Special-case Timestamp. - printers.put(Timestamp.getDescriptor().getFullName(), + printers.put( + Timestamp.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printTimestamp(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printTimestamp(message); + } + }); // Special-case Duration. - printers.put(Duration.getDescriptor().getFullName(), + printers.put( + Duration.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printDuration(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printDuration(message); + } + }); // Special-case FieldMask. - printers.put(FieldMask.getDescriptor().getFullName(), + printers.put( + FieldMask.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printFieldMask(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printFieldMask(message); + } + }); // Special-case Struct. - printers.put(Struct.getDescriptor().getFullName(), + printers.put( + Struct.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printStruct(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printStruct(message); + } + }); // Special-case Value. - printers.put(Value.getDescriptor().getFullName(), + printers.put( + Value.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printValue(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printValue(message); + } + }); // Special-case ListValue. - printers.put(ListValue.getDescriptor().getFullName(), + printers.put( + ListValue.getDescriptor().getFullName(), new WellKnownTypePrinter() { - @Override - public void print(PrinterImpl printer, MessageOrBuilder message) - throws IOException { - printer.printListValue(message); - } - }); + @Override + public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException { + printer.printListValue(message); + } + }); return printers; } - + /** Prints google.protobuf.Any */ private void printAny(MessageOrBuilder message) throws IOException { + if (Any.getDefaultInstance().equals(message)) { + generator.print("{}"); + return; + } Descriptor descriptor = message.getDescriptorForType(); FieldDescriptor typeUrlField = descriptor.findFieldByName("type_url"); FieldDescriptor valueField = descriptor.findFieldByName("value"); // Validates type of the message. Note that we can't just cast the message - // to com.google.protobuf.Any because it might be a DynamicMessage. - if (typeUrlField == null || valueField == null + // to com.google.protobuf.Any because it might be a DynamicMessage. + if (typeUrlField == null + || valueField == null || typeUrlField.getType() != FieldDescriptor.Type.STRING || valueField.getType() != FieldDescriptor.Type.BYTES) { throw new InvalidProtocolBufferException("Invalid Any type."); @@ -570,22 +762,21 @@ public class JsonFormat { String typeName = getTypeName(typeUrl); Descriptor type = registry.find(typeName); if (type == null) { - throw new InvalidProtocolBufferException( - "Cannot find type for url: " + typeUrl); + throw new InvalidProtocolBufferException("Cannot find type for url: " + typeUrl); } ByteString content = (ByteString) message.getField(valueField); - Message contentMessage = DynamicMessage.getDefaultInstance(type) - .getParserForType().parseFrom(content); + Message contentMessage = + DynamicMessage.getDefaultInstance(type).getParserForType().parseFrom(content); WellKnownTypePrinter printer = wellKnownTypePrinters.get(typeName); if (printer != null) { // If the type is one of the well-known types, we use a special // formatting. - generator.print("{\n"); + generator.print("{" + blankOrNewLine); generator.indent(); - generator.print("\"@type\": " + gson.toJson(typeUrl) + ",\n"); - generator.print("\"value\": "); + generator.print("\"@type\":" + blankOrSpace + gson.toJson(typeUrl) + "," + blankOrNewLine); + generator.print("\"value\":" + blankOrSpace); printer.print(this, contentMessage); - generator.print("\n"); + generator.print(blankOrNewLine); generator.outdent(); generator.print("}"); } else { @@ -593,7 +784,7 @@ public class JsonFormat { print(contentMessage, typeUrl); } } - + /** Prints wrapper types (e.g., google.protobuf.Int32Value) */ private void printWrapper(MessageOrBuilder message) throws IOException { Descriptor descriptor = message.getDescriptorForType(); @@ -605,7 +796,7 @@ public class JsonFormat { // the whole message. printSingleFieldValue(valueField, message.getField(valueField)); } - + private ByteString toByteString(MessageOrBuilder message) { if (message instanceof Message) { return ((Message) message).toByteString(); @@ -613,26 +804,25 @@ public class JsonFormat { return ((Message.Builder) message).build().toByteString(); } } - + /** Prints google.protobuf.Timestamp */ private void printTimestamp(MessageOrBuilder message) throws IOException { Timestamp value = Timestamp.parseFrom(toByteString(message)); - generator.print("\"" + TimeUtil.toString(value) + "\""); + generator.print("\"" + Timestamps.toString(value) + "\""); } - + /** Prints google.protobuf.Duration */ private void printDuration(MessageOrBuilder message) throws IOException { Duration value = Duration.parseFrom(toByteString(message)); - generator.print("\"" + TimeUtil.toString(value) + "\""); - + generator.print("\"" + Durations.toString(value) + "\""); } - + /** Prints google.protobuf.FieldMask */ private void printFieldMask(MessageOrBuilder message) throws IOException { FieldMask value = FieldMask.parseFrom(toByteString(message)); - generator.print("\"" + FieldMaskUtil.toString(value) + "\""); + generator.print("\"" + FieldMaskUtil.toJsonString(value) + "\""); } - + /** Prints google.protobuf.Struct */ private void printStruct(MessageOrBuilder message) throws IOException { Descriptor descriptor = message.getDescriptorForType(); @@ -643,7 +833,7 @@ public class JsonFormat { // Struct is formatted as a map object. printMapFieldValue(field, message.getField(field)); } - + /** Prints google.protobuf.Value */ private void printValue(MessageOrBuilder message) throws IOException { // For a Value message, only the value of the field is formatted. @@ -662,7 +852,7 @@ public class JsonFormat { printSingleFieldValue(entry.getKey(), entry.getValue()); } } - + /** Prints google.protobuf.ListValue */ private void printListValue(MessageOrBuilder message) throws IOException { Descriptor descriptor = message.getDescriptorForType(); @@ -674,28 +864,36 @@ public class JsonFormat { } /** Prints a regular message with an optional type URL. */ - private void print(MessageOrBuilder message, String typeUrl) - throws IOException { - generator.print("{\n"); + private void print(MessageOrBuilder message, String typeUrl) throws IOException { + generator.print("{" + blankOrNewLine); generator.indent(); boolean printedField = false; if (typeUrl != null) { - generator.print("\"@type\": " + gson.toJson(typeUrl)); + generator.print("\"@type\":" + blankOrSpace + gson.toJson(typeUrl)); printedField = true; } Map<FieldDescriptor, Object> fieldsToPrint = null; - if (includingDefaultValueFields) { - fieldsToPrint = new TreeMap<FieldDescriptor, Object>(); + if (alwaysOutputDefaultValueFields || !includingDefaultValueFields.isEmpty()) { + fieldsToPrint = new TreeMap<FieldDescriptor, Object>(message.getAllFields()); for (FieldDescriptor field : message.getDescriptorForType().getFields()) { - if (field.isOptional() - && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE - && !message.hasField(field)) { - // Always skip empty optional message fields. If not we will recurse indefinitely if - // a message has itself as a sub-field. - continue; + if (field.isOptional()) { + if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE + && !message.hasField(field)) { + // Always skip empty optional message fields. If not we will recurse indefinitely if + // a message has itself as a sub-field. + continue; + } + OneofDescriptor oneof = field.getContainingOneof(); + if (oneof != null && !message.hasField(field)) { + // Skip all oneof fields except the one that is actually set + continue; + } + } + if (!fieldsToPrint.containsKey(field) + && (alwaysOutputDefaultValueFields || includingDefaultValueFields.contains(field))) { + fieldsToPrint.put(field, message.getField(field)); } - fieldsToPrint.put(field, message.getField(field)); } } else { fieldsToPrint = message.getAllFields(); @@ -703,27 +901,26 @@ public class JsonFormat { for (Map.Entry<FieldDescriptor, Object> field : fieldsToPrint.entrySet()) { if (printedField) { // Add line-endings for the previous field. - generator.print(",\n"); + generator.print("," + blankOrNewLine); } else { printedField = true; } printField(field.getKey(), field.getValue()); } - + // Add line-endings for the last field. if (printedField) { - generator.print("\n"); + generator.print(blankOrNewLine); } generator.outdent(); generator.print("}"); } - private void printField(FieldDescriptor field, Object value) - throws IOException { + private void printField(FieldDescriptor field, Object value) throws IOException { if (preservingProtoFieldNames) { - generator.print("\"" + field.getName() + "\": "); + generator.print("\"" + field.getName() + "\":" + blankOrSpace); } else { - generator.print("\"" + field.getJsonName() + "\": "); + generator.print("\"" + field.getJsonName() + "\":" + blankOrSpace); } if (field.isMapField()) { printMapFieldValue(field, value); @@ -733,15 +930,14 @@ public class JsonFormat { printSingleFieldValue(field, value); } } - + @SuppressWarnings("rawtypes") - private void printRepeatedFieldValue(FieldDescriptor field, Object value) - throws IOException { + private void printRepeatedFieldValue(FieldDescriptor field, Object value) throws IOException { generator.print("["); boolean printedElement = false; for (Object element : (List) value) { if (printedElement) { - generator.print(", "); + generator.print("," + blankOrSpace); } else { printedElement = true; } @@ -749,17 +945,16 @@ public class JsonFormat { } generator.print("]"); } - + @SuppressWarnings("rawtypes") - private void printMapFieldValue(FieldDescriptor field, Object value) - throws IOException { + private void printMapFieldValue(FieldDescriptor field, Object value) throws IOException { Descriptor type = field.getMessageType(); FieldDescriptor keyField = type.findFieldByName("key"); FieldDescriptor valueField = type.findFieldByName("value"); if (keyField == null || valueField == null) { throw new InvalidProtocolBufferException("Invalid map field."); } - generator.print("{\n"); + generator.print("{" + blankOrNewLine); generator.indent(); boolean printedElement = false; for (Object element : (List) value) { @@ -767,36 +962,35 @@ public class JsonFormat { Object entryKey = entry.getField(keyField); Object entryValue = entry.getField(valueField); if (printedElement) { - generator.print(",\n"); + generator.print("," + blankOrNewLine); } else { printedElement = true; } // Key fields are always double-quoted. printSingleFieldValue(keyField, entryKey, true); - generator.print(": "); + generator.print(":" + blankOrSpace); printSingleFieldValue(valueField, entryValue); } if (printedElement) { - generator.print("\n"); + generator.print(blankOrNewLine); } generator.outdent(); generator.print("}"); } - - private void printSingleFieldValue(FieldDescriptor field, Object value) - throws IOException { + + private void printSingleFieldValue(FieldDescriptor field, Object value) throws IOException { printSingleFieldValue(field, value, false); } /** * Prints a field's value in JSON format. - * + * * @param alwaysWithQuotes whether to always add double-quotes to primitive * types. */ private void printSingleFieldValue( - final FieldDescriptor field, final Object value, - boolean alwaysWithQuotes) throws IOException { + final FieldDescriptor field, final Object value, boolean alwaysWithQuotes) + throws IOException { switch (field.getType()) { case INT32: case SINT32: @@ -850,7 +1044,7 @@ public class JsonFormat { } } break; - + case DOUBLE: Double doubleValue = (Double) value; if (doubleValue.isNaN()) { @@ -894,15 +1088,13 @@ public class JsonFormat { case BYTES: generator.print("\""); - generator.print( - BaseEncoding.base64().encode(((ByteString) value).toByteArray())); + generator.print(BaseEncoding.base64().encode(((ByteString) value).toByteArray())); generator.print("\""); break; case ENUM: // Special-case google.protobuf.NullValue (it's an Enum). - if (field.getEnumType().getFullName().equals( - "google.protobuf.NullValue")) { + if (field.getEnumType().getFullName().equals("google.protobuf.NullValue")) { // No matter what value it contains, we always print it as "null". if (alwaysWithQuotes) { generator.print("\""); @@ -912,12 +1104,10 @@ public class JsonFormat { generator.print("\""); } } else { - if (((EnumValueDescriptor) value).getIndex() == -1) { - generator.print( - String.valueOf(((EnumValueDescriptor) value).getNumber())); + if (printingEnumsAsInts || ((EnumValueDescriptor) value).getIndex() == -1) { + generator.print(String.valueOf(((EnumValueDescriptor) value).getNumber())); } else { - generator.print( - "\"" + ((EnumValueDescriptor) value).getName() + "\""); + generator.print("\"" + ((EnumValueDescriptor) value).getName() + "\""); } } break; @@ -946,41 +1136,54 @@ public class JsonFormat { } else { // Pull off the most-significant bit so that BigInteger doesn't think // the number is negative, then set it again using setBit(). - return BigInteger.valueOf(value & Long.MAX_VALUE) - .setBit(Long.SIZE - 1).toString(); + return BigInteger.valueOf(value & Long.MAX_VALUE).setBit(Long.SIZE - 1).toString(); } } - - private static final String TYPE_URL_PREFIX = "type.googleapis.com"; - - private static String getTypeName(String typeUrl) - throws InvalidProtocolBufferException { + + private static String getTypeName(String typeUrl) throws InvalidProtocolBufferException { String[] parts = typeUrl.split("/"); - if (parts.length != 2 || !parts[0].equals(TYPE_URL_PREFIX)) { - throw new InvalidProtocolBufferException( - "Invalid type url found: " + typeUrl); + if (parts.length == 1) { + throw new InvalidProtocolBufferException("Invalid type url found: " + typeUrl); } - return parts[1]; + return parts[parts.length - 1]; } - + private static class ParserImpl { private final TypeRegistry registry; private final JsonParser jsonParser; - - ParserImpl(TypeRegistry registry) { + private final boolean ignoringUnknownFields; + private final int recursionLimit; + private int currentDepth; + + ParserImpl(TypeRegistry registry, boolean ignoreUnknownFields, int recursionLimit) { this.registry = registry; + this.ignoringUnknownFields = ignoreUnknownFields; this.jsonParser = new JsonParser(); + this.recursionLimit = recursionLimit; + this.currentDepth = 0; } - - void merge(Reader json, Message.Builder builder) - throws IOException { - JsonReader reader = new JsonReader(json); - reader.setLenient(false); - merge(jsonParser.parse(reader), builder); + + void merge(Reader json, Message.Builder builder) throws IOException { + try { + JsonReader reader = new JsonReader(json); + reader.setLenient(false); + merge(jsonParser.parse(reader), builder); + } catch (InvalidProtocolBufferException e) { + throw e; + } catch (JsonIOException e) { + // Unwrap IOException. + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + throw new InvalidProtocolBufferException(e.getMessage()); + } + } catch (Exception e) { + // We convert all exceptions from JSON parsing to our own exceptions. + throw new InvalidProtocolBufferException(e.getMessage()); + } } - - void merge(String json, Message.Builder builder) - throws InvalidProtocolBufferException { + + void merge(String json, Message.Builder builder) throws InvalidProtocolBufferException { try { JsonReader reader = new JsonReader(new StringReader(json)); reader.setLenient(false); @@ -992,35 +1195,36 @@ public class JsonFormat { throw new InvalidProtocolBufferException(e.getMessage()); } } - + private interface WellKnownTypeParser { void merge(ParserImpl parser, JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException; } - + private static final Map<String, WellKnownTypeParser> wellKnownTypeParsers = buildWellKnownTypeParsers(); - - private static Map<String, WellKnownTypeParser> - buildWellKnownTypeParsers() { - Map<String, WellKnownTypeParser> parsers = - new HashMap<String, WellKnownTypeParser>(); + + private static Map<String, WellKnownTypeParser> buildWellKnownTypeParsers() { + Map<String, WellKnownTypeParser> parsers = new HashMap<String, WellKnownTypeParser>(); // Special-case Any. - parsers.put(Any.getDescriptor().getFullName(), new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeAny(json, builder); - } - }); + parsers.put( + Any.getDescriptor().getFullName(), + new WellKnownTypeParser() { + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeAny(json, builder); + } + }); // Special-case wrapper types. - WellKnownTypeParser wrappersPrinter = new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeWrapper(json, builder); - } - }; + WellKnownTypeParser wrappersPrinter = + new WellKnownTypeParser() { + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeWrapper(json, builder); + } + }; parsers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter); parsers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter); parsers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter); @@ -1031,73 +1235,86 @@ public class JsonFormat { parsers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter); parsers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter); // Special-case Timestamp. - parsers.put(Timestamp.getDescriptor().getFullName(), + parsers.put( + Timestamp.getDescriptor().getFullName(), new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeTimestamp(json, builder); - } - }); + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeTimestamp(json, builder); + } + }); // Special-case Duration. - parsers.put(Duration.getDescriptor().getFullName(), + parsers.put( + Duration.getDescriptor().getFullName(), new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeDuration(json, builder); - } - }); + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeDuration(json, builder); + } + }); // Special-case FieldMask. - parsers.put(FieldMask.getDescriptor().getFullName(), + parsers.put( + FieldMask.getDescriptor().getFullName(), new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeFieldMask(json, builder); - } - }); + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeFieldMask(json, builder); + } + }); // Special-case Struct. - parsers.put(Struct.getDescriptor().getFullName(), + parsers.put( + Struct.getDescriptor().getFullName(), new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeStruct(json, builder); - } - }); + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeStruct(json, builder); + } + }); + // Special-case ListValue. + parsers.put( + ListValue.getDescriptor().getFullName(), + new WellKnownTypeParser() { + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeListValue(json, builder); + } + }); // Special-case Value. - parsers.put(Value.getDescriptor().getFullName(), + parsers.put( + Value.getDescriptor().getFullName(), new WellKnownTypeParser() { - @Override - public void merge(ParserImpl parser, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { - parser.mergeValue(json, builder); - } - }); + @Override + public void merge(ParserImpl parser, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + parser.mergeValue(json, builder); + } + }); return parsers; } - + private void merge(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { - WellKnownTypeParser specialParser = wellKnownTypeParsers.get( - builder.getDescriptorForType().getFullName()); + WellKnownTypeParser specialParser = + wellKnownTypeParsers.get(builder.getDescriptorForType().getFullName()); if (specialParser != null) { specialParser.merge(this, json, builder); return; } mergeMessage(json, builder, false); } - + // Maps from camel-case field names to FieldDescriptor. private final Map<Descriptor, Map<String, FieldDescriptor>> fieldNameMaps = new HashMap<Descriptor, Map<String, FieldDescriptor>>(); - - private Map<String, FieldDescriptor> getFieldNameMap( - Descriptor descriptor) { + + private Map<String, FieldDescriptor> getFieldNameMap(Descriptor descriptor) { if (!fieldNameMaps.containsKey(descriptor)) { - Map<String, FieldDescriptor> fieldNameMap = - new HashMap<String, FieldDescriptor>(); + Map<String, FieldDescriptor> fieldNameMap = new HashMap<String, FieldDescriptor>(); for (FieldDescriptor field : descriptor.getFields()) { fieldNameMap.put(field.getName(), field); fieldNameMap.put(field.getJsonName(), field); @@ -1107,64 +1324,67 @@ public class JsonFormat { } return fieldNameMaps.get(descriptor); } - - private void mergeMessage(JsonElement json, Message.Builder builder, - boolean skipTypeUrl) throws InvalidProtocolBufferException { + + private void mergeMessage(JsonElement json, Message.Builder builder, boolean skipTypeUrl) + throws InvalidProtocolBufferException { if (!(json instanceof JsonObject)) { - throw new InvalidProtocolBufferException( - "Expect message object but got: " + json); + throw new InvalidProtocolBufferException("Expect message object but got: " + json); } JsonObject object = (JsonObject) json; - Map<String, FieldDescriptor> fieldNameMap = - getFieldNameMap(builder.getDescriptorForType()); + Map<String, FieldDescriptor> fieldNameMap = getFieldNameMap(builder.getDescriptorForType()); for (Map.Entry<String, JsonElement> entry : object.entrySet()) { if (skipTypeUrl && entry.getKey().equals("@type")) { continue; } FieldDescriptor field = fieldNameMap.get(entry.getKey()); if (field == null) { + if (ignoringUnknownFields) { + continue; + } throw new InvalidProtocolBufferException( - "Cannot find field: " + entry.getKey() + " in message " - + builder.getDescriptorForType().getFullName()); + "Cannot find field: " + + entry.getKey() + + " in message " + + builder.getDescriptorForType().getFullName()); } mergeField(field, entry.getValue(), builder); } } - + private void mergeAny(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { Descriptor descriptor = builder.getDescriptorForType(); FieldDescriptor typeUrlField = descriptor.findFieldByName("type_url"); FieldDescriptor valueField = descriptor.findFieldByName("value"); // Validates type of the message. Note that we can't just cast the message - // to com.google.protobuf.Any because it might be a DynamicMessage. - if (typeUrlField == null || valueField == null + // to com.google.protobuf.Any because it might be a DynamicMessage. + if (typeUrlField == null + || valueField == null || typeUrlField.getType() != FieldDescriptor.Type.STRING || valueField.getType() != FieldDescriptor.Type.BYTES) { throw new InvalidProtocolBufferException("Invalid Any type."); } - + if (!(json instanceof JsonObject)) { - throw new InvalidProtocolBufferException( - "Expect message object but got: " + json); + throw new InvalidProtocolBufferException("Expect message object but got: " + json); } JsonObject object = (JsonObject) json; + if (object.entrySet().isEmpty()) { + return; // builder never modified, so it will end up building the default instance of Any + } JsonElement typeUrlElement = object.get("@type"); if (typeUrlElement == null) { - throw new InvalidProtocolBufferException( - "Missing type url when parsing: " + json); + throw new InvalidProtocolBufferException("Missing type url when parsing: " + json); } String typeUrl = typeUrlElement.getAsString(); Descriptor contentType = registry.find(getTypeName(typeUrl)); if (contentType == null) { - throw new InvalidProtocolBufferException( - "Cannot resolve type: " + typeUrl); + throw new InvalidProtocolBufferException("Cannot resolve type: " + typeUrl); } builder.setField(typeUrlField, typeUrl); Message.Builder contentBuilder = DynamicMessage.getDefaultInstance(contentType).newBuilderForType(); - WellKnownTypeParser specialParser = - wellKnownTypeParsers.get(contentType.getFullName()); + WellKnownTypeParser specialParser = wellKnownTypeParsers.get(contentType.getFullName()); if (specialParser != null) { JsonElement value = object.get("value"); if (value != null) { @@ -1175,35 +1395,33 @@ public class JsonFormat { } builder.setField(valueField, contentBuilder.build().toByteString()); } - + private void mergeFieldMask(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { - FieldMask value = FieldMaskUtil.fromString(json.getAsString()); + FieldMask value = FieldMaskUtil.fromJsonString(json.getAsString()); builder.mergeFrom(value.toByteString()); } - + private void mergeTimestamp(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { try { - Timestamp value = TimeUtil.parseTimestamp(json.getAsString()); + Timestamp value = Timestamps.parse(json.getAsString()); builder.mergeFrom(value.toByteString()); } catch (ParseException e) { - throw new InvalidProtocolBufferException( - "Failed to parse timestamp: " + json); + throw new InvalidProtocolBufferException("Failed to parse timestamp: " + json); } } - + private void mergeDuration(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { try { - Duration value = TimeUtil.parseDuration(json.getAsString()); + Duration value = Durations.parse(json.getAsString()); builder.mergeFrom(value.toByteString()); } catch (ParseException e) { - throw new InvalidProtocolBufferException( - "Failed to parse duration: " + json); + throw new InvalidProtocolBufferException("Failed to parse duration: " + json); } } - + private void mergeStruct(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { Descriptor descriptor = builder.getDescriptorForType(); @@ -1213,21 +1431,28 @@ public class JsonFormat { } mergeMapField(field, json, builder); } - + + private void mergeListValue(JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { + Descriptor descriptor = builder.getDescriptorForType(); + FieldDescriptor field = descriptor.findFieldByName("values"); + if (field == null) { + throw new InvalidProtocolBufferException("Invalid ListValue type."); + } + mergeRepeatedField(field, json, builder); + } + private void mergeValue(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { Descriptor type = builder.getDescriptorForType(); if (json instanceof JsonPrimitive) { JsonPrimitive primitive = (JsonPrimitive) json; if (primitive.isBoolean()) { - builder.setField(type.findFieldByName("bool_value"), - primitive.getAsBoolean()); + builder.setField(type.findFieldByName("bool_value"), primitive.getAsBoolean()); } else if (primitive.isNumber()) { - builder.setField(type.findFieldByName("number_value"), - primitive.getAsDouble()); + builder.setField(type.findFieldByName("number_value"), primitive.getAsDouble()); } else { - builder.setField(type.findFieldByName("string_value"), - primitive.getAsString()); + builder.setField(type.findFieldByName("string_value"), primitive.getAsString()); } } else if (json instanceof JsonObject) { FieldDescriptor field = type.findFieldByName("struct_value"); @@ -1237,28 +1462,28 @@ public class JsonFormat { } else if (json instanceof JsonArray) { FieldDescriptor field = type.findFieldByName("list_value"); Message.Builder listBuilder = builder.newBuilderForField(field); - FieldDescriptor listField = - listBuilder.getDescriptorForType().findFieldByName("values"); - mergeRepeatedField(listField, json, listBuilder); + merge(json, listBuilder); builder.setField(field, listBuilder.build()); + } else if (json instanceof JsonNull) { + builder.setField( + type.findFieldByName("null_value"), NullValue.NULL_VALUE.getValueDescriptor()); } else { throw new IllegalStateException("Unexpected json data: " + json); } } - + private void mergeWrapper(JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { Descriptor type = builder.getDescriptorForType(); FieldDescriptor field = type.findFieldByName("value"); if (field == null) { - throw new InvalidProtocolBufferException( - "Invalid wrapper type: " + type.getFullName()); + throw new InvalidProtocolBufferException("Invalid wrapper type: " + type.getFullName()); } builder.setField(field, parseFieldValue(field, json, builder)); } - - private void mergeField(FieldDescriptor field, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { + + private void mergeField(FieldDescriptor field, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { if (field.isRepeated()) { if (builder.getRepeatedFieldCount(field) > 0) { throw new InvalidProtocolBufferException( @@ -1273,8 +1498,11 @@ public class JsonFormat { && builder.getOneofFieldDescriptor(field.getContainingOneof()) != null) { FieldDescriptor other = builder.getOneofFieldDescriptor(field.getContainingOneof()); throw new InvalidProtocolBufferException( - "Cannot set field " + field.getFullName() + " because another field " - + other.getFullName() + " belonging to the same oneof has already been set "); + "Cannot set field " + + field.getFullName() + + " because another field " + + other.getFullName() + + " belonging to the same oneof has already been set "); } } if (field.isRepeated() && json instanceof JsonNull) { @@ -1293,97 +1521,50 @@ public class JsonFormat { } } } - - private void mergeMapField(FieldDescriptor field, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { + + private void mergeMapField(FieldDescriptor field, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { if (!(json instanceof JsonObject)) { - throw new InvalidProtocolBufferException( - "Expect a map object but found: " + json); + throw new InvalidProtocolBufferException("Expect a map object but found: " + json); } Descriptor type = field.getMessageType(); FieldDescriptor keyField = type.findFieldByName("key"); FieldDescriptor valueField = type.findFieldByName("value"); if (keyField == null || valueField == null) { - throw new InvalidProtocolBufferException( - "Invalid map field: " + field.getFullName()); + throw new InvalidProtocolBufferException("Invalid map field: " + field.getFullName()); } JsonObject object = (JsonObject) json; for (Map.Entry<String, JsonElement> entry : object.entrySet()) { Message.Builder entryBuilder = builder.newBuilderForField(field); - Object key = parseFieldValue( - keyField, new JsonPrimitive(entry.getKey()), entryBuilder); - Object value = parseFieldValue( - valueField, entry.getValue(), entryBuilder); + Object key = parseFieldValue(keyField, new JsonPrimitive(entry.getKey()), entryBuilder); + Object value = parseFieldValue(valueField, entry.getValue(), entryBuilder); if (value == null) { - throw new InvalidProtocolBufferException( - "Map value cannot be null."); + throw new InvalidProtocolBufferException("Map value cannot be null."); } entryBuilder.setField(keyField, key); entryBuilder.setField(valueField, value); builder.addRepeatedField(field, entryBuilder.build()); } } - - /** - * Gets the default value for a field type. Note that we use proto3 - * language defaults and ignore any default values set through the - * proto "default" option. - */ - private Object getDefaultValue(FieldDescriptor field, - Message.Builder builder) { - switch (field.getType()) { - case INT32: - case SINT32: - case SFIXED32: - case UINT32: - case FIXED32: - return 0; - case INT64: - case SINT64: - case SFIXED64: - case UINT64: - case FIXED64: - return 0L; - case FLOAT: - return 0.0f; - case DOUBLE: - return 0.0; - case BOOL: - return false; - case STRING: - return ""; - case BYTES: - return ByteString.EMPTY; - case ENUM: - return field.getEnumType().getValues().get(0); - case MESSAGE: - case GROUP: - return builder.newBuilderForField(field).getDefaultInstanceForType(); - default: - throw new IllegalStateException( - "Invalid field type: " + field.getType()); - } - } - - private void mergeRepeatedField(FieldDescriptor field, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { + + private void mergeRepeatedField( + FieldDescriptor field, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { if (!(json instanceof JsonArray)) { - throw new InvalidProtocolBufferException( - "Expect an array but found: " + json); + throw new InvalidProtocolBufferException("Expect an array but found: " + json); } JsonArray array = (JsonArray) json; for (int i = 0; i < array.size(); ++i) { Object value = parseFieldValue(field, array.get(i), builder); if (value == null) { throw new InvalidProtocolBufferException( - "Repeated field elements cannot be null"); + "Repeated field elements cannot be null in field: " + field.getFullName()); } builder.addRepeatedField(field, value); } } - - private int parseInt32(JsonElement json) - throws InvalidProtocolBufferException { + + private int parseInt32(JsonElement json) throws InvalidProtocolBufferException { try { return Integer.parseInt(json.getAsString()); } catch (Exception e) { @@ -1399,9 +1580,8 @@ public class JsonFormat { throw new InvalidProtocolBufferException("Not an int32 value: " + json); } } - - private long parseInt64(JsonElement json) - throws InvalidProtocolBufferException { + + private long parseInt64(JsonElement json) throws InvalidProtocolBufferException { try { return Long.parseLong(json.getAsString()); } catch (Exception e) { @@ -1414,17 +1594,15 @@ public class JsonFormat { BigDecimal value = new BigDecimal(json.getAsString()); return value.longValueExact(); } catch (Exception e) { - throw new InvalidProtocolBufferException("Not an int32 value: " + json); + throw new InvalidProtocolBufferException("Not an int64 value: " + json); } } - - private int parseUint32(JsonElement json) - throws InvalidProtocolBufferException { + + private int parseUint32(JsonElement json) throws InvalidProtocolBufferException { try { long result = Long.parseLong(json.getAsString()); if (result < 0 || result > 0xFFFFFFFFL) { - throw new InvalidProtocolBufferException( - "Out of range uint32 value: " + json); + throw new InvalidProtocolBufferException("Out of range uint32 value: " + json); } return (int) result; } catch (InvalidProtocolBufferException e) { @@ -1445,35 +1623,28 @@ public class JsonFormat { } catch (InvalidProtocolBufferException e) { throw e; } catch (Exception e) { - throw new InvalidProtocolBufferException( - "Not an uint32 value: " + json); + throw new InvalidProtocolBufferException("Not an uint32 value: " + json); } } - - private static final BigInteger MAX_UINT64 = - new BigInteger("FFFFFFFFFFFFFFFF", 16); - - private long parseUint64(JsonElement json) - throws InvalidProtocolBufferException { + + private static final BigInteger MAX_UINT64 = new BigInteger("FFFFFFFFFFFFFFFF", 16); + + private long parseUint64(JsonElement json) throws InvalidProtocolBufferException { try { BigDecimal decimalValue = new BigDecimal(json.getAsString()); BigInteger value = decimalValue.toBigIntegerExact(); - if (value.compareTo(BigInteger.ZERO) < 0 - || value.compareTo(MAX_UINT64) > 0) { - throw new InvalidProtocolBufferException( - "Out of range uint64 value: " + json); + if (value.compareTo(BigInteger.ZERO) < 0 || value.compareTo(MAX_UINT64) > 0) { + throw new InvalidProtocolBufferException("Out of range uint64 value: " + json); } return value.longValue(); } catch (InvalidProtocolBufferException e) { throw e; } catch (Exception e) { - throw new InvalidProtocolBufferException( - "Not an uint64 value: " + json); + throw new InvalidProtocolBufferException("Not an uint64 value: " + json); } } - - private boolean parseBool(JsonElement json) - throws InvalidProtocolBufferException { + + private boolean parseBool(JsonElement json) throws InvalidProtocolBufferException { if (json.getAsString().equals("true")) { return true; } @@ -1482,11 +1653,10 @@ public class JsonFormat { } throw new InvalidProtocolBufferException("Invalid bool value: " + json); } - + private static final double EPSILON = 1e-6; - - private float parseFloat(JsonElement json) - throws InvalidProtocolBufferException { + + private float parseFloat(JsonElement json) throws InvalidProtocolBufferException { if (json.getAsString().equals("NaN")) { return Float.NaN; } else if (json.getAsString().equals("Infinity")) { @@ -1504,8 +1674,7 @@ public class JsonFormat { // of tolerance when checking whether the float value is in range. if (value > Float.MAX_VALUE * (1.0 + EPSILON) || value < -Float.MAX_VALUE * (1.0 + EPSILON)) { - throw new InvalidProtocolBufferException( - "Out of range float value: " + json); + throw new InvalidProtocolBufferException("Out of range float value: " + json); } return (float) value; } catch (InvalidProtocolBufferException e) { @@ -1514,19 +1683,17 @@ public class JsonFormat { throw new InvalidProtocolBufferException("Not a float value: " + json); } } - - private static final BigDecimal MORE_THAN_ONE = new BigDecimal( - String.valueOf(1.0 + EPSILON)); + + private static final BigDecimal MORE_THAN_ONE = new BigDecimal(String.valueOf(1.0 + EPSILON)); // When a float value is printed, the printed value might be a little // larger or smaller due to precision loss. Here we need to add a bit // of tolerance when checking whether the float value is in range. - private static final BigDecimal MAX_DOUBLE = new BigDecimal( - String.valueOf(Double.MAX_VALUE)).multiply(MORE_THAN_ONE); - private static final BigDecimal MIN_DOUBLE = new BigDecimal( - String.valueOf(-Double.MAX_VALUE)).multiply(MORE_THAN_ONE); - - private double parseDouble(JsonElement json) - throws InvalidProtocolBufferException { + private static final BigDecimal MAX_DOUBLE = + new BigDecimal(String.valueOf(Double.MAX_VALUE)).multiply(MORE_THAN_ONE); + private static final BigDecimal MIN_DOUBLE = + new BigDecimal(String.valueOf(-Double.MAX_VALUE)).multiply(MORE_THAN_ONE); + + private double parseDouble(JsonElement json) throws InvalidProtocolBufferException { if (json.getAsString().equals("NaN")) { return Double.NaN; } else if (json.getAsString().equals("Infinity")) { @@ -1539,36 +1706,31 @@ public class JsonFormat { // accepts all values. Here we parse the value into a BigDecimal and do // explicit range check on it. BigDecimal value = new BigDecimal(json.getAsString()); - if (value.compareTo(MAX_DOUBLE) > 0 - || value.compareTo(MIN_DOUBLE) < 0) { - throw new InvalidProtocolBufferException( - "Out of range double value: " + json); + if (value.compareTo(MAX_DOUBLE) > 0 || value.compareTo(MIN_DOUBLE) < 0) { + throw new InvalidProtocolBufferException("Out of range double value: " + json); } return value.doubleValue(); } catch (InvalidProtocolBufferException e) { throw e; } catch (Exception e) { - throw new InvalidProtocolBufferException( - "Not an double value: " + json); + throw new InvalidProtocolBufferException("Not an double value: " + json); } } - + private String parseString(JsonElement json) { return json.getAsString(); } - + private ByteString parseBytes(JsonElement json) throws InvalidProtocolBufferException { - String encoded = json.getAsString(); - if (encoded.length() % 4 != 0) { - throw new InvalidProtocolBufferException( - "Bytes field is not encoded in standard BASE64 with paddings: " + encoded); + try { + return ByteString.copyFrom(BaseEncoding.base64().decode(json.getAsString())); + } catch (IllegalArgumentException e) { + return ByteString.copyFrom(BaseEncoding.base64Url().decode(json.getAsString())); } - return ByteString.copyFrom( - BaseEncoding.base64().decode(json.getAsString())); } - - private EnumValueDescriptor parseEnum(EnumDescriptor enumDescriptor, - JsonElement json) throws InvalidProtocolBufferException { + + private EnumValueDescriptor parseEnum(EnumDescriptor enumDescriptor, JsonElement json) + throws InvalidProtocolBufferException { String value = json.getAsString(); EnumValueDescriptor result = enumDescriptor.findValueByName(value); if (result == null) { @@ -1585,27 +1747,28 @@ public class JsonFormat { // that's not the exception we want the user to see. Since result == null, we will throw // an exception later. } - + if (result == null) { throw new InvalidProtocolBufferException( - "Invalid enum value: " + value + " for enum type: " - + enumDescriptor.getFullName()); + "Invalid enum value: " + value + " for enum type: " + enumDescriptor.getFullName()); } } return result; } - - private Object parseFieldValue(FieldDescriptor field, JsonElement json, - Message.Builder builder) throws InvalidProtocolBufferException { + + private Object parseFieldValue(FieldDescriptor field, JsonElement json, Message.Builder builder) + throws InvalidProtocolBufferException { if (json instanceof JsonNull) { if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE - && field.getMessageType().getFullName().equals( - Value.getDescriptor().getFullName())) { + && field.getMessageType().getFullName().equals(Value.getDescriptor().getFullName())) { // For every other type, "null" means absence, but for the special // Value message, it means the "null_value" field has been set. Value value = Value.newBuilder().setNullValueValue(0).build(); - return builder.newBuilderForField(field).mergeFrom( - value.toByteString()).build(); + return builder.newBuilderForField(field).mergeFrom(value.toByteString()).build(); + } else if (field.getJavaType() == FieldDescriptor.JavaType.ENUM + && field.getEnumType().getFullName().equals(NullValue.getDescriptor().getFullName())) { + // If the type of the field is a NullValue, then the value should be explicitly set. + return field.getEnumType().findValueByNumber(0); } return null; } @@ -1625,7 +1788,7 @@ public class JsonFormat { case FLOAT: return parseFloat(json); - + case DOUBLE: return parseDouble(json); @@ -1648,14 +1811,18 @@ public class JsonFormat { case MESSAGE: case GROUP: + if (currentDepth >= recursionLimit) { + throw new InvalidProtocolBufferException("Hit recursion limit."); + } + ++currentDepth; Message.Builder subBuilder = builder.newBuilderForField(field); merge(json, subBuilder); + --currentDepth; return subBuilder.build(); - + default: - throw new InvalidProtocolBufferException( - "Invalid field type: " + field.getType()); - } + throw new InvalidProtocolBufferException("Invalid field type: " + field.getType()); + } } } } |