LogFormatter.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.logging;
import net.morimekta.providence.PEnumValue;
import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageOrBuilder;
import net.morimekta.providence.PType;
import net.morimekta.providence.PUnion;
import net.morimekta.providence.descriptor.PContainer;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PMap;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PPrimitive;
import net.morimekta.providence.serializer.pretty.PrettyToken;
import net.morimekta.util.Binary;
import net.morimekta.util.Strings;
import net.morimekta.util.collect.UnmodifiableList;
import net.morimekta.util.io.IndentedPrintWriter;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* LogFormatter is a formatter (one-way serialization) similar to the PrettySerializer,
* except it supports adding FieldHandlers to modify in.
*
* Note that the LogFormatter is <b>not</b> a serializer, as there is no guarantee the
* result can be parsed back at all.
*/
public class LogFormatter {
/**
* Handler for a single field in a message. If it returns true, will consume the field.
* The visible (printed) value must be written to the IndentedPrintWriter.
*/
@FunctionalInterface
public interface FieldHandler {
boolean appendFieldValue(IndentedPrintWriter writer, PField field, Object value);
}
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 List<FieldHandler> fieldHandlers;
/**
* Create a log formatter with compact format.
*
* @param fieldHandlers Field handlers to specify formatted values of specific fields.
*/
public LogFormatter(FieldHandler... fieldHandlers) {
this(false, UnmodifiableList.copyOf(fieldHandlers));
}
/**
* Create a log formatter.
*
* @param pretty If true will add lines, line indentation and extra spaces.
* @param fieldHandlers Field handlers to specify formatted values of specific fields.
*/
public LogFormatter(boolean pretty, FieldHandler... fieldHandlers) {
this(pretty, UnmodifiableList.copyOf(fieldHandlers));
}
/**
* Create a log formatter.
*
* @param pretty If true will add lines, line indentation and extra spaces.
* @param fieldHandlers Field handlers to specify formatted values of specific fields.
*/
public LogFormatter(boolean pretty, Collection<FieldHandler> fieldHandlers) {
this.indent = pretty ? INDENT : "";
this.space = pretty ? SPACE : "";
this.newline = pretty ? NEWLINE : "";
this.entrySep = pretty ? "" : LIST_SEP;
this.fieldHandlers = UnmodifiableList.copyOf(fieldHandlers);
}
/**
* Format message and write to the output stream.
*
* @param out The output stream to write to.
* @param message The message to be written.
* @param <Message> The message type.
*/
public <Message extends PMessage<Message>>
void formatTo(OutputStream out, PMessageOrBuilder<Message> message) {
IndentedPrintWriter builder = new IndentedPrintWriter(out, indent, newline);
if (message == null) {
builder.append(null);
} else {
builder.append(message.descriptor().getQualifiedName())
.append(space);
appendMessage(builder, message);
}
builder.flush();
}
/**
* Format message to a string.
*
* @param message The message to be written.
* @param <Message> The message type.
* @return The formatted message.
*/
public <Message extends PMessage<Message>>
String format(PMessageOrBuilder<Message> message) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
formatTo(out, message);
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
private void appendMessage(IndentedPrintWriter writer, PMessageOrBuilder<?> message) {
PMessageDescriptor<?> type = message.descriptor();
writer.append(PrettyToken.kMessageStart)
.begin();
if (message instanceof PUnion) {
if (((PUnion) message).unionFieldIsSet()) {
PField field = ((PUnion) message).unionField();
Object o = message.get(field.getId());
writer.appendln()
.append(field.getName())
.append(space)
.append(PrettyToken.kFieldValueSep)
.append(space);
appendFieldValue(writer, field, o);
}
} else {
boolean first = true;
for (PField field : type.getFields()) {
if (message.has(field.getId())) {
if (first) {
first = false;
} else {
writer.append(entrySep);
}
Object o = message.get(field.getId());
writer.appendln()
.append(field.getName())
.append(space)
.append(PrettyToken.kFieldValueSep)
.append(space);
appendFieldValue(writer, field, o);
}
}
}
writer.end()
.appendln(PrettyToken.kMessageEnd);
}
private void appendFieldValue(IndentedPrintWriter writer, PField field, Object value) {
if (field.getType() != PType.MESSAGE) {
for (FieldHandler handler : fieldHandlers) {
if (handler.appendFieldValue(writer, field, value)) {
return;
}
}
}
appendTypedValue(writer, field.getDescriptor(), value);
}
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;
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(PrettyToken.kListStart);
boolean first = true;
for (Object i : collection) {
if (first) {
first = false;
} else {
// Lists are always comma-delimited
writer.append(PrettyToken.kListSep)
.append(space);
}
appendTypedValue(writer, containerType.itemDescriptor(), i);
}
writer.append(PrettyToken.kListEnd);
} else {
writer.append(PrettyToken.kListStart)
.begin();
boolean first = true;
for (Object i : collection) {
if (first) {
first = false;
} else {
// Lists are always comma-delimited
writer.append(PrettyToken.kListSep);
}
writer.appendln();
appendTypedValue(writer, containerType.itemDescriptor(), i);
}
writer.end()
.appendln(PrettyToken.kListEnd);
}
break;
}
case MAP: {
PMap<?, ?> mapType = (PMap<?, ?>) descriptor;
Map<?, ?> map = (Map<?, ?>) o;
writer.append(PrettyToken.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(PrettyToken.kKeyValueSep)
.append(space);
appendTypedValue(writer, mapType.itemDescriptor(), entry.getValue());
}
writer.end()
.appendln(PrettyToken.kMessageEnd);
break;
}
case VOID:
writer.print(true);
break;
case MESSAGE:
PMessage<?> message = (PMessage<?>) o;
appendMessage(writer, message);
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(PrettyToken.kLiteralDoubleQuote);
writer.print(Strings.escape((CharSequence) o));
writer.print(PrettyToken.kLiteralDoubleQuote);
} else if (o instanceof Binary) {
Binary b = (Binary) o;
writer.append(PrettyToken.B64)
.append(PrettyToken.kParamsStart)
.append(b.toBase64())
.append(PrettyToken.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());
}
}
}