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 bf70834a6b..297545e52a 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 @@ -101,7 +101,7 @@ public class 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, false, false); } /** @@ -111,14 +111,16 @@ public class JsonFormat { private final TypeRegistry registry; private final boolean includingDefaultValueFields; private final boolean preservingProtoFieldNames; + private final boolean omittingInsignificantWhitespace; private Printer( TypeRegistry registry, boolean includingDefaultValueFields, - boolean preservingProtoFieldNames) { + boolean preservingProtoFieldNames, boolean omittingInsignificantWhitespace) { this.registry = registry; this.includingDefaultValueFields = includingDefaultValueFields; this.preservingProtoFieldNames = preservingProtoFieldNames; + this.omittingInsignificantWhitespace = omittingInsignificantWhitespace; } /** @@ -131,7 +133,7 @@ public class JsonFormat { if (this.registry != TypeRegistry.getEmptyTypeRegistry()) { throw new IllegalArgumentException("Only one registry is allowed."); } - return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames); + return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames, omittingInsignificantWhitespace); } /** @@ -141,7 +143,7 @@ public class JsonFormat { * {@link Printer}. */ public Printer includingDefaultValueFields() { - return new Printer(registry, true, preservingProtoFieldNames); + return new Printer(registry, true, preservingProtoFieldNames, omittingInsignificantWhitespace); } /** @@ -151,7 +153,27 @@ public class JsonFormat { * current {@link Printer}. */ public Printer preservingProtoFieldNames() { - return new Printer(registry, includingDefaultValueFields, true); + return new Printer(registry, includingDefaultValueFields, true, omittingInsignificantWhitespace); + } + + + /** + * 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: + *
+     * ws = *(
+     * %x20 /              ; Space
+     * %x09 /              ; Horizontal tab
+     * %x0A /              ; Line feed or New line
+     * %x0D )              ; Carriage return
+     * 
+ * See https://tools.ietf.org/html/rfc7159 + * current {@link Printer}. + */ + public Printer omittingInsignificantWhitespace(){ + return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames, true); } /** @@ -164,7 +186,7 @@ public class JsonFormat { 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, includingDefaultValueFields, preservingProtoFieldNames, output, omittingInsignificantWhitespace) .print(message); } @@ -351,15 +373,55 @@ public class JsonFormat { } } + /** + * 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 + */ + public void indent() {} + + /** + * ignored by compact printer + */ + public void outdent() {} + + /** + * Print text to the output stream. + */ + 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; } @@ -423,6 +485,8 @@ public class JsonFormat { 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().disableHtmlEscaping().create(); @@ -432,12 +496,21 @@ public class JsonFormat { TypeRegistry registry, boolean includingDefaultValueFields, boolean preservingProtoFieldNames, - Appendable jsonOutput) { + Appendable jsonOutput, boolean omittingInsignificantWhitespace) { this.registry = registry; this.includingDefaultValueFields = includingDefaultValueFields; this.preservingProtoFieldNames = preservingProtoFieldNames; - this.generator = new TextGenerator(jsonOutput); 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 { @@ -568,12 +641,12 @@ public class JsonFormat { 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 { @@ -661,13 +734,15 @@ 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 fieldsToPrint = null; @@ -689,7 +764,7 @@ public class JsonFormat { for (Map.Entry field : fieldsToPrint.entrySet()) { if (printedField) { // Add line-endings for the previous field. - generator.print(",\n"); + generator.print("," + blankOrNewLine); } else { printedField = true; } @@ -698,7 +773,7 @@ public class JsonFormat { // Add line-endings for the last field. if (printedField) { - generator.print("\n"); + generator.print(blankOrNewLine); } generator.outdent(); generator.print("}"); @@ -706,9 +781,9 @@ public class JsonFormat { 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); @@ -725,7 +800,7 @@ public class JsonFormat { boolean printedElement = false; for (Object element : (List) value) { if (printedElement) { - generator.print(", "); + generator.print("," + blankOrSpace); } else { printedElement = true; } @@ -742,7 +817,7 @@ public class JsonFormat { 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) { @@ -750,17 +825,17 @@ 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("}"); diff --git a/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java index 4d9a417d33..e68c7be122 100644 --- a/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java +++ b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java @@ -140,6 +140,9 @@ public class JsonFormatTest extends TestCase { private String toJsonString(Message message) throws IOException { return JsonFormat.printer().print(message); } + private String toCompactJsonString(Message message) throws IOException{ + return JsonFormat.printer().omittingInsignificantWhitespace().print(message); + } private void mergeFromJson(String json, Message.Builder builder) throws IOException { JsonFormat.parser().merge(json, builder); @@ -1166,4 +1169,59 @@ public class JsonFormatTest extends TestCase { JsonFormat.parser().merge("{\"optional_int32\": 54321}", builder); assertEquals(54321, builder.getOptionalInt32()); } + + public void testOmittingInsignificantWhiteSpace() throws Exception { + TestAllTypes message = TestAllTypes.newBuilder().setOptionalInt32(12345).build(); + assertEquals("{" + "\"optionalInt32\":12345" + "}", JsonFormat.printer().omittingInsignificantWhitespace().print(message)); + TestAllTypes message1 = TestAllTypes.getDefaultInstance(); + assertEquals("{}", JsonFormat.printer().omittingInsignificantWhitespace().print(message1)); + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + setAllFields(builder); + TestAllTypes message2 = builder.build(); + assertEquals( + "{" + + "\"optionalInt32\":1234," + + "\"optionalInt64\":\"1234567890123456789\"," + + "\"optionalUint32\":5678," + + "\"optionalUint64\":\"2345678901234567890\"," + + "\"optionalSint32\":9012," + + "\"optionalSint64\":\"3456789012345678901\"," + + "\"optionalFixed32\":3456," + + "\"optionalFixed64\":\"4567890123456789012\"," + + "\"optionalSfixed32\":7890," + + "\"optionalSfixed64\":\"5678901234567890123\"," + + "\"optionalFloat\":1.5," + + "\"optionalDouble\":1.25," + + "\"optionalBool\":true," + + "\"optionalString\":\"Hello world!\"," + + "\"optionalBytes\":\"AAEC\"," + + "\"optionalNestedMessage\":{" + + "\"value\":100" + + "}," + + "\"optionalNestedEnum\":\"BAR\"," + + "\"repeatedInt32\":[1234,234]," + + "\"repeatedInt64\":[\"1234567890123456789\",\"234567890123456789\"]," + + "\"repeatedUint32\":[5678,678]," + + "\"repeatedUint64\":[\"2345678901234567890\",\"345678901234567890\"]," + + "\"repeatedSint32\":[9012,10]," + + "\"repeatedSint64\":[\"3456789012345678901\",\"456789012345678901\"]," + + "\"repeatedFixed32\":[3456,456]," + + "\"repeatedFixed64\":[\"4567890123456789012\",\"567890123456789012\"]," + + "\"repeatedSfixed32\":[7890,890]," + + "\"repeatedSfixed64\":[\"5678901234567890123\",\"678901234567890123\"]," + + "\"repeatedFloat\":[1.5,11.5]," + + "\"repeatedDouble\":[1.25,11.25]," + + "\"repeatedBool\":[true,true]," + + "\"repeatedString\":[\"Hello world!\",\"ello world!\"]," + + "\"repeatedBytes\":[\"AAEC\",\"AQI=\"]," + + "\"repeatedNestedMessage\":[{" + + "\"value\":100" + + "},{" + + "\"value\":200" + + "}]," + + "\"repeatedNestedEnum\":[\"BAR\",\"BAZ\"]" + + "}", + toCompactJsonString(message2)); + } + }