ValueUtil.java
/*
* Copyright 2020 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.proto.utils;
import static java.util.Objects.requireNonNull;
import static net.morimekta.collect.UnmodifiableList.listOf;
import static net.morimekta.proto.ProtoEnum.getEnumDescriptor;
import static net.morimekta.proto.utils.ByteStringUtil.toBase64;
import static net.morimekta.proto.utils.FieldUtil.getDefaultTypeValue;
import static net.morimekta.proto.utils.FieldUtil.getMapKeyDescriptor;
import static net.morimekta.proto.utils.FieldUtil.getMapValueDescriptor;
import static net.morimekta.strings.EscapeUtil.javaEscape;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import com.google.protobuf.MapEntry;
import com.google.protobuf.Message;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.ProtocolMessageEnum;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import net.morimekta.collect.UnmodifiableList;
import net.morimekta.collect.UnmodifiableMap;
import net.morimekta.proto.ProtoMessage;
import net.morimekta.strings.Stringable;
/**
* Most of these are pretty trivial methods, but here to vastly simplify generated code when making builder, mutable
* values or build a message.
*/
public final class ValueUtil {
/**
* @param message The message to check.
* @return Ture if the message it it's default instance.
*/
public static boolean isDefault(Message message) {
requireNonNull(message, "message == null");
return message.equals(message.getDefaultInstanceForType());
}
/**
* @param message The message to check.
* @return Ture if the message it it's default instance.
*/
public static boolean isNullOrDefault(Message message) {
return message == null || isDefault(message);
}
/**
* @param message The message to check.
* @return True if the message is not it's default instance.
*/
public static boolean isNotDefault(Message message) {
return !isNullOrDefault(message);
}
/**
* @param message Message to check.
* @param <T> The message type.
* @return The message, or null if the message it it's own default value.
*/
public static <T extends Message> T defaultToNull(T message) {
if (isNullOrDefault(message)) {
return null;
}
return message;
}
// --- Conversion ---
/**
* Convert a proto value to a java value. The input is expected to be the
* result of a call to {@link Message#getField(Descriptors.FieldDescriptor)}.
* The result of the method should be the equivalent to <code>MessageType.getMyField()</code>
* or it's list variant for repeated fields.
*
* @param field The proto field describing the value.
* @param value The proto value.
* @return The java value.
* @see ValueUtil#toProtoValue(Descriptors.FieldDescriptor, Object) for the opposite conversion.
*/
public static Object toJavaValue(Descriptors.FieldDescriptor field, Object value) {
if (value == null) {
return null;
} else if (field.isRepeated() && value instanceof Collection) {
if (field.isMapField()) {
var keyType = getMapKeyDescriptor(field);
var valueType = getMapValueDescriptor(field);
@SuppressWarnings("unchecked")
var list = (List<MapEntry<?, ?>>) value;
var builder = UnmodifiableMap.newBuilder(list.size());
list.forEach(o -> builder.put(
toJavaValue(keyType, o.getKey()),
toJavaValue(valueType, o.getValue())));
return builder.build();
} else {
var list = (List<?>) value;
var builder = UnmodifiableList.newBuilder(list.size());
list.forEach(item -> builder.add(toJavaValue(field, item)));
return builder.build();
}
} else if (field.getType() == Descriptors.FieldDescriptor.Type.ENUM) {
if (value instanceof Descriptors.EnumValueDescriptor) {
var desc = (Descriptors.EnumValueDescriptor) value;
return getEnumDescriptor(desc.getType()).valueFor(desc);
} else if (value instanceof Integer) {
// TODO: Fail if no value?
return getEnumDescriptor(field.getEnumType()).findByNumber((Integer) value);
}
}
return value;
}
/**
* Convert a java value to a proto value. The input is expected to be the
* result of a call to <code>MessageType.getMyField()</code> or it's list
* variant for repeated fields. The output of the method should be the value
* compatible with the value param when calling
* {@link Message.Builder#setField(Descriptors.FieldDescriptor, Object)}.
*
* @param field The proto field describing the value.
* @param value The java value.
* @return The proto value.
* @see ValueUtil#toJavaValue(Descriptors.FieldDescriptor, Object) for the opposite conversion.
*/
public static Object toProtoValue(Descriptors.FieldDescriptor field, Object value) {
if (value == null) {
return null;
} else if (value instanceof Map) {
var map = (Map<?, ?>) value;
if (map.isEmpty()) {
return listOf();
}
var keyType = getMapKeyDescriptor(field);
var valueType = getMapValueDescriptor(field);
var defaultEntry = MapEntry.newDefaultInstance(field.getMessageType(),
keyType.getLiteType(),
getDefaultTypeValue(keyType),
valueType.getLiteType(),
getDefaultTypeValue(valueType));
var builder = UnmodifiableList.newBuilder();
map.forEach((k, v) -> builder.add(defaultEntry.toBuilder()
.setKey(toProtoValue(keyType, k))
.setValue(toProtoValue(valueType, v))
.build()));
return builder.build();
} else if (value instanceof Collection) {
var list = (Collection<?>) value;
var builder = UnmodifiableList.newBuilder(list.size());
list.forEach(i -> builder.add(toProtoValue(field, i)));
return builder.build();
} else if (value instanceof ProtocolMessageEnum) {
return ((ProtocolMessageEnum) value).getValueDescriptor();
}
return value;
}
/**
* Get a java like <code>toString()</code> value, but that is meant to be used in
* displaying the value in a safe manner. This means strings are quoted and escaped,
* optionals are unwrapped etc.
* <p>
* Messages are also formatted differently from the default {@link Message#toString()},
* where it follows the list in {@link #toDebugString(Message)}, but without newlines,
* indent, and much fewer spaces.
*
* @param o The value to stringify.
* @return The string value.
* @see #toDebugString(Message) for main differences with {@link Message#toString()}.
*/
public static String asString(Object o) {
if (o == null) {
return "null";
}
if (o instanceof Optional) {
return ((Optional<?>) o).map(ValueUtil::asString).orElse("null");
}
if (o instanceof Message) {
var message = (Message) o;
var builder = new StringBuilder("{");
for (var field : message.getAllFields().entrySet()) {
if (builder.length() > 1) {
builder.append(", ");
}
if (field.getKey().isExtension()) {
builder.append('(')
.append(field.getKey().getFullName())
.append(')');
} else {
builder.append(field.getKey().getName());
}
builder.append("=");
var value = toJavaValue(field.getKey(), field.getValue());
if (value instanceof MessageOrBuilder) {
builder.append(new ProtoMessage((MessageOrBuilder) value).asString());
} else {
builder.append(ValueUtil.asString(value));
}
}
builder.append('}');
return builder.toString();
}
if (o instanceof ProtocolMessageEnum) {
return ((ProtocolMessageEnum) o).getValueDescriptor().getName();
}
if (o instanceof CharSequence) {
return "\"" + javaEscape((CharSequence) o) + "\"";
}
if (o instanceof ByteString) {
return toBase64((ByteString) o);
}
if (o instanceof Map) {
Map<?, ?> map = (Map<?, ?>) o;
AtomicBoolean val = new AtomicBoolean();
StringBuilder builder = new StringBuilder();
builder.append('{');
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (val.getAndSet(true)) {
builder.append(", ");
}
builder.append(asString(entry.getKey()))
.append(": ")
.append(asString(entry.getValue()));
}
builder.append('}');
return builder.toString();
}
if (o instanceof Collection) {
Collection<?> collection = (Collection<?>) o;
AtomicBoolean val = new AtomicBoolean();
StringBuilder builder = new StringBuilder();
builder.append('[');
for (Object item : collection) {
if (val.getAndSet(true)) {
builder.append(", ");
}
builder.append(asString(item));
}
builder.append(']');
return builder.toString();
}
return Stringable.asString(o);
}
/**
* Make an easily readable debug string from the message. This differs from the
* standard text format of proto with:
*
* <ul>
* <li>
* The entire message is encapsulated with '{...}' and named to show which message type
* is shown.
* </li>
* <li>
* Field / Value separator for messages is '=', not ':' (':' is used for maps).
* </li>
* <li>
* <code>bytes</code> fields are encoded with base64 end encapsulated with
* "b64(...)".
* </li>
* <li>
* Lists are encoded as lists, not as repeated fields. This is much easier to read.
* </li>
* <li>
* Short lists of non-message, non-strings are written on a single line. Max 5 elements.
* </li>
* <li>
* Maps are encoded as maps, not as repeated messages. This is much easier to read.
* </li>
* <li>
* Extension names are encapsulated in '(...)', not '[...]', Which looks more similar
* to how custom extensions are named when setting them in .proto files.
* </li>
* </ul>
*
* @param message The message to get debug string from.
* @return The debug string.
*/
public static String toDebugString(Message message) {
StringBuilder builder = new StringBuilder();
builder.append(message.getDescriptorForType().getFullName());
appendDebugString("", message, builder);
return builder.toString();
}
// --- Private ---
private static void appendDebugString(String prefix, Message message, StringBuilder builder) {
var all = message.getAllFields();
if (all.isEmpty()) {
builder.append("{}");
return;
}
builder.append("{\n");
for (var field : message.getAllFields().entrySet()) {
builder.append(prefix)
.append(" ");
if (field.getKey().isExtension()) {
builder.append("(").append(field.getKey().getFullName()).append(")");
} else {
builder.append(field.getKey().getName());
}
builder.append(" = ");
var value = toJavaValue(field.getKey(), field.getValue());
appendDebugValue(prefix + " ", value, builder);
builder.append("\n");
}
builder.append(prefix).append("}");
}
private static void appendDebugValue(String prefix, Object value, StringBuilder builder) {
if (value instanceof Message) {
appendDebugString(prefix, (Message) value, builder);
} else if (value instanceof Map) {
var map = (Map<?, ?>) value;
builder.append("{");
boolean first = true;
for (var entry : map.entrySet()) {
if (first) {
first = false;
} else {
builder.append(",");
}
builder.append("\n")
.append(prefix)
.append(" ")
.append(asString(entry.getKey()))
.append(": ");
appendDebugValue(prefix + " ", entry.getValue(), builder);
}
builder.append("\n")
.append(prefix).append("}");
} else if (value instanceof List) {
var list = (List<?>) value;
var first = list.get(0);
if (list.size() > 5 || first instanceof Message || first instanceof ByteString || first instanceof String) {
builder.append("[");
for (var item : list) {
if (first != null) {
first = null;
} else {
builder.append(",");
}
builder.append("\n")
.append(prefix).append(" ");
appendDebugValue(prefix + " ", item, builder);
}
builder.append("\n")
.append(prefix).append("]");
} else {
builder.append("[");
for (var item : list) {
if (first != null) {
first = null;
} else {
builder.append(", ");
}
builder.append(asString(item));
}
builder.append("]");
}
} else if (value instanceof ByteString) {
builder.append("b64(")
.append(ByteStringUtil.toBase64((ByteString) value))
.append(")");
} else {
builder.append(asString(value));
}
}
private ValueUtil() {}
}