PrettySerializer.java
/*
* Copyright 2015-2016 Providence Authors
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.providence.serializer;
import net.morimekta.providence.PApplicationException;
import net.morimekta.providence.PApplicationExceptionType;
import net.morimekta.providence.PEnumBuilder;
import net.morimekta.providence.PEnumValue;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageBuilder;
import net.morimekta.providence.PMessageOrBuilder;
import net.morimekta.providence.PServiceCall;
import net.morimekta.providence.PServiceCallType;
import net.morimekta.providence.PUnion;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PEnumDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PList;
import net.morimekta.providence.descriptor.PMap;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PPrimitive;
import net.morimekta.providence.descriptor.PService;
import net.morimekta.providence.descriptor.PServiceMethod;
import net.morimekta.providence.descriptor.PSet;
import net.morimekta.providence.serializer.pretty.PrettyException;
import net.morimekta.providence.serializer.pretty.PrettyLexer;
import net.morimekta.providence.serializer.pretty.PrettyToken;
import net.morimekta.providence.serializer.pretty.PrettyTokenizer;
import net.morimekta.util.Binary;
import net.morimekta.util.Strings;
import net.morimekta.util.io.CountingOutputStream;
import net.morimekta.util.io.IndentedPrintWriter;
import net.morimekta.util.lexer.LexerException;
import net.morimekta.util.lexer.UncheckedLexerException;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kFieldSep;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kFieldValueSep;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kKeyValueSep;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kListEnd;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kListSep;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kListStart;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kLiteralDoubleQuote;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kMessageEnd;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kMessageStart;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kParamsEnd;
import static net.morimekta.providence.serializer.pretty.PrettyToken.kParamsStart;
/**
* Pretty printer that can print message content for easily reading and
* debugging. This is a write only format used in stringifying messages.
*/
public class PrettySerializer extends Serializer {
public final static String MEDIA_TYPE = "text/plain";
private static final PrettySerializer DEBUG_STRING_SERIALIZER = new PrettySerializer();
private final static String INDENT = " ";
private final static String SPACE = " ";
private final static String NEWLINE = "\n";
private final static String LIST_SEP = ",";
private final String indent;
private final String space;
private final String newline;
private final String entrySep;
private final boolean strict;
private final boolean prefixWithQualifiedName;
/**
* Prints a pretty formatted string that is optimized for diffing (mainly
* for testing and debugging).
*
* @param message The message to stringify.
* @param <Message> The message type.
* @return The resulting string.
*/
@Nonnull
public static <Message extends PMessage<Message>>
String toDebugString(PMessageOrBuilder<Message> message) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DEBUG_STRING_SERIALIZER.serialize(out, message);
return new String(out.toByteArray(), UTF_8);
}
/**
* Parses a pretty formatted string, and makes exceptions unchecked.
*
* @param string The message string to parse.
* @param descriptor The message descriptor.
* @param <Message> The message type.
* @return The parsed message.
*/
@Nonnull
public static <Message extends PMessage<Message>>
Message parseDebugString(String string, PMessageDescriptor<Message> descriptor) {
try {
ByteArrayInputStream in = new ByteArrayInputStream(string.getBytes(UTF_8));
return DEBUG_STRING_SERIALIZER.deserialize(in, descriptor);
} catch (LexerException e) {
throw new UncheckedLexerException(e);
} catch (IOException e) {
throw new UncheckedIOException(e.getMessage(), e);
}
}
public PrettySerializer() {
this(DEFAULT_STRICT);
}
public PrettySerializer(boolean strict) {
this(INDENT, SPACE, NEWLINE, "", strict, false);
}
/**
* Make a PrettySerializer that generates content similar to the PMessage asString methods.
* The output of this has <b>very little</b> whitespace, so can be pretty difficult to read.
* It's similar to the {@link #string()} variant, but without the qualified name prefix.
*
* @return Compact pretty serializer.
*/
public PrettySerializer compact() {
return new PrettySerializer("", "", "", LIST_SEP, strict, false);
}
/**
* Make a PrettySerializer that generates content similar to the PMessage toString methods.
* The output of this has <b>very little</b> whitespace, so can be pretty difficult to read.
* It prefixes the message with the root message qualified name, as any
* {@link PMessage}.toString() would expect.
*
* @return String pretty serializer.
*/
public PrettySerializer string() {
return new PrettySerializer("", "", "", LIST_SEP, strict, true);
}
/**
* Make a PrettySerializer that generates content similar to what the ProvidenceConfig
* reads. It will not make use of references or anything fancy though.
*
* @return Config-like pretty serializer.
*/
public PrettySerializer config() {
return new PrettySerializer(indent,
space,
newline,
entrySep,
strict,
true);
}
private PrettySerializer(String indent,
String space,
String newline,
String entrySep,
boolean strict,
boolean prefixWithQualifiedName) {
this.indent = indent;
this.space = space;
this.newline = newline;
this.entrySep = entrySep;
this.strict = strict;
this.prefixWithQualifiedName = prefixWithQualifiedName;
}
public <Message extends PMessage<Message>>
int serialize(@Nonnull OutputStream out, @Nonnull PMessageOrBuilder<Message> message) {
CountingOutputStream cout = new CountingOutputStream(out);
IndentedPrintWriter builder = new IndentedPrintWriter(cout, indent, newline);
if (prefixWithQualifiedName) {
builder.append(message.descriptor().getQualifiedName())
.append(space);
}
appendMessage(builder, message, false);
builder.flush();
return cout.getByteCount();
}
@Override
public <Message extends PMessage<Message>>
int serialize(@Nonnull OutputStream out, @Nonnull PServiceCall<Message> call) {
CountingOutputStream cout = new CountingOutputStream(out);
IndentedPrintWriter builder = new IndentedPrintWriter(cout, indent, newline);
if (call.getSequence() != 0) {
builder.format("%d: ", call.getSequence());
}
builder.format("%s %s",
call.getType().asString().toLowerCase(Locale.US),
call.getMethod())
.begin(indent + indent);
appendMessage(builder, call.getMessage(), true);
builder.end()
.newline()
.flush();
return cout.getByteCount();
}
@Nonnull
@Override
@SuppressWarnings("unchecked")
public <Message extends PMessage<Message>>
PServiceCall<Message> deserialize(
@Nonnull InputStream input,
@Nonnull PService service) throws IOException {
String methodName = null;
int sequence = 0;
PServiceCallType callType = null;
try {
// pretty printed service calls cannot be chained-serialized, so this should be totally safe.
PrettyLexer lexer = new PrettyLexer(input);
PrettyToken token = lexer.expect("Sequence or type");
if (token.isInteger()) {
sequence = (int) token.parseInteger();
lexer.expect("Sequence type sep", t -> t.isSymbol(kKeyValueSep));
token = lexer.expect("Call Type", PrettyToken::isIdentifier);
}
callType = PServiceCallType.findByName(token.toString().toUpperCase(Locale.US));
if (callType == null) {
throw new PrettyException(token, "No such call type %s", token)
.setLine(token.line())
.setExceptionType(PApplicationExceptionType.INVALID_MESSAGE_TYPE);
}
token = lexer.expect("method name", PrettyToken::isIdentifier);
methodName = token.toString();
PServiceMethod method = service.getMethod(methodName);
if (method == null) {
throw new PrettyException(token, "no such method %s on service %s",
methodName, service.getQualifiedName())
.setLine(token.line())
.setExceptionType(PApplicationExceptionType.UNKNOWN_METHOD);
}
lexer.expect("call params start", t -> t.isSymbol(kParamsStart));
Message message;
switch (callType) {
case CALL:
case ONEWAY:
message = (Message) readMessage(
lexer,
method.getRequestType(),
true);
break;
case REPLY:
message = (Message) readMessage(
lexer,
Optional.ofNullable(method.getResponseType())
.orElseThrow(() -> new PrettyException("No reply type for method %s", method.getName())),
true);
break;
case EXCEPTION:
message = (Message) readMessage(
lexer,
PApplicationException.kDescriptor,
true);
break;
default:
throw new IllegalStateException("Unreachable code reached");
}
return new PServiceCall<>(methodName, callType, sequence, message);
} catch (LexerException e) {
throw new PrettyException(e, e.getMessage());
} catch (PrettyException e) {
e.setCallType(callType)
.setSequenceNo(sequence)
.setMethodName(methodName);
throw e;
} catch (IOException e) {
throw new SerializerException(e, e.getMessage())
.setCallType(callType)
.setSequenceNo(sequence)
.setMethodName(methodName);
}
}
@Nonnull
@Override
public <Message extends PMessage<Message>>
Message deserialize(@Nonnull InputStream input,
@Nonnull PMessageDescriptor<Message> descriptor)
throws IOException {
try {
PrettyTokenizer tokenizer = new PrettyTokenizer(input);
PrettyLexer lexer = new PrettyLexer(tokenizer);
PrettyToken first = lexer.peek("start of message");
if (first.isSymbol(kMessageStart)) {
lexer.next();
} else if (first.isQualifiedIdentifier()) {
if (first.toString().equals(descriptor.getQualifiedName())) {
lexer.next(); // skip the name
lexer.expectSymbol("message start after qualifier", kMessageStart);
} else {
throw tokenizer.failure(first, "Expected qualifier " + descriptor.getQualifiedName() +
" or message start, but got '" + first + "'");
}
} else {
throw tokenizer.failure(first, "Expected message start or qualifier, but got '" + first + "'");
}
return readMessage(lexer, descriptor, false);
} catch (LexerException e) {
throw new PrettyException(e, e.getMessage());
}
}
private <Message extends PMessage<Message>>
Message readMessage(PrettyLexer tokenizer,
PMessageDescriptor<Message> descriptor,
boolean params)
throws IOException {
PMessageBuilder<Message> builder = descriptor.builder();
PrettyToken token = tokenizer.expect("message field or end");
for (;;) {
if (params) {
if (token.isSymbol(kParamsEnd)) {
break;
}
} else if (token.isSymbol(kMessageEnd)) {
break;
}
if (!token.isIdentifier()) {
throw new PrettyException(token, "Expected field name, but got '%s'",
Strings.escape(token))
.setLine(token.line());
}
PField field = descriptor.findFieldByName(token.toString());
tokenizer.expectSymbol("field value separator", kFieldValueSep);
if (field == null) {
consumeValue(tokenizer, tokenizer.expect("field value"));
} else {
builder.set(field.getId(), readFieldValue(
tokenizer, tokenizer.expect("field value"), field.getDescriptor()));
}
token = tokenizer.expect("Message field or end");
if (token.isSymbol(kListSep) || token.isSymbol(kFieldSep)) {
token = tokenizer.expect("Message field or end");
}
}
return builder.build();
}
private Object readFieldValue(PrettyLexer lexer, PrettyToken token, PDescriptor descriptor)
throws IOException {
switch (descriptor.getType()) {
case VOID: {
// Even void fields needs a value token...
// Allow any boolean true value that is an _identifier_. No numbers here.
switch (token.toString().toLowerCase(Locale.US)) {
case "1":
case "t":
case "true":
case "y":
case "yes":
return Boolean.TRUE;
}
throw new PrettyException(token, "Invalid void value " + token)
.setLine(token.line());
}
case BOOL: {
switch (token.toString().toLowerCase(Locale.US)) {
case "1":
case "t":
case "true":
case "y":
case "yes":
return Boolean.TRUE;
case "0":
case "f":
case "false":
case "n":
case "no":
return Boolean.FALSE;
}
throw new PrettyException(token, "Invalid boolean value " + token)
.setLine(token.line());
}
case BYTE: {
if (token.isInteger()) {
long val = token.parseInteger();
if (val > Byte.MAX_VALUE || val < Byte.MIN_VALUE) {
throw new PrettyException(token, "Byte value out of bounds: " + token)
.setLine(token.line());
}
return (byte) val;
} else {
throw new PrettyException(token, "Invalid byte value: " + token)
.setLine(token.line());
}
}
case I16: {
if (token.isInteger()) {
long val = token.parseInteger();
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
throw new PrettyException(token, "Short value out of bounds: " + token)
.setLine(token.line());
}
return (short) val;
} else {
throw new PrettyException(token, "Invalid byte value: " + token)
.setLine(token.line());
}
}
case I32: {
if (token.isInteger()) {
long val = token.parseInteger();
if (val > Integer.MAX_VALUE || val < Integer.MIN_VALUE) {
throw new PrettyException(token, "Integer value out of bounds: " + token)
.setLine(token.line());
}
return (int) val;
} else {
throw new PrettyException(token, "Invalid byte value: " + token)
.setLine(token.line());
}
}
case I64: {
if (token.isInteger()) {
return token.parseInteger();
} else {
throw new PrettyException(token, "Invalid byte value: " + token)
.setLine(token.line());
}
}
case DOUBLE: {
try {
return token.parseDouble();
} catch (NumberFormatException nfe) {
throw new PrettyException(token, "Number format error: " + nfe.getMessage())
.setLine(token.line());
}
}
case STRING: {
if (!token.isStringLiteral()) {
throw new PrettyException(token, "Expected string literal, but got '%s'", token)
.setLine(token.line());
}
return token.decodeString(strict);
}
case BINARY: {
switch (token.toString()) {
case "b64": {
lexer.expectSymbol("binary content start", kParamsStart);
PrettyToken binary = lexer.readBinary(kParamsEnd);
if (binary == null) {
return Binary.empty();
}
try {
return Binary.fromBase64(binary.toString().replaceAll("[\\s\\n=]*", ""));
} catch (IllegalArgumentException e) {
throw lexer.failure(binary, e.getMessage()).initCause(e);
}
}
case "hex": {
lexer.expectSymbol("binary content start", kParamsStart);
PrettyToken binary = lexer.readBinary(kParamsEnd);
if (binary == null) {
return Binary.empty();
}
try {
return Binary.fromHexString(binary.toString().replaceAll("[\\s\\n]*", ""));
} catch (IllegalArgumentException e) {
throw lexer.failure(binary, e.getMessage()).initCause(e);
}
}
default:
throw new PrettyException(token, "Unrecognized binary format " + token)
.setLine(token.line());
}
}
case ENUM: {
PEnumBuilder b = ((PEnumDescriptor) descriptor).builder();
b.setByName(token.toString());
if (strict && !b.valid()) {
throw new PrettyException(token, "No such " + descriptor.getQualifiedName() + " value " + token)
.setLine(token.line());
}
return b.build();
}
case MESSAGE: {
if (!token.isSymbol(kMessageStart)) {
throw new PrettyException(token, "Expected message start, but got '%s'", token)
.setLine(token.line());
}
return readMessage(lexer, (PMessageDescriptor<?>) descriptor, false);
}
case MAP: {
if (!token.isSymbol(kMessageStart)) {
throw new PrettyException(token, "Expected map start, but got '%s'", token)
.setLine(token.line());
}
@SuppressWarnings("unchecked")
PMap<Object, Object> pMap = (PMap) descriptor;
PDescriptor kDesc = pMap.keyDescriptor();
PDescriptor iDesc = pMap.itemDescriptor();
PMap.Builder<Object, Object> builder = pMap.builder(10);
token = lexer.expect("list end or value");
while (!token.isSymbol(kMessageEnd)) {
Object key = readFieldValue(lexer, token, kDesc);
lexer.expectSymbol("map kv sep", kKeyValueSep);
Object value = readFieldValue(lexer, lexer.expect("map value"), iDesc);
builder.put(key, value);
token = lexer.expect("map sep, end or value");
if (token.isSymbol(kListSep) || token.isSymbol(kFieldSep)) {
token = lexer.expect("map end or value");
}
}
return builder.build();
}
case LIST: {
if (!token.isSymbol(kListStart)) {
throw new PrettyException(token, "Expected list start, but got '%s'", token)
.setLine(token.line());
}
@SuppressWarnings("unchecked")
PList<Object> pList = (PList) descriptor;
PDescriptor iDesc = pList.itemDescriptor();
PList.Builder<Object> builder = pList.builder(10);
token = lexer.expect("list end or value");
while (!token.isSymbol(kListEnd)) {
builder.add(readFieldValue(lexer, token, iDesc));
token = lexer.expect("list sep, end or value");
if (token.isSymbol(kListSep)) {
token = lexer.expect("list end or value");
}
}
return builder.build();
}
case SET: {
if (!token.isSymbol(kListStart)) {
throw new PrettyException(token, "Expected set start, but got '%s'", token)
.setLine(token.line());
}
@SuppressWarnings("unchecked")
PSet<Object> pList = (PSet) descriptor;
PDescriptor iDesc = pList.itemDescriptor();
PSet.Builder<Object> builder = pList.builder(10);
token = lexer.expect("set end or value");
while (!token.isSymbol(kListEnd)) {
builder.add(readFieldValue(lexer, token, iDesc));
token = lexer.expect("set sep, end or value");
if (token.isSymbol(kListSep)) {
token = lexer.expect("set end or value");
}
}
return builder.build();
}
default: {
throw new IllegalStateException("Unhandled field type: " + descriptor.getType());
}
}
}
@Override
public boolean binaryProtocol() {
return false;
}
@Override
public void verifyEndOfContent(@Nonnull InputStream input) throws IOException {
try {
PrettyLexer tokenizer = new PrettyLexer(input);
PrettyToken next = tokenizer.next();
if (next != null) {
throw new PrettyException(next, "More content after end: " + next);
}
} catch (LexerException e) {
throw new PrettyException(e, "More content after end: " + e.getMessage());
} finally {
input.close();
}
}
@Nonnull
@Override
public String mediaType() {
return MEDIA_TYPE;
}
private void appendMessage(IndentedPrintWriter builder, PMessageOrBuilder<?> message, boolean encloseParams) {
PMessageDescriptor<?> type = message.descriptor();
boolean empty = true;
if (encloseParams) {
builder.append(kParamsStart);
} else {
builder.append(kMessageStart);
}
builder.begin();
if (message instanceof PUnion) {
if (((PUnion) message).unionFieldIsSet()) {
PField field = ((PUnion) message).unionField();
Object o = message.get(field.getId());
builder.appendln()
.append(field.getName())
.append(space)
.append(kFieldValueSep)
.append(space);
appendTypedValue(builder, field.getDescriptor(), o);
empty = false;
}
} else {
boolean first = true;
for (PField field : type.getFields()) {
if (message.has(field.getId())) {
if (first) {
first = false;
} else {
builder.append(entrySep);
}
Object o = message.get(field.getId());
builder.appendln()
.append(field.getName())
.append(space)
.append(kFieldValueSep)
.append(space);
appendTypedValue(builder, field.getDescriptor(), o);
empty = false;
}
}
}
builder.end();
if (!empty) {
builder.appendln();
}
if (encloseParams) {
builder.append(kParamsEnd);
} else {
builder.append(kMessageEnd);
}
}
private void appendTypedValue(IndentedPrintWriter writer, PDescriptor descriptor, Object o) {
switch (descriptor.getType()) {
case LIST:
case SET: {
PContainer<?> containerType = (PContainer<?>) descriptor;
PDescriptor itemType = containerType.itemDescriptor();
Collection<?> collection = (Collection<?>) o;
if (collection.isEmpty()) {
writer.append(kListStart)
.append(kListEnd);
break;
}
PPrimitive primitive = PPrimitive.findByName(itemType.getName());
if (primitive != null &&
primitive != PPrimitive.STRING &&
primitive != PPrimitive.BINARY &&
collection.size() <= 10) {
// special case if we have simple primitives (numbers and bools) in a "short" list,
// print in one single line.
writer.append(kListStart);
boolean first = true;
for (Object i : collection) {
if (first) {
first = false;
} else {
// Lists are always comma-delimited
writer.append(kListSep)
.append(space);
}
appendTypedValue(writer, containerType.itemDescriptor(), i);
}
writer.append(kListEnd);
} else {
writer.append(kListStart)
.begin();
boolean first = true;
for (Object i : collection) {
if (first) {
first = false;
} else {
// Lists are always comma-delimited
writer.append(kListSep);
}
writer.appendln();
appendTypedValue(writer, containerType.itemDescriptor(), i);
}
writer.end()
.appendln(kListEnd);
}
break;
}
case MAP: {
PMap<?, ?> mapType = (PMap<?, ?>) descriptor;
Map<?, ?> map = (Map<?, ?>) o;
if (map.isEmpty()) {
writer.append(kMessageStart)
.append(kMessageEnd);
break;
}
writer.append(kMessageStart)
.begin();
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (first) {
first = false;
} else {
writer.append(entrySep);
}
writer.appendln();
appendTypedValue(writer, mapType.keyDescriptor(), entry.getKey());
writer.append(kKeyValueSep)
.append(space);
appendTypedValue(writer, mapType.itemDescriptor(), entry.getValue());
}
writer.end()
.appendln(kMessageEnd);
break;
}
case VOID:
writer.print(true);
break;
case MESSAGE:
PMessage<?> message = (PMessage<?>) o;
appendMessage(writer, message, false);
break;
default:
appendPrimitive(writer, o);
break;
}
}
private void appendPrimitive(IndentedPrintWriter writer, Object o) {
if (o instanceof PEnumValue) {
writer.print(((PEnumValue) o).asString());
} else if (o instanceof CharSequence) {
writer.print(kLiteralDoubleQuote);
writer.print(Strings.escape((CharSequence) o));
writer.print(kLiteralDoubleQuote);
} else if (o instanceof Binary) {
Binary b = (Binary) o;
writer.append(PrettyToken.B64)
.append(kParamsStart)
.append(b.toBase64())
.append(kParamsEnd);
} else if (o instanceof Boolean) {
writer.print(((Boolean) o).booleanValue());
} else if (o instanceof Byte || o instanceof Short || o instanceof Integer || o instanceof Long) {
writer.print(o.toString());
} else if (o instanceof Double) {
Double d = (Double) o;
if (d.equals(((double) d.longValue()))) {
// actually an integer or long value.
writer.print(d.longValue());
} else {
writer.print(d.doubleValue());
}
} else {
throw new IllegalArgumentException("Unknown primitive type class " + o.getClass()
.getSimpleName());
}
}
private void consumeValue(PrettyLexer lexer, PrettyToken token) throws IOException {
if (token.isSymbol(kMessageStart)) {
// message or map.
token = lexer.expect("map or message first entry");
// ignore empty map or message.
if (!token.isSymbol(kMessageEnd)) {
// key = value: message
// key : value: map
// potential message.
boolean idKey = token.isIdentifier();
consumeValue(lexer, token);
if (lexer.expectSymbol("map", ':', '=').isSymbol(kFieldValueSep)) {
// message!
if (!idKey) {
throw lexer.failure(token, "Invalid field name token");
}
consumeValue(lexer, lexer.expect("message field value"));
token = nextNotLineSep(lexer);
while (!token.isSymbol(kMessageEnd)) {
if (!token.isIdentifier()) {
throw lexer.failure(token, "Invalid field name token");
}
lexer.expectSymbol("message field value sep", kFieldValueSep);
consumeValue(lexer, lexer.expect("message field value"));
token = nextNotLineSep(lexer);
}
} else {
// map!
consumeValue(lexer, lexer.expect("map entry value"));
token = nextNotLineSep(lexer);
while (!token.isSymbol(kMessageEnd)) {
consumeValue(lexer, token);
lexer.expectSymbol("message field value sep", kKeyValueSep);
consumeValue(lexer, lexer.expect("message field value"));
token = nextNotLineSep(lexer);
}
}
if (!token.isSymbol(kFieldValueSep)) {
// assume map.
while (!token.isSymbol(kMessageEnd)) {
consumeValue(lexer, token);
lexer.expectSymbol("key value sep.", kKeyValueSep);
consumeValue(lexer, lexer.expect("map value"));
// maps do *not* require separator, but allows ',' separator, and separator after last.
token = nextNotLineSep(lexer);
}
} else {
// assume message.
while (!token.isSymbol(kMessageEnd)) {
if (!token.isIdentifier()) {
throw new PrettyException(token, "Invalid field name: " + token)
.setLine(token.line());
}
lexer.expectSymbol("field value sep.", kFieldValueSep);
consumeValue(lexer, lexer.expect(""));
token = nextNotLineSep(lexer);
}
}
}
} else if (token.isSymbol(kListStart)) {
token = lexer.expect("");
while (!token.isSymbol(kListEnd)) {
consumeValue(lexer, token);
// lists and sets require list separator (,), and allows trailing separator.
if (lexer.expectSymbol("list separator or end", kListSep, kListEnd).isSymbol(kListEnd)) {
break;
}
token = lexer.expect("list value or end");
}
} else if (token.toString().equals(PrettyToken.HEX) ||
token.toString().equals(PrettyToken.B64)) {
lexer.expectSymbol("binary body start", kParamsStart);
lexer.readBinary(kParamsEnd);
} else if (!(token.isReal() || // number (double)
token.isInteger() || // number (int)
token.isStringLiteral() || // string literal
token.isIdentifier())) { // enum value reference.
throw new PrettyException(token, "Unknown value token '%s'", token)
.setLine(token.line());
}
}
private PrettyToken nextNotLineSep(PrettyLexer lexer) throws IOException {
if (lexer.peek("message field or end").isSymbol(kListSep)) {
lexer.next();
}
return lexer.expect("message field or end");
}
}