diff --git a/java/core/BUILD.bazel b/java/core/BUILD.bazel index 5bf52209a9c6..a51f6198be0f 100644 --- a/java/core/BUILD.bazel +++ b/java/core/BUILD.bazel @@ -549,6 +549,7 @@ LITE_TEST_EXCLUSIONS = [ "src/test/java/com/google/protobuf/Proto2ExtensionLookupSchemaTest.java", "src/test/java/com/google/protobuf/Proto2SchemaTest.java", "src/test/java/com/google/protobuf/Proto2UnknownEnumValueTest.java", + "src/test/java/com/google/protobuf/ProtobufToStringOutputTest.java", "src/test/java/com/google/protobuf/RepeatedFieldBuilderTest.java", "src/test/java/com/google/protobuf/ServiceTest.java", "src/test/java/com/google/protobuf/SingleFieldBuilderTest.java", diff --git a/java/core/src/main/java/com/google/protobuf/AbstractMessage.java b/java/core/src/main/java/com/google/protobuf/AbstractMessage.java index 6b592be25225..fbf151875930 100644 --- a/java/core/src/main/java/com/google/protobuf/AbstractMessage.java +++ b/java/core/src/main/java/com/google/protobuf/AbstractMessage.java @@ -84,8 +84,11 @@ public FieldDescriptor getOneofFieldDescriptor(OneofDescriptor oneof) { @Override public final String toString() { - return TextFormat.printer() - .printToString(this, TextFormat.Printer.FieldReporterLevel.ABSTRACT_TO_STRING); + TextFormat.Printer printer = + ProtobufToStringOutput.shouldOutputDebugFormat() + ? TextFormat.debugFormatPrinter() + : TextFormat.printer(); + return printer.printToString(this, TextFormat.Printer.FieldReporterLevel.ABSTRACT_TO_STRING); } @Override diff --git a/java/core/src/main/java/com/google/protobuf/ProtobufToStringOutput.java b/java/core/src/main/java/com/google/protobuf/ProtobufToStringOutput.java new file mode 100644 index 000000000000..e6df9267d788 --- /dev/null +++ b/java/core/src/main/java/com/google/protobuf/ProtobufToStringOutput.java @@ -0,0 +1,52 @@ +package com.google.protobuf; + +/** + * ProtobufToStringOutput controls the output format of {@link Message#toString()}. Specifically, for + * the Runnable object passed to `callWithDebugFormat` and `callWithTextFormat`, Message.toString() + * will always output the specified format unless ProtobufToStringOutput is used again to change the + * output format. + */ +public final class ProtobufToStringOutput { + private enum OutputMode { + DEBUG_FORMAT, + TEXT_FORMAT + } + + private static final ThreadLocal outputMode = + new ThreadLocal() { + @Override + protected OutputMode initialValue() { + return OutputMode.TEXT_FORMAT; + } + }; + + private ProtobufToStringOutput() {} + + @CanIgnoreReturnValue + private static OutputMode setOutputMode(OutputMode newMode) { + OutputMode oldMode = outputMode.get(); + outputMode.set(newMode); + return oldMode; + } + + private static void callWithSpecificFormat(Runnable impl, OutputMode mode) { + OutputMode oldMode = setOutputMode(mode); + try { + impl.run(); + } finally { + OutputMode unused = setOutputMode(oldMode); + } + } + + public static void callWithDebugFormat(Runnable impl) { + callWithSpecificFormat(impl, OutputMode.DEBUG_FORMAT); + } + + public static void callWithTextFormat(Runnable impl) { + callWithSpecificFormat(impl, OutputMode.TEXT_FORMAT); + } + + public static boolean shouldOutputDebugFormat() { + return outputMode.get() == OutputMode.DEBUG_FORMAT; + } +} diff --git a/java/core/src/main/java/com/google/protobuf/TextFormat.java b/java/core/src/main/java/com/google/protobuf/TextFormat.java index 2c045279d97b..c4657ea39000 100644 --- a/java/core/src/main/java/com/google/protobuf/TextFormat.java +++ b/java/core/src/main/java/com/google/protobuf/TextFormat.java @@ -111,14 +111,19 @@ private static void printUnknownFieldValue( /** Printer instance which escapes non-ASCII characters. */ public static Printer printer() { - return Printer.DEFAULT; + return Printer.DEFAULT_TEXT_FORMAT; + } + + /** Printer instance which escapes non-ASCII characters and prints in the debug format. */ + public static Printer debugFormatPrinter() { + return Printer.DEFAULT_DEBUG_FORMAT; } /** Helper class for converting protobufs to text. */ public static final class Printer { - // Printer instance which escapes non-ASCII characters. - private static final Printer DEFAULT = + // Printer instance which escapes non-ASCII characters and prints in the text format. + private static final Printer DEFAULT_TEXT_FORMAT = new Printer( true, TypeRegistry.getEmptyTypeRegistry(), @@ -126,6 +131,15 @@ public static final class Printer { false, false); + // Printer instance which escapes non-ASCII characters and prints in the debug format. + private static final Printer DEFAULT_DEBUG_FORMAT = + new Printer( + true, + TypeRegistry.getEmptyTypeRegistry(), + ExtensionRegistryLite.getEmptyRegistry(), + true, + false); + /** * A list of the public APIs that output human-readable text from a message. A higher-level API * must be larger than any lower-level APIs it calls under the hood, e.g diff --git a/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java b/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java index daf2924d9806..29d06fff5458 100644 --- a/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java +++ b/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java @@ -12,11 +12,11 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public final class DebugFormatTest { +public class DebugFormatTest { - private static final String REDACTED_REGEX = "\\[REDACTED\\]"; - private static final String UNSTABLE_PREFIX_SINGLE_LINE = getUnstablePrefix(true); - private static final String UNSTABLE_PREFIX_MULTILINE = getUnstablePrefix(false); + static final String REDACTED_REGEX = "\\[REDACTED\\]"; + static final String UNSTABLE_PREFIX_SINGLE_LINE = getUnstablePrefix(true); + static final String UNSTABLE_PREFIX_MULTILINE = getUnstablePrefix(false); private static String getUnstablePrefix(boolean singleLine) { return ""; diff --git a/java/core/src/test/java/com/google/protobuf/ProtobufToStringOutputTest.java b/java/core/src/test/java/com/google/protobuf/ProtobufToStringOutputTest.java new file mode 100644 index 000000000000..809734a47537 --- /dev/null +++ b/java/core/src/test/java/com/google/protobuf/ProtobufToStringOutputTest.java @@ -0,0 +1,103 @@ +package com.google.protobuf; + +import static com.google.common.truth.Truth.assertThat; + +import protobuf_unittest.UnittestProto.RedactedFields; +import protobuf_unittest.UnittestProto.TestNestedMessageRedaction; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ProtobufToStringOutputTest extends DebugFormatTest { + RedactedFields message; + + @Before + public void setupTest() { + message = + RedactedFields.newBuilder() + .setOptionalUnredactedString("foo") + .setOptionalRedactedString("bar") + .setOptionalRedactedMessage( + TestNestedMessageRedaction.newBuilder().setOptionalUnredactedNestedString("foobar")) + .build(); + } + + @Test + public void toStringFormat_defaultFormat() { + assertThat(message.toString()) + .matches( + "optional_redacted_string: \"bar\"\n" + + "optional_unredacted_string: \"foo\"\n" + + "optional_redacted_message \\{\n" + + " optional_unredacted_nested_string: \"foobar\"\n" + + "\\}\n"); + } + + @Test + public void toStringFormat_testDebugFormat() { + ProtobufToStringOutput.callWithDebugFormat( + () -> + assertThat(message.toString()) + .matches( + String.format( + "%soptional_redacted_string: %s\n" + + "optional_unredacted_string: \"foo\"\n" + + "optional_redacted_message \\{\n" + + " %s\n" + + "\\}\n", + UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX, REDACTED_REGEX))); + } + + @Test + public void toStringFormat_testTextFormat() { + ProtobufToStringOutput.callWithTextFormat( + () -> { + assertThat(message.toString()) + .matches( + "optional_redacted_string: \"bar\"\n" + + "optional_unredacted_string: \"foo\"\n" + + "optional_redacted_message \\{\n" + + " optional_unredacted_nested_string: \"foobar\"\n" + + "\\}\n"); + }); + } + + @Test + public void toStringFormat_testProtoWrapperWithDebugFormat() { + ProtobufToStringOutput.callWithDebugFormat( + () -> { + ArrayList list = new ArrayList<>(); + list.add(message); + assertThat(list.toString()) + .matches( + String.format( + "\\[%soptional_redacted_string: %s\n" + + "optional_unredacted_string: \"foo\"\n" + + "optional_redacted_message \\{\n" + + " %s\n" + + "\\}\n" + + "\\]", + UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX, REDACTED_REGEX)); + }); + } + + @Test + public void toStringFormat_testProtoWrapperWithTextFormat() { + ProtobufToStringOutput.callWithTextFormat( + () -> { + ArrayList list = new ArrayList<>(); + list.add(message); + assertThat(list.toString()) + .matches( + "\\[optional_redacted_string: \"bar\"\n" + + "optional_unredacted_string: \"foo\"\n" + + "optional_redacted_message \\{\n" + + " optional_unredacted_nested_string: \"foobar\"\n" + + "\\}\n" + + "\\]"); + }); + } +}