aboutsummaryrefslogtreecommitdiffhomepage
path: root/java/src/main/java/com/google/protobuf/TextFormat.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/main/java/com/google/protobuf/TextFormat.java')
-rw-r--r--java/src/main/java/com/google/protobuf/TextFormat.java488
1 files changed, 313 insertions, 175 deletions
diff --git a/java/src/main/java/com/google/protobuf/TextFormat.java b/java/src/main/java/com/google/protobuf/TextFormat.java
index 7ca2b4bf..d5fbdabf 100644
--- a/java/src/main/java/com/google/protobuf/TextFormat.java
+++ b/java/src/main/java/com/google/protobuf/TextFormat.java
@@ -46,15 +46,17 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
- * Provide ascii text parsing and formatting support for proto2 instances.
+ * Provide text parsing and formatting support for proto2 instances.
* The implementation largely follows google/protobuf/text_format.cc.
*
* @author wenboz@google.com Wenbo Zhu
* @author kenton@google.com Kenton Varda
*/
public final class TextFormat {
- private TextFormat() {
- }
+ private TextFormat() {}
+
+ private static final Printer DEFAULT_PRINTER = new Printer(false);
+ private static final Printer SINGLE_LINE_PRINTER = new Printer(true);
/**
* Outputs a textual representation of the Protocol Message supplied into
@@ -63,16 +65,44 @@ public final class TextFormat {
*/
public static void print(final Message message, final Appendable output)
throws IOException {
- final TextGenerator generator = new TextGenerator(output);
- print(message, generator);
+ DEFAULT_PRINTER.print(message, new TextGenerator(output));
}
/** Outputs a textual representation of {@code fields} to {@code output}. */
public static void print(final UnknownFieldSet fields,
final Appendable output)
throws IOException {
- final TextGenerator generator = new TextGenerator(output);
- printUnknownFields(fields, generator);
+ DEFAULT_PRINTER.printUnknownFields(fields, new TextGenerator(output));
+ }
+
+ /**
+ * Generates a human readable form of this message, useful for debugging and
+ * other purposes, with no newline characters.
+ */
+ public static String shortDebugString(final Message message) {
+ try {
+ final StringBuilder sb = new StringBuilder();
+ SINGLE_LINE_PRINTER.print(message, new TextGenerator(sb));
+ // Single line mode currently might have an extra space at the end.
+ return sb.toString().trim();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Generates a human readable form of the unknown fields, useful for debugging
+ * and other purposes, with no newline characters.
+ */
+ public static String shortDebugString(final UnknownFieldSet fields) {
+ try {
+ final StringBuilder sb = new StringBuilder();
+ SINGLE_LINE_PRINTER.printUnknownFields(fields, new TextGenerator(sb));
+ // Single line mode currently might have an extra space at the end.
+ return sb.toString().trim();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
}
/**
@@ -85,9 +115,7 @@ public final class TextFormat {
print(message, text);
return text.toString();
} catch (IOException e) {
- throw new RuntimeException(
- "Writing to a StringBuilder threw an IOException (should never " +
- "happen).", e);
+ throw new IllegalStateException(e);
}
}
@@ -101,28 +129,15 @@ public final class TextFormat {
print(fields, text);
return text.toString();
} catch (IOException e) {
- throw new RuntimeException(
- "Writing to a StringBuilder threw an IOException (should never " +
- "happen).", e);
+ throw new IllegalStateException(e);
}
}
- private static void print(final Message message,
- final TextGenerator generator)
- throws IOException {
- for (final Map.Entry<FieldDescriptor, Object> field :
- message.getAllFields().entrySet()) {
- printField(field.getKey(), field.getValue(), generator);
- }
- printUnknownFields(message.getUnknownFields(), generator);
- }
-
public static void printField(final FieldDescriptor field,
final Object value,
final Appendable output)
throws IOException {
- final TextGenerator generator = new TextGenerator(output);
- printField(field, value, generator);
+ DEFAULT_PRINTER.printField(field, value, new TextGenerator(output));
}
public static String printFieldToString(final FieldDescriptor field,
@@ -132,157 +147,263 @@ public final class TextFormat {
printField(field, value, text);
return text.toString();
} catch (IOException e) {
- throw new RuntimeException(
- "Writing to a StringBuilder threw an IOException (should never " +
- "happen).", e);
+ throw new IllegalStateException(e);
}
}
- private static void printField(final FieldDescriptor field,
- final Object value,
- final TextGenerator generator)
- throws IOException {
- if (field.isRepeated()) {
- // Repeated field. Print each element.
- for (final Object element : (List<?>) value) {
- printSingleField(field, element, generator);
- }
- } else {
- printSingleField(field, value, generator);
+ /**
+ * Outputs a textual representation of the value of given field value.
+ *
+ * @param field the descriptor of the field
+ * @param value the value of the field
+ * @param output the output to which to append the formatted value
+ * @throws ClassCastException if the value is not appropriate for the
+ * given field descriptor
+ * @throws IOException if there is an exception writing to the output
+ */
+ public static void printFieldValue(final FieldDescriptor field,
+ final Object value,
+ final Appendable output)
+ throws IOException {
+ DEFAULT_PRINTER.printFieldValue(field, value, new TextGenerator(output));
+ }
+
+ /**
+ * Outputs a textual representation of the value of an unknown field.
+ *
+ * @param tag the field's tag number
+ * @param value the value of the field
+ * @param output the output to which to append the formatted value
+ * @throws ClassCastException if the value is not appropriate for the
+ * given field descriptor
+ * @throws IOException if there is an exception writing to the output
+ */
+ public static void printUnknownFieldValue(final int tag,
+ final Object value,
+ final Appendable output)
+ throws IOException {
+ printUnknownFieldValue(tag, value, new TextGenerator(output));
+ }
+
+ private static void printUnknownFieldValue(final int tag,
+ final Object value,
+ final TextGenerator generator)
+ throws IOException {
+ switch (WireFormat.getTagWireType(tag)) {
+ case WireFormat.WIRETYPE_VARINT:
+ generator.print(unsignedToString((Long) value));
+ break;
+ case WireFormat.WIRETYPE_FIXED32:
+ generator.print(
+ String.format((Locale) null, "0x%08x", (Integer) value));
+ break;
+ case WireFormat.WIRETYPE_FIXED64:
+ generator.print(String.format((Locale) null, "0x%016x", (Long) value));
+ break;
+ case WireFormat.WIRETYPE_LENGTH_DELIMITED:
+ generator.print("\"");
+ generator.print(escapeBytes((ByteString) value));
+ generator.print("\"");
+ break;
+ case WireFormat.WIRETYPE_START_GROUP:
+ DEFAULT_PRINTER.printUnknownFields((UnknownFieldSet) value, generator);
+ break;
+ default:
+ throw new IllegalArgumentException("Bad tag: " + tag);
}
}
- private static void printSingleField(final FieldDescriptor field,
- final Object value,
- final TextGenerator generator)
- throws IOException {
- if (field.isExtension()) {
- generator.print("[");
- // We special-case MessageSet elements for compatibility with proto1.
- if (field.getContainingType().getOptions().getMessageSetWireFormat()
- && (field.getType() == FieldDescriptor.Type.MESSAGE)
- && (field.isOptional())
- // object equality
- && (field.getExtensionScope() == field.getMessageType())) {
- generator.print(field.getMessageType().getFullName());
- } else {
- generator.print(field.getFullName());
+ /** Helper class for converting protobufs to text. */
+ private static final class Printer {
+ /** Whether to omit newlines from the output. */
+ final boolean singleLineMode;
+
+ private Printer(final boolean singleLineMode) {
+ this.singleLineMode = singleLineMode;
+ }
+
+ private void print(final Message message, final TextGenerator generator)
+ throws IOException {
+ for (Map.Entry<FieldDescriptor, Object> field
+ : message.getAllFields().entrySet()) {
+ printField(field.getKey(), field.getValue(), generator);
}
- generator.print("]");
- } else {
- if (field.getType() == FieldDescriptor.Type.GROUP) {
- // Groups must be serialized with their original capitalization.
- generator.print(field.getMessageType().getName());
+ printUnknownFields(message.getUnknownFields(), generator);
+ }
+
+ private void printField(final FieldDescriptor field, final Object value,
+ final TextGenerator generator) throws IOException {
+ if (field.isRepeated()) {
+ // Repeated field. Print each element.
+ for (Object element : (List<?>) value) {
+ printSingleField(field, element, generator);
+ }
} else {
- generator.print(field.getName());
+ printSingleField(field, value, generator);
}
}
- if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
- generator.print(" {\n");
- generator.indent();
- } else {
- generator.print(": ");
- }
+ private void printSingleField(final FieldDescriptor field,
+ final Object value,
+ final TextGenerator generator)
+ throws IOException {
+ if (field.isExtension()) {
+ generator.print("[");
+ // We special-case MessageSet elements for compatibility with proto1.
+ if (field.getContainingType().getOptions().getMessageSetWireFormat()
+ && (field.getType() == FieldDescriptor.Type.MESSAGE)
+ && (field.isOptional())
+ // object equality
+ && (field.getExtensionScope() == field.getMessageType())) {
+ generator.print(field.getMessageType().getFullName());
+ } else {
+ generator.print(field.getFullName());
+ }
+ generator.print("]");
+ } else {
+ if (field.getType() == FieldDescriptor.Type.GROUP) {
+ // Groups must be serialized with their original capitalization.
+ generator.print(field.getMessageType().getName());
+ } else {
+ generator.print(field.getName());
+ }
+ }
- printFieldValue(field, value, generator);
+ if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+ if (singleLineMode) {
+ generator.print(" { ");
+ } else {
+ generator.print(" {\n");
+ generator.indent();
+ }
+ } else {
+ generator.print(": ");
+ }
- if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
- generator.outdent();
- generator.print("}");
+ printFieldValue(field, value, generator);
+
+ if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+ if (singleLineMode) {
+ generator.print("} ");
+ } else {
+ generator.outdent();
+ generator.print("}\n");
+ }
+ } else {
+ if (singleLineMode) {
+ generator.print(" ");
+ } else {
+ generator.print("\n");
+ }
+ }
}
- generator.print("\n");
- }
- private static void printFieldValue(final FieldDescriptor field,
- final Object value,
- final TextGenerator generator)
- throws IOException {
- switch (field.getType()) {
- case INT32:
- case INT64:
- case SINT32:
- case SINT64:
- case SFIXED32:
- case SFIXED64:
- case FLOAT:
- case DOUBLE:
- case BOOL:
- // Good old toString() does what we want for these types.
- generator.print(value.toString());
- break;
+ private void printFieldValue(final FieldDescriptor field,
+ final Object value,
+ final TextGenerator generator)
+ throws IOException {
+ switch (field.getType()) {
+ case INT32:
+ case SINT32:
+ case SFIXED32:
+ generator.print(((Integer) value).toString());
+ break;
- case UINT32:
- case FIXED32:
- generator.print(unsignedToString((Integer) value));
- break;
+ case INT64:
+ case SINT64:
+ case SFIXED64:
+ generator.print(((Long) value).toString());
+ break;
- case UINT64:
- case FIXED64:
- generator.print(unsignedToString((Long) value));
- break;
+ case BOOL:
+ generator.print(((Boolean) value).toString());
+ break;
- case STRING:
- generator.print("\"");
- generator.print(escapeText((String) value));
- generator.print("\"");
- break;
+ case FLOAT:
+ generator.print(((Float) value).toString());
+ break;
- case BYTES:
- generator.print("\"");
- generator.print(escapeBytes((ByteString) value));
- generator.print("\"");
- break;
+ case DOUBLE:
+ generator.print(((Double) value).toString());
+ break;
- case ENUM:
- generator.print(((EnumValueDescriptor) value).getName());
- break;
+ case UINT32:
+ case FIXED32:
+ generator.print(unsignedToString((Integer) value));
+ break;
- case MESSAGE:
- case GROUP:
- print((Message) value, generator);
- break;
- }
- }
+ case UINT64:
+ case FIXED64:
+ generator.print(unsignedToString((Long) value));
+ break;
- private static void printUnknownFields(final UnknownFieldSet unknownFields,
- final TextGenerator generator)
- throws IOException {
- for (final Map.Entry<Integer, UnknownFieldSet.Field> entry :
- unknownFields.asMap().entrySet()) {
- final UnknownFieldSet.Field field = entry.getValue();
+ case STRING:
+ generator.print("\"");
+ generator.print(escapeText((String) value));
+ generator.print("\"");
+ break;
- for (final long value : field.getVarintList()) {
- generator.print(entry.getKey().toString());
- generator.print(": ");
- generator.print(unsignedToString(value));
- generator.print("\n");
+ case BYTES:
+ generator.print("\"");
+ generator.print(escapeBytes((ByteString) value));
+ generator.print("\"");
+ break;
+
+ case ENUM:
+ generator.print(((EnumValueDescriptor) value).getName());
+ break;
+
+ case MESSAGE:
+ case GROUP:
+ print((Message) value, generator);
+ break;
}
- for (final int value : field.getFixed32List()) {
- generator.print(entry.getKey().toString());
- generator.print(": ");
- generator.print(String.format((Locale) null, "0x%08x", value));
- generator.print("\n");
+ }
+
+ private void printUnknownFields(final UnknownFieldSet unknownFields,
+ final TextGenerator generator)
+ throws IOException {
+ for (Map.Entry<Integer, UnknownFieldSet.Field> entry :
+ unknownFields.asMap().entrySet()) {
+ final int number = entry.getKey();
+ final UnknownFieldSet.Field field = entry.getValue();
+ printUnknownField(number, WireFormat.WIRETYPE_VARINT,
+ field.getVarintList(), generator);
+ printUnknownField(number, WireFormat.WIRETYPE_FIXED32,
+ field.getFixed32List(), generator);
+ printUnknownField(number, WireFormat.WIRETYPE_FIXED64,
+ field.getFixed64List(), generator);
+ printUnknownField(number, WireFormat.WIRETYPE_LENGTH_DELIMITED,
+ field.getLengthDelimitedList(), generator);
+ for (final UnknownFieldSet value : field.getGroupList()) {
+ generator.print(entry.getKey().toString());
+ if (singleLineMode) {
+ generator.print(" { ");
+ } else {
+ generator.print(" {\n");
+ generator.indent();
+ }
+ printUnknownFields(value, generator);
+ if (singleLineMode) {
+ generator.print("} ");
+ } else {
+ generator.outdent();
+ generator.print("}\n");
+ }
+ }
}
- for (final long value : field.getFixed64List()) {
- generator.print(entry.getKey().toString());
+ }
+
+ private void printUnknownField(final int number,
+ final int wireType,
+ final List<?> values,
+ final TextGenerator generator)
+ throws IOException {
+ for (final Object value : values) {
+ generator.print(String.valueOf(number));
generator.print(": ");
- generator.print(String.format((Locale) null, "0x%016x", value));
- generator.print("\n");
- }
- for (final ByteString value : field.getLengthDelimitedList()) {
- generator.print(entry.getKey().toString());
- generator.print(": \"");
- generator.print(escapeBytes(value));
- generator.print("\"\n");
- }
- for (final UnknownFieldSet value : field.getGroupList()) {
- generator.print(entry.getKey().toString());
- generator.print(" {\n");
- generator.indent();
- printUnknownFields(value, generator);
- generator.outdent();
- generator.print("}\n");
+ printUnknownFieldValue(wireType, value, generator);
+ generator.print(singleLineMode ? " " : "\n");
}
}
}
@@ -312,9 +433,9 @@ public final class TextFormat {
* An inner class for writing text to the output stream.
*/
private static final class TextGenerator {
- private Appendable output;
- private boolean atStartOfLine = true;
+ private final Appendable output;
private final StringBuilder indent = new StringBuilder();
+ private boolean atStartOfLine = true;
private TextGenerator(final Appendable output) {
this.output = output;
@@ -670,10 +791,14 @@ public final class TextFormat {
* Otherwise, throw a {@link ParseException}.
*/
public boolean consumeBoolean() throws ParseException {
- if (currentToken.equals("true")) {
+ if (currentToken.equals("true") ||
+ currentToken.equals("t") ||
+ currentToken.equals("1")) {
nextToken();
return true;
- } else if (currentToken.equals("false")) {
+ } else if (currentToken.equals("false") ||
+ currentToken.equals("f") ||
+ currentToken.equals("0")) {
nextToken();
return false;
} else {
@@ -1063,6 +1188,9 @@ public final class TextFormat {
case '\'': builder.append("\\\'"); break;
case '"' : builder.append("\\\""); break;
default:
+ // Note: Bytes with the high-order bit set should be escaped. Since
+ // bytes are signed, such bytes will compare less than 0x20, hence
+ // the following line is correct.
if (b >= 0x20) {
builder.append((char) b);
} else {
@@ -1082,27 +1210,37 @@ public final class TextFormat {
* {@link #escapeBytes(ByteString)}. Two-digit hex escapes (starting with
* "\x") are also recognized.
*/
- static ByteString unescapeBytes(final CharSequence input)
+ static ByteString unescapeBytes(final CharSequence charString)
throws InvalidEscapeSequenceException {
- final byte[] result = new byte[input.length()];
+ // First convert the Java characater sequence to UTF-8 bytes.
+ ByteString input = ByteString.copyFromUtf8(charString.toString());
+ // Then unescape certain byte sequences introduced by ASCII '\\'. The valid
+ // escapes can all be expressed with ASCII characters, so it is safe to
+ // operate on bytes here.
+ //
+ // Unescaping the input byte array will result in a byte sequence that's no
+ // longer than the input. That's because each escape sequence is between
+ // two and four bytes long and stands for a single byte.
+ final byte[] result = new byte[input.size()];
int pos = 0;
- for (int i = 0; i < input.length(); i++) {
- char c = input.charAt(i);
+ for (int i = 0; i < input.size(); i++) {
+ byte c = input.byteAt(i);
if (c == '\\') {
- if (i + 1 < input.length()) {
+ if (i + 1 < input.size()) {
++i;
- c = input.charAt(i);
+ c = input.byteAt(i);
if (isOctal(c)) {
// Octal escape.
int code = digitValue(c);
- if (i + 1 < input.length() && isOctal(input.charAt(i + 1))) {
+ if (i + 1 < input.size() && isOctal(input.byteAt(i + 1))) {
++i;
- code = code * 8 + digitValue(input.charAt(i));
+ code = code * 8 + digitValue(input.byteAt(i));
}
- if (i + 1 < input.length() && isOctal(input.charAt(i + 1))) {
+ if (i + 1 < input.size() && isOctal(input.byteAt(i + 1))) {
++i;
- code = code * 8 + digitValue(input.charAt(i));
+ code = code * 8 + digitValue(input.byteAt(i));
}
+ // TODO: Check that 0 <= code && code <= 0xFF.
result[pos++] = (byte)code;
} else {
switch (c) {
@@ -1120,31 +1258,31 @@ public final class TextFormat {
case 'x':
// hex escape
int code = 0;
- if (i + 1 < input.length() && isHex(input.charAt(i + 1))) {
+ if (i + 1 < input.size() && isHex(input.byteAt(i + 1))) {
++i;
- code = digitValue(input.charAt(i));
+ code = digitValue(input.byteAt(i));
} else {
throw new InvalidEscapeSequenceException(
- "Invalid escape sequence: '\\x' with no digits");
+ "Invalid escape sequence: '\\x' with no digits");
}
- if (i + 1 < input.length() && isHex(input.charAt(i + 1))) {
+ if (i + 1 < input.size() && isHex(input.byteAt(i + 1))) {
++i;
- code = code * 16 + digitValue(input.charAt(i));
+ code = code * 16 + digitValue(input.byteAt(i));
}
result[pos++] = (byte)code;
break;
default:
throw new InvalidEscapeSequenceException(
- "Invalid escape sequence: '\\" + c + '\'');
+ "Invalid escape sequence: '\\" + (char)c + '\'');
}
}
} else {
throw new InvalidEscapeSequenceException(
- "Invalid escape sequence: '\\' at end of string.");
+ "Invalid escape sequence: '\\' at end of string.");
}
} else {
- result[pos++] = (byte)c;
+ result[pos++] = c;
}
}
@@ -1182,12 +1320,12 @@ public final class TextFormat {
}
/** Is this an octal digit? */
- private static boolean isOctal(final char c) {
+ private static boolean isOctal(final byte c) {
return '0' <= c && c <= '7';
}
/** Is this a hex digit? */
- private static boolean isHex(final char c) {
+ private static boolean isHex(final byte c) {
return ('0' <= c && c <= '9') ||
('a' <= c && c <= 'f') ||
('A' <= c && c <= 'F');
@@ -1198,7 +1336,7 @@ public final class TextFormat {
* numeric value. This is like {@code Character.digit()} but we don't accept
* non-ASCII digits.
*/
- private static int digitValue(final char c) {
+ private static int digitValue(final byte c) {
if ('0' <= c && c <= '9') {
return c - '0';
} else if ('a' <= c && c <= 'z') {