com.google.protobuf.util.JsonFormat Maven / Gradle / Ivy
Show all versions of protobuf-java-util Show documentation
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package com.google.protobuf.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
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;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.google.protobuf.Any;
import com.google.protobuf.BoolValue;
import com.google.protobuf.ByteString;
import com.google.protobuf.BytesValue;
import com.google.protobuf.Descriptors.Descriptor;
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.DoubleValue;
import com.google.protobuf.Duration;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.FieldMask;
import com.google.protobuf.FloatValue;
import com.google.protobuf.Int32Value;
import com.google.protobuf.Int64Value;
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;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Utility class to convert protobuf messages to/from the Proto3 JSON format.
* Only proto3 features are supported. Proto2 only features such as extensions and unknown fields
* are discarded in the conversion. That is, when converting proto2 messages to JSON format,
* extensions and unknown fields are treated as if they do not exist. This applies to proto2
* messages embedded in proto3 messages as well.
*/
public class JsonFormat {
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(
com.google.protobuf.TypeRegistry.getEmptyTypeRegistry(),
TypeRegistry.getEmptyTypeRegistry(),
ShouldPrintDefaults.ONLY_IF_PRESENT,
/* includingDefaultValueFields */ ImmutableSet.of(),
/* preservingProtoFieldNames */ false,
/* omittingInsignificantWhitespace */ false,
/* printingEnumsAsInts */ false,
/* sortingMapKeys */ false);
}
private enum ShouldPrintDefaults {
ONLY_IF_PRESENT, // The "normal" behavior; the others add more compared to this baseline.
ALWAYS_PRINT_EXCEPT_MESSAGES_AND_ONEOFS,
ALWAYS_PRINT_WITHOUT_PRESENCE_FIELDS,
ALWAYS_PRINT_SPECIFIED_FIELDS
}
/** A Printer converts a protobuf message to the proto3 JSON format. */
public static class Printer {
private final com.google.protobuf.TypeRegistry registry;
private final TypeRegistry oldRegistry;
private final ShouldPrintDefaults shouldPrintDefaults;
// Empty unless shouldPrintDefaults is set to ALWAYS_PRINT_SPECIFIED_FIELDS.
private final Set includingDefaultValueFields;
private final boolean preservingProtoFieldNames;
private final boolean omittingInsignificantWhitespace;
private final boolean printingEnumsAsInts;
private final boolean sortingMapKeys;
private Printer(
com.google.protobuf.TypeRegistry registry,
TypeRegistry oldRegistry,
ShouldPrintDefaults shouldOutputDefaults,
Set includingDefaultValueFields,
boolean preservingProtoFieldNames,
boolean omittingInsignificantWhitespace,
boolean printingEnumsAsInts,
boolean sortingMapKeys) {
this.registry = registry;
this.oldRegistry = oldRegistry;
this.shouldPrintDefaults = shouldOutputDefaults;
this.includingDefaultValueFields = includingDefaultValueFields;
this.preservingProtoFieldNames = preservingProtoFieldNames;
this.omittingInsignificantWhitespace = omittingInsignificantWhitespace;
this.printingEnumsAsInts = printingEnumsAsInts;
this.sortingMapKeys = sortingMapKeys;
}
/**
* 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 oldRegistry) {
if (this.oldRegistry != TypeRegistry.getEmptyTypeRegistry()
|| this.registry != com.google.protobuf.TypeRegistry.getEmptyTypeRegistry()) {
throw new IllegalArgumentException("Only one registry is allowed.");
}
return new Printer(
com.google.protobuf.TypeRegistry.getEmptyTypeRegistry(),
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
preservingProtoFieldNames,
omittingInsignificantWhitespace,
printingEnumsAsInts,
sortingMapKeys);
}
/**
* 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(com.google.protobuf.TypeRegistry registry) {
if (this.oldRegistry != TypeRegistry.getEmptyTypeRegistry()
|| this.registry != com.google.protobuf.TypeRegistry.getEmptyTypeRegistry()) {
throw new IllegalArgumentException("Only one registry is allowed.");
}
return new Printer(
registry,
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
preservingProtoFieldNames,
omittingInsignificantWhitespace,
printingEnumsAsInts,
sortingMapKeys);
}
/**
* 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.
*
* Note that non-repeated message fields or fields in a oneof are not honored if provided
* here.
*/
public Printer includingDefaultValueFields(Set fieldsToAlwaysOutput) {
Preconditions.checkArgument(
null != fieldsToAlwaysOutput && !fieldsToAlwaysOutput.isEmpty(),
"Non-empty Set must be supplied for includingDefaultValueFields.");
if (shouldPrintDefaults != ShouldPrintDefaults.ONLY_IF_PRESENT) {
throw new IllegalStateException(
"JsonFormat includingDefaultValueFields has already been set.");
}
return new Printer(
registry,
oldRegistry,
ShouldPrintDefaults.ALWAYS_PRINT_SPECIFIED_FIELDS,
ImmutableSet.copyOf(fieldsToAlwaysOutput),
preservingProtoFieldNames,
omittingInsignificantWhitespace,
printingEnumsAsInts,
sortingMapKeys);
}
/**
* Creates a new {@link Printer} that will print any field that does not support presence even
* if it would not otherwise be printed (empty repeated fields, empty map fields, and implicit
* presence scalars set to their default value). The new Printer clones all other configurations
* from the current {@link Printer}.
*/
public Printer alwaysPrintFieldsWithNoPresence() {
if (shouldPrintDefaults != ShouldPrintDefaults.ONLY_IF_PRESENT) {
throw new IllegalStateException("Only one of the JsonFormat defaults options can be set.");
}
return new Printer(
registry,
oldRegistry,
ShouldPrintDefaults.ALWAYS_PRINT_WITHOUT_PRESENCE_FIELDS,
ImmutableSet.of(),
preservingProtoFieldNames,
omittingInsignificantWhitespace,
printingEnumsAsInts,
sortingMapKeys);
}
/**
* Creates a new {@link Printer} that prints 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,
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
preservingProtoFieldNames,
omittingInsignificantWhitespace,
true,
sortingMapKeys);
}
private void checkUnsetPrintingEnumsAsInts() {
if (printingEnumsAsInts) {
throw new IllegalStateException("JsonFormat printingEnumsAsInts has already been set.");
}
}
/**
* Creates a new {@link Printer} that is configured to use the original proto
* field names as defined in the .proto file rather than converting them to
* lowerCamelCase. The new Printer clones all other configurations from the
* current {@link Printer}.
*/
public Printer preservingProtoFieldNames() {
return new Printer(
registry,
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
true,
omittingInsignificantWhitespace,
printingEnumsAsInts,
sortingMapKeys);
}
/**
* Create a new {@link Printer} that omits 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 appears between JSON structural
* elements:
*
*
* ws = *(
* %x20 / ; Space
* %x09 / ; Horizontal tab
* %x0A / ; Line feed or New line
* %x0D ) ; Carriage return
*
*
* See https://tools.ietf.org/html/rfc7159.
*/
public Printer omittingInsignificantWhitespace() {
return new Printer(
registry,
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
preservingProtoFieldNames,
true,
printingEnumsAsInts,
sortingMapKeys);
}
/**
* Create a new {@link Printer} that will sort the map keys in the JSON output.
*
* Use of this modifier is discouraged. The generated JSON messages are equivalent with and
* without this option set, but there are some corner use cases that demand a stable output,
* while order of map keys is otherwise arbitrary.
*
*
The generated order is not well-defined and should not be depended on, but it's stable.
*
*
This new Printer clones all other configurations from the current {@link Printer}.
*/
public Printer sortingMapKeys() {
return new Printer(
registry,
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
preservingProtoFieldNames,
omittingInsignificantWhitespace,
printingEnumsAsInts,
true);
}
/**
* Converts a protobuf message to the proto3 JSON format.
*
* @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 {
// TODO: Investigate the allocation overhead and optimize for
// mobile.
new PrinterImpl(
registry,
oldRegistry,
shouldPrintDefaults,
includingDefaultValueFields,
preservingProtoFieldNames,
output,
omittingInsignificantWhitespace,
printingEnumsAsInts,
sortingMapKeys)
.print(message);
}
/**
* Converts a protobuf message to the proto3 JSON format. Throws exceptions if there
* are unknown Any types in the message.
*/
public String print(MessageOrBuilder message) throws InvalidProtocolBufferException {
try {
StringBuilder builder = new StringBuilder();
appendTo(message, builder);
return builder.toString();
} catch (InvalidProtocolBufferException e) {
throw e;
} catch (IOException e) {
// Unexpected IOException.
throw new IllegalStateException(e);
}
}
}
/**
* Creates a {@link Parser} with default configuration.
*/
public static Parser parser() {
return new Parser(
com.google.protobuf.TypeRegistry.getEmptyTypeRegistry(),
TypeRegistry.getEmptyTypeRegistry(),
false,
Parser.DEFAULT_RECURSION_LIMIT);
}
/**
* A Parser parses the proto3 JSON format into a protobuf message.
*/
public static class Parser {
private final com.google.protobuf.TypeRegistry registry;
private final TypeRegistry oldRegistry;
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(
com.google.protobuf.TypeRegistry registry,
TypeRegistry oldRegistry,
boolean ignoreUnknownFields,
int recursionLimit) {
this.registry = registry;
this.oldRegistry = oldRegistry;
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 oldRegistry) {
if (this.oldRegistry != TypeRegistry.getEmptyTypeRegistry()
|| this.registry != com.google.protobuf.TypeRegistry.getEmptyTypeRegistry()) {
throw new IllegalArgumentException("Only one registry is allowed.");
}
return new Parser(
com.google.protobuf.TypeRegistry.getEmptyTypeRegistry(),
oldRegistry,
ignoringUnknownFields,
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(com.google.protobuf.TypeRegistry registry) {
if (this.oldRegistry != TypeRegistry.getEmptyTypeRegistry()
|| this.registry != com.google.protobuf.TypeRegistry.getEmptyTypeRegistry()) {
throw new IllegalArgumentException("Only one registry is allowed.");
}
return new Parser(registry, oldRegistry, 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, oldRegistry, true, recursionLimit);
}
/**
* Parses from the proto3 JSON format into a protobuf message.
*
* @throws InvalidProtocolBufferException if the input is not valid JSON
* proto3 format or there are unknown fields in the input.
*/
public void merge(String json, Message.Builder builder) throws InvalidProtocolBufferException {
// TODO: Investigate the allocation overhead and optimize for
// mobile.
new ParserImpl(registry, oldRegistry, ignoringUnknownFields, recursionLimit)
.merge(json, builder);
}
/**
* Parses from the proto3 JSON encoding into a protobuf message.
*
* @throws InvalidProtocolBufferException if the input is not valid proto3 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 {
// TODO: Investigate the allocation overhead and optimize for
// mobile.
new ParserImpl(registry, oldRegistry, ignoringUnknownFields, recursionLimit)
.merge(json, builder);
}
// For testing only.
Parser usingRecursionLimit(int recursionLimit) {
return new Parser(registry, oldRegistry, ignoringUnknownFields, recursionLimit);
}
}
/**
* A TypeRegistry is used to resolve Any messages in the JSON conversion.
* You must provide a TypeRegistry containing all message types used in
* Any message fields, or the JSON conversion will fail because data
* in Any message fields is unrecognizable. You don't need to supply a
* TypeRegistry if you don't use Any message fields.
*/
public static class TypeRegistry {
private static class EmptyTypeRegistryHolder {
private static final TypeRegistry EMPTY =
new TypeRegistry(Collections.emptyMap());
}
public static TypeRegistry getEmptyTypeRegistry() {
return EmptyTypeRegistryHolder.EMPTY;
}
public static Builder newBuilder() {
return new Builder();
}
/**
* Find a type by its full name. Returns null if it cannot be found in this {@link
* TypeRegistry}.
*/
@Nullable
public Descriptor find(String name) {
return types.get(name);
}
@Nullable
Descriptor getDescriptorForTypeUrl(String typeUrl) throws InvalidProtocolBufferException {
return find(getTypeName(typeUrl));
}
private final Map types;
private TypeRegistry(Map types) {
this.types = types;
}
/** A Builder is used to build {@link TypeRegistry}. */
public static class Builder {
private Builder() {}
/**
* Adds a message type and all types defined in the same .proto file as well as all
* transitively imported .proto files to this {@link Builder}.
*/
@CanIgnoreReturnValue
public Builder add(Descriptor messageType) {
if (built) {
throw new IllegalStateException("A TypeRegistry.Builder can only be used once.");
}
addFile(messageType.getFile());
return this;
}
/**
* Adds message types and all types defined in the same .proto file as well as all
* transitively imported .proto files to this {@link Builder}.
*/
@CanIgnoreReturnValue
public Builder add(Iterable messageTypes) {
if (built) {
throw new IllegalStateException("A TypeRegistry.Builder can only be used once.");
}
for (Descriptor type : messageTypes) {
addFile(type.getFile());
}
return this;
}
/**
* Builds a {@link TypeRegistry}. This method can only be called once for
* one Builder.
*/
public TypeRegistry build() {
built = true;
return new TypeRegistry(types);
}
private void addFile(FileDescriptor file) {
// Skip the file if it's already added.
if (!files.add(file.getFullName())) {
return;
}
for (FileDescriptor dependency : file.getDependencies()) {
addFile(dependency);
}
for (Descriptor message : file.getMessageTypes()) {
addMessage(message);
}
}
private void addMessage(Descriptor message) {
for (Descriptor nestedType : message.getNestedTypes()) {
addMessage(nestedType);
}
if (types.containsKey(message.getFullName())) {
logger.warning("Type " + message.getFullName() + " is added multiple times.");
return;
}
types.put(message.getFullName(), message);
}
private final Set files = new HashSet<>();
private final Map types = new HashMap<>();
private boolean built = false;
}
}
/**
* 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 PrettyTextGenerator implements TextGenerator {
private final Appendable output;
private final StringBuilder indent = new StringBuilder();
private boolean atStartOfLine = true;
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.
*/
@Override
public void indent() {
indent.append(" ");
}
/** 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().");
}
indent.delete(length - 2, length);
}
/** Print text to the output stream. */
@Override
public void print(final CharSequence text) throws IOException {
final int size = text.length();
int pos = 0;
for (int i = 0; i < size; i++) {
if (text.charAt(i) == '\n') {
write(text.subSequence(pos, i + 1));
pos = i + 1;
atStartOfLine = true;
}
}
write(text.subSequence(pos, size));
}
private void write(final CharSequence data) throws IOException {
if (data.length() == 0) {
return;
}
if (atStartOfLine) {
atStartOfLine = false;
output.append(indent);
}
output.append(data);
}
}
/**
* A Printer converts protobuf messages to the proto3 JSON format.
*/
private static final class PrinterImpl {
private final com.google.protobuf.TypeRegistry registry;
private final TypeRegistry oldRegistry;
private final ShouldPrintDefaults shouldPrintDefaults;
private final Set includingDefaultValueFields;
private final boolean preservingProtoFieldNames;
private final boolean printingEnumsAsInts;
private final boolean sortingMapKeys;
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 GsonBuilder().create();
}
PrinterImpl(
com.google.protobuf.TypeRegistry registry,
TypeRegistry oldRegistry,
ShouldPrintDefaults shouldPrintDefaults,
Set includingDefaultValueFields,
boolean preservingProtoFieldNames,
Appendable jsonOutput,
boolean omittingInsignificantWhitespace,
boolean printingEnumsAsInts,
boolean sortingMapKeys) {
this.registry = registry;
this.oldRegistry = oldRegistry;
this.shouldPrintDefaults = shouldPrintDefaults;
this.includingDefaultValueFields = includingDefaultValueFields;
this.preservingProtoFieldNames = preservingProtoFieldNames;
this.printingEnumsAsInts = printingEnumsAsInts;
this.sortingMapKeys = sortingMapKeys;
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());
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 wellKnownTypePrinters =
buildWellKnownTypePrinters();
private static Map buildWellKnownTypePrinters() {
Map printers = new HashMap();
// Special-case Any.
printers.put(
Any.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@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);
}
};
printers.put(BoolValue.getDescriptor().getFullName(), wrappersPrinter);
printers.put(Int32Value.getDescriptor().getFullName(), wrappersPrinter);
printers.put(UInt32Value.getDescriptor().getFullName(), wrappersPrinter);
printers.put(Int64Value.getDescriptor().getFullName(), wrappersPrinter);
printers.put(UInt64Value.getDescriptor().getFullName(), wrappersPrinter);
printers.put(StringValue.getDescriptor().getFullName(), wrappersPrinter);
printers.put(BytesValue.getDescriptor().getFullName(), wrappersPrinter);
printers.put(FloatValue.getDescriptor().getFullName(), wrappersPrinter);
printers.put(DoubleValue.getDescriptor().getFullName(), wrappersPrinter);
// Special-case Timestamp.
printers.put(
Timestamp.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@Override
public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
printer.printTimestamp(message);
}
});
// Special-case Duration.
printers.put(
Duration.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@Override
public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
printer.printDuration(message);
}
});
// Special-case FieldMask.
printers.put(
FieldMask.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@Override
public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
printer.printFieldMask(message);
}
});
// Special-case Struct.
printers.put(
Struct.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@Override
public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
printer.printStruct(message);
}
});
// Special-case Value.
printers.put(
Value.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@Override
public void print(PrinterImpl printer, MessageOrBuilder message) throws IOException {
printer.printValue(message);
}
});
// Special-case ListValue.
printers.put(
ListValue.getDescriptor().getFullName(),
new WellKnownTypePrinter() {
@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
|| typeUrlField.getType() != FieldDescriptor.Type.STRING
|| valueField.getType() != FieldDescriptor.Type.BYTES) {
throw new InvalidProtocolBufferException("Invalid Any type.");
}
String typeUrl = (String) message.getField(typeUrlField);
Descriptor type = registry.getDescriptorForTypeUrl(typeUrl);
if (type == null) {
type = oldRegistry.getDescriptorForTypeUrl(typeUrl);
if (type == null) {
throw new InvalidProtocolBufferException("Cannot find type for url: " + typeUrl);
}
}
ByteString content = (ByteString) message.getField(valueField);
Message contentMessage =
DynamicMessage.getDefaultInstance(type).getParserForType().parseFrom(content);
WellKnownTypePrinter printer = wellKnownTypePrinters.get(getTypeName(typeUrl));
if (printer != null) {
// If the type is one of the well-known types, we use a special
// formatting.
generator.print("{" + blankOrNewLine);
generator.indent();
generator.print("\"@type\":" + blankOrSpace + gson.toJson(typeUrl) + "," + blankOrNewLine);
generator.print("\"value\":" + blankOrSpace);
printer.print(this, contentMessage);
generator.print(blankOrNewLine);
generator.outdent();
generator.print("}");
} else {
// Print the content message instead (with a "@type" field added).
print(contentMessage, typeUrl);
}
}
/** Prints wrapper types (e.g., google.protobuf.Int32Value) */
private void printWrapper(MessageOrBuilder message) throws IOException {
Descriptor descriptor = message.getDescriptorForType();
FieldDescriptor valueField = descriptor.findFieldByName("value");
if (valueField == null) {
throw new InvalidProtocolBufferException("Invalid Wrapper type.");
}
// When formatting wrapper types, we just print its value field instead of
// the whole message.
printSingleFieldValue(valueField, message.getField(valueField));
}
private ByteString toByteString(MessageOrBuilder message) {
if (message instanceof Message) {
return ((Message) message).toByteString();
} else {
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("\"" + Timestamps.toString(value) + "\"");
}
/** Prints google.protobuf.Duration */
private void printDuration(MessageOrBuilder message) throws IOException {
Duration value = Duration.parseFrom(toByteString(message));
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.toJsonString(value) + "\"");
}
/** Prints google.protobuf.Struct */
private void printStruct(MessageOrBuilder message) throws IOException {
Descriptor descriptor = message.getDescriptorForType();
FieldDescriptor field = descriptor.findFieldByName("fields");
if (field == null) {
throw new InvalidProtocolBufferException("Invalid Struct type.");
}
// 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.
Map fields = message.getAllFields();
if (fields.isEmpty()) {
// No value set.
generator.print("null");
return;
}
// A Value message can only have at most one field set (it only contains
// an oneof).
if (fields.size() != 1) {
throw new InvalidProtocolBufferException("Invalid Value type.");
}
for (Map.Entry entry : fields.entrySet()) {
FieldDescriptor field = entry.getKey();
if (field.getType() == FieldDescriptor.Type.DOUBLE) {
Double doubleValue = (Double) entry.getValue();
if (doubleValue.isNaN() || doubleValue.isInfinite()) {
throw new IllegalArgumentException(
"google.protobuf.Value cannot encode double values for "
+ "infinity or nan, because they would be parsed as a string.");
}
}
printSingleFieldValue(field, entry.getValue());
}
}
/** Prints google.protobuf.ListValue */
private void printListValue(MessageOrBuilder message) throws IOException {
Descriptor descriptor = message.getDescriptorForType();
FieldDescriptor field = descriptor.findFieldByName("values");
if (field == null) {
throw new InvalidProtocolBufferException("Invalid ListValue type.");
}
printRepeatedFieldValue(field, message.getField(field));
}
// Whether a set option means the corresponding field should be printed even if it normally
// wouldn't be.
private boolean shouldSpeciallyPrint(FieldDescriptor field) {
switch (shouldPrintDefaults) {
case ONLY_IF_PRESENT:
return false;
case ALWAYS_PRINT_EXCEPT_MESSAGES_AND_ONEOFS:
return !field.hasPresence()
|| (field.getJavaType() != FieldDescriptor.JavaType.MESSAGE
&& field.getContainingOneof() == null);
case ALWAYS_PRINT_WITHOUT_PRESENCE_FIELDS:
return !field.hasPresence();
case ALWAYS_PRINT_SPECIFIED_FIELDS:
// For legacy code compatibility, we don't honor non-repeated message or oneof fields even
// if they're explicitly requested. :(
return !(field.getJavaType() == FieldDescriptor.JavaType.MESSAGE && !field.isRepeated())
&& field.getContainingOneof() == null
&& includingDefaultValueFields.contains(field);
}
throw new AssertionError("Unknown shouldPrintDefaults: " + shouldPrintDefaults);
}
/** Prints a regular message with an optional type URL. */
private void print(MessageOrBuilder message, @Nullable String typeUrl) throws IOException {
generator.print("{" + blankOrNewLine);
generator.indent();
boolean printedField = false;
if (typeUrl != null) {
generator.print("\"@type\":" + blankOrSpace + gson.toJson(typeUrl));
printedField = true;
}
// message.getAllFields() will already contain all of the fields that would be
// printed normally (non-empty repeated fields, with-presence fields that are set, implicit
// presence fields that have a nonzero value). Loop over all of the fields to add any more
// fields that should be printed based on the shouldPrintDefaults setting.
Map fieldsToPrint;
if (shouldPrintDefaults == ShouldPrintDefaults.ONLY_IF_PRESENT) {
fieldsToPrint = message.getAllFields();
} else {
fieldsToPrint = new TreeMap(message.getAllFields());
for (FieldDescriptor field : message.getDescriptorForType().getFields()) {
if (shouldSpeciallyPrint(field)) {
fieldsToPrint.put(field, message.getField(field));
}
}
}
for (Map.Entry field : fieldsToPrint.entrySet()) {
if (printedField) {
// Add line-endings for the previous field.
generator.print("," + blankOrNewLine);
} else {
printedField = true;
}
printField(field.getKey(), field.getValue());
}
// Add line-endings for the last field.
if (printedField) {
generator.print(blankOrNewLine);
}
generator.outdent();
generator.print("}");
}
private void printField(FieldDescriptor field, Object value) throws IOException {
if (preservingProtoFieldNames) {
generator.print("\"" + field.getName() + "\":" + blankOrSpace);
} else {
generator.print("\"" + field.getJsonName() + "\":" + blankOrSpace);
}
if (field.isMapField()) {
printMapFieldValue(field, value);
} else if (field.isRepeated()) {
printRepeatedFieldValue(field, value);
} else {
printSingleFieldValue(field, value);
}
}
@SuppressWarnings("rawtypes")
private void printRepeatedFieldValue(FieldDescriptor field, Object value) throws IOException {
generator.print("[");
boolean printedElement = false;
for (Object element : (List) value) {
if (printedElement) {
generator.print("," + blankOrSpace);
} else {
printedElement = true;
}
printSingleFieldValue(field, element);
}
generator.print("]");
}
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("{" + blankOrNewLine);
generator.indent();
@SuppressWarnings("unchecked") // Object guaranteed to be a List for a map field.
Collection