aboutsummaryrefslogtreecommitdiff
path: root/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java
diff options
context:
space:
mode:
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.java255
1 files changed, 166 insertions, 89 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 ac712c94..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,11 +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;
@@ -101,7 +103,9 @@ public class JsonFormat {
* Creates a {@link Printer} with default configurations.
*/
public static Printer printer() {
- return new Printer(TypeRegistry.getEmptyTypeRegistry(), false, false, false);
+ return new Printer(
+ TypeRegistry.getEmptyTypeRegistry(), false, Collections.<FieldDescriptor>emptySet(),
+ false, false, false);
}
/**
@@ -109,19 +113,33 @@ public class JsonFormat {
*/
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 alwaysOutputDefaultValueFields,
+ Set<FieldDescriptor> includingDefaultValueFields,
boolean preservingProtoFieldNames,
- boolean omittingInsignificantWhitespace) {
+ boolean omittingInsignificantWhitespace,
+ boolean printingEnumsAsInts) {
this.registry = registry;
+ this.alwaysOutputDefaultValueFields = alwaysOutputDefaultValueFields;
this.includingDefaultValueFields = includingDefaultValueFields;
this.preservingProtoFieldNames = preservingProtoFieldNames;
this.omittingInsignificantWhitespace = omittingInsignificantWhitespace;
+ this.printingEnumsAsInts = printingEnumsAsInts;
}
/**
@@ -136,9 +154,11 @@ public class JsonFormat {
}
return new Printer(
registry,
+ alwaysOutputDefaultValueFields,
includingDefaultValueFields,
preservingProtoFieldNames,
- omittingInsignificantWhitespace);
+ omittingInsignificantWhitespace,
+ printingEnumsAsInts);
}
/**
@@ -148,8 +168,66 @@ public class JsonFormat {
* {@link Printer}.
*/
public Printer includingDefaultValueFields() {
+ checkUnsetIncludingDefaultValueFields();
return new Printer(
- registry, true, preservingProtoFieldNames, omittingInsignificantWhitespace);
+ 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.");
+ }
}
/**
@@ -160,15 +238,21 @@ public class JsonFormat {
*/
public Printer preservingProtoFieldNames() {
return new Printer(
- registry, includingDefaultValueFields, true, omittingInsignificantWhitespace);
+ 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:
+ * 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
@@ -176,18 +260,25 @@ public class JsonFormat {
* %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, includingDefaultValueFields, preservingProtoFieldNames, true);
+ 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 {
@@ -195,10 +286,12 @@ public class JsonFormat {
// mobile.
new PrinterImpl(
registry,
+ alwaysOutputDefaultValueFields,
includingDefaultValueFields,
preservingProtoFieldNames,
output,
- omittingInsignificantWhitespace)
+ omittingInsignificantWhitespace,
+ printingEnumsAsInts)
.print(message);
}
@@ -355,7 +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());
@@ -427,19 +520,16 @@ public class JsonFormat {
this.output = output;
}
- /**
- * ignored by compact printer
- */
+ /** ignored by compact printer */
+ @Override
public void indent() {}
- /**
- * ignored by compact printer
- */
+ /** ignored by compact printer */
+ @Override
public void outdent() {}
- /**
- * Print text to the output stream.
- */
+ /** Print text to the output stream. */
+ @Override
public void print(final CharSequence text) throws IOException {
output.append(text);
}
@@ -457,18 +547,17 @@ public class JsonFormat {
}
/**
- * 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) {
@@ -477,9 +566,8 @@ public class JsonFormat {
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;
@@ -511,8 +599,10 @@ 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;
@@ -525,13 +615,17 @@ public class JsonFormat {
PrinterImpl(
TypeRegistry registry,
- boolean includingDefaultValueFields,
+ boolean alwaysOutputDefaultValueFields,
+ Set<FieldDescriptor> includingDefaultValueFields,
boolean preservingProtoFieldNames,
Appendable jsonOutput,
- boolean omittingInsignificantWhitespace) {
+ boolean omittingInsignificantWhitespace,
+ boolean printingEnumsAsInts) {
this.registry = registry;
+ this.alwaysOutputDefaultValueFields = alwaysOutputDefaultValueFields;
this.includingDefaultValueFields = includingDefaultValueFields;
this.preservingProtoFieldNames = preservingProtoFieldNames;
+ this.printingEnumsAsInts = printingEnumsAsInts;
this.gson = GsonHolder.DEFAULT_GSON;
// json format related properties, determined by printerType
if (omittingInsignificantWhitespace) {
@@ -780,23 +874,26 @@ public class JsonFormat {
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()) {
if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE
- && !message.hasField(field)){
+ && !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
+ // Skip all oneof fields except the one that is actually set
continue;
}
}
- fieldsToPrint.put(field, message.getField(field));
+ if (!fieldsToPrint.containsKey(field)
+ && (alwaysOutputDefaultValueFields || includingDefaultValueFields.contains(field))) {
+ fieldsToPrint.put(field, message.getField(field));
+ }
}
} else {
fieldsToPrint = message.getAllFields();
@@ -1007,7 +1104,7 @@ public class JsonFormat {
generator.print("\"");
}
} else {
- if (((EnumValueDescriptor) value).getIndex() == -1) {
+ if (printingEnumsAsInts || ((EnumValueDescriptor) value).getIndex() == -1) {
generator.print(String.valueOf(((EnumValueDescriptor) value).getNumber()));
} else {
generator.print("\"" + ((EnumValueDescriptor) value).getName() + "\"");
@@ -1067,9 +1164,23 @@ public class JsonFormat {
}
void merge(Reader json, Message.Builder builder) throws IOException {
- JsonReader reader = new JsonReader(json);
- reader.setLenient(false);
- merge(jsonParser.parse(reader), builder);
+ 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 {
@@ -1436,45 +1547,6 @@ public class JsonFormat {
}
}
- /**
- * 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 {
@@ -1485,7 +1557,8 @@ public class JsonFormat {
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");
+ throw new InvalidProtocolBufferException(
+ "Repeated field elements cannot be null in field: " + field.getFullName());
}
builder.addRepeatedField(field, value);
}
@@ -1521,7 +1594,7 @@ 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);
}
}
@@ -1649,7 +1722,11 @@ public class JsonFormat {
}
private ByteString parseBytes(JsonElement json) throws InvalidProtocolBufferException {
- return ByteString.copyFrom(BaseEncoding.base64().decode(json.getAsString()));
+ try {
+ return ByteString.copyFrom(BaseEncoding.base64().decode(json.getAsString()));
+ } catch (IllegalArgumentException e) {
+ return ByteString.copyFrom(BaseEncoding.base64Url().decode(json.getAsString()));
+ }
}
private EnumValueDescriptor parseEnum(EnumDescriptor enumDescriptor, JsonElement json)