From a16121550b4afa5cb6842ec6cc9ceab998b2d31a Mon Sep 17 00:00:00 2001 From: Andrew Flynn Date: Tue, 28 May 2013 16:07:32 -0700 Subject: [PATCH] Add toString() method to MessageNano. - All of the real work for printing the proto is actually done in MessageNanoPrinter. - Uses reflection to find proto-defined fields and prints those. - Prints all fields, even defaults and nulls. - Also added a simple test to make sure it handles all proto types well. Tried not to make the test too brittle (but hey it's testing a toString() so how flexible can it be) Change-Id: I3e360ef8b0561041e010c1f3445ec45ecdcd2559 --- .../com/google/protobuf/nano/MessageNano.java | 8 + .../protobuf/nano/MessageNanoPrinter.java | 179 ++++++++++++++++++ .../java/com/google/protobuf/NanoTest.java | 54 ++++++ 3 files changed, 241 insertions(+) create mode 100644 java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java diff --git a/java/src/main/java/com/google/protobuf/nano/MessageNano.java b/java/src/main/java/com/google/protobuf/nano/MessageNano.java index 66080cc87f..d6c1e9a84e 100644 --- a/java/src/main/java/com/google/protobuf/nano/MessageNano.java +++ b/java/src/main/java/com/google/protobuf/nano/MessageNano.java @@ -125,4 +125,12 @@ public abstract class MessageNano { + "never happen)."); } } + + /** + * Intended for debugging purposes only. It does not use ASCII protobuf formatting. + */ + @Override + public String toString() { + return MessageNanoPrinter.print(this); + } } diff --git a/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java b/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java new file mode 100644 index 0000000000..d87346204b --- /dev/null +++ b/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java @@ -0,0 +1,179 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2013 Google Inc. All rights reserved. +// http://code.google.com/p/protobuf/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package com.google.protobuf.nano; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +/** + * Static helper methods for printing nano protos. + * + * @author flynn@google.com Andrew Flynn + */ +public final class MessageNanoPrinter { + // Do not allow instantiation + private MessageNanoPrinter() {} + + private static final String INDENT = " "; + private static final int MAX_STRING_LEN = 200; + + /** + * Returns an text representation of a MessageNano suitable for debugging. + * + *

Employs Java reflection on the given object and recursively prints primitive fields, + * groups, and messages.

+ */ + public static String print(T message) { + if (message == null) { + return "null"; + } + + StringBuffer buf = new StringBuffer(); + try { + print(message.getClass().getSimpleName(), message.getClass(), message, + new StringBuffer(), buf); + } catch (IllegalAccessException e) { + return "Error printing proto: " + e.getMessage(); + } + return buf.toString(); + } + + /** + * Function that will print the given message/class into the StringBuffer. + * Meant to be called recursively. + */ + private static void print(String identifier, Class clazz, Object message, + StringBuffer indentBuf, StringBuffer buf) throws IllegalAccessException { + if (MessageNano.class.isAssignableFrom(clazz)) { + // Nano proto message + buf.append(indentBuf).append(identifier); + + // If null, just print it and return + if (message == null) { + buf.append(": ").append(message).append("\n"); + return; + } + + indentBuf.append(INDENT); + buf.append(" <\n"); + for (Field field : clazz.getFields()) { + // Proto fields are public, non-static variables that do not begin or end with '_' + int modifiers = field.getModifiers(); + String fieldName = field.getName(); + if ((modifiers & Modifier.PUBLIC) != Modifier.PUBLIC + || (modifiers & Modifier.STATIC) == Modifier.STATIC + || fieldName.startsWith("_") || fieldName.endsWith("_")) { + continue; + } + + Class fieldType = field.getType(); + Object value = field.get(message); + + if (fieldType.isArray()) { + Class arrayType = fieldType.getComponentType(); + + // bytes is special since it's not repeated, but is represented by an array + if (arrayType == byte.class) { + print(fieldName, fieldType, value, indentBuf, buf); + } else { + int len = Array.getLength(value); + for (int i = 0; i < len; i++) { + Object elem = Array.get(value, i); + print(fieldName, arrayType, elem, indentBuf, buf); + } + } + } else { + print(fieldName, fieldType, value, indentBuf, buf); + } + } + indentBuf.delete(indentBuf.length() - INDENT.length(), indentBuf.length()); + buf.append(indentBuf).append(">\n"); + } else { + // Primitive value + identifier = deCamelCaseify(identifier); + buf.append(indentBuf).append(identifier).append(": "); + if (message instanceof String) { + String stringMessage = sanitizeString((String) message); + buf.append("\"").append(stringMessage).append("\""); + } else { + buf.append(message); + } + buf.append("\n"); + } + } + + /** + * Converts an identifier of the format "FieldName" into "field_name". + */ + private static String deCamelCaseify(String identifier) { + StringBuffer out = new StringBuffer(); + for (int i = 0; i < identifier.length(); i++) { + char currentChar = identifier.charAt(i); + if (i == 0) { + out.append(Character.toLowerCase(currentChar)); + } else if (Character.isUpperCase(currentChar)) { + out.append('_').append(Character.toLowerCase(currentChar)); + } else { + out.append(currentChar); + } + } + return out.toString(); + } + + /** + * Shortens and escapes the given string. + */ + private static String sanitizeString(String str) { + if (!str.startsWith("http") && str.length() > MAX_STRING_LEN) { + // Trim non-URL strings. + str = str.substring(0, MAX_STRING_LEN) + "[...]"; + } + return escapeString(str); + } + + /** + * Escape everything except for low ASCII code points. + */ + private static String escapeString(String str) { + int strLen = str.length(); + StringBuilder b = new StringBuilder(strLen); + for (int i = 0; i < strLen; i++) { + char original = str.charAt(i); + if (original >= ' ' && original <= '~' && original != '"' && original != '\'') { + b.append(original); + } else { + b.append(String.format("\\u%04x", (int) original)); + } + } + return b.toString(); + } +} diff --git a/java/src/test/java/com/google/protobuf/NanoTest.java b/java/src/test/java/com/google/protobuf/NanoTest.java index 85389d1f64..da17a9e13f 100644 --- a/java/src/test/java/com/google/protobuf/NanoTest.java +++ b/java/src/test/java/com/google/protobuf/NanoTest.java @@ -2101,4 +2101,58 @@ public class NanoTest extends TestCase { input.popLimit(limit); assertEquals(5, input.readRawByte()); } + + // Test a smattering of various proto types for printing + public void testMessageNanoPrinter() { + TestAllTypesNano msg = new TestAllTypesNano(); + msg.optionalInt32 = 14; + msg.optionalFloat = 42.3f; + msg.optionalString = "String \"with' both quotes"; + msg.optionalBytes = new byte[5]; + msg.optionalGroup = new TestAllTypesNano.OptionalGroup(); + msg.optionalGroup.a = 15; + msg.repeatedInt64 = new long[2]; + msg.repeatedInt64[0] = 1L; + msg.repeatedInt64[1] = -1L; + msg.repeatedBytes = new byte[2][]; + msg.repeatedBytes[1] = new byte[5]; + msg.repeatedGroup = new TestAllTypesNano.RepeatedGroup[2]; + msg.repeatedGroup[0] = new TestAllTypesNano.RepeatedGroup(); + msg.repeatedGroup[0].a = -27; + msg.repeatedGroup[1] = new TestAllTypesNano.RepeatedGroup(); + msg.repeatedGroup[1].a = -72; + msg.optionalNestedMessage = new TestAllTypesNano.NestedMessage(); + msg.optionalNestedMessage.bb = 7; + msg.repeatedNestedMessage = new TestAllTypesNano.NestedMessage[2]; + msg.repeatedNestedMessage[0] = new TestAllTypesNano.NestedMessage(); + msg.repeatedNestedMessage[0].bb = 77; + msg.repeatedNestedMessage[1] = new TestAllTypesNano.NestedMessage(); + msg.repeatedNestedMessage[1].bb = 88; + msg.optionalNestedEnum = TestAllTypesNano.BAZ; + msg.repeatedNestedEnum = new int[2]; + msg.repeatedNestedEnum[0] = TestAllTypesNano.BAR; + msg.repeatedNestedEnum[1] = TestAllTypesNano.FOO; + + String protoPrint = msg.toString(); + assertTrue(protoPrint.contains("TestAllTypesNano <")); + assertTrue(protoPrint.contains(" optional_int32: 14")); + assertTrue(protoPrint.contains(" optional_float: 42.3")); + assertTrue(protoPrint.contains(" optional_double: 0.0")); + assertTrue(protoPrint.contains(" optional_string: \"String \\u0022with\\u0027 both quotes\"")); + assertTrue(protoPrint.contains(" optional_bytes: [B@")); + assertTrue(protoPrint.contains(" optionalGroup <\n a: 15\n >")); + + assertTrue(protoPrint.contains(" repeated_int64: 1")); + assertTrue(protoPrint.contains(" repeated_int64: -1")); + assertTrue(protoPrint.contains(" repeated_bytes: null\n repeated_bytes: [B@")); + assertTrue(protoPrint.contains(" repeatedGroup <\n a: -27\n >\n" + + " repeatedGroup <\n a: -72\n >")); + assertTrue(protoPrint.contains(" optionalNestedMessage <\n bb: 7\n >")); + assertTrue(protoPrint.contains(" repeatedNestedMessage <\n bb: 77\n >\n" + + " repeatedNestedMessage <\n bb: 88\n >")); + assertTrue(protoPrint.contains(" optional_nested_enum: 3")); + assertTrue(protoPrint.contains(" repeated_nested_enum: 2\n repeated_nested_enum: 1")); + assertTrue(protoPrint.contains(" default_int32: 41")); + assertTrue(protoPrint.contains(" default_string: \"hello\"")); + } }