MessageUtil.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 com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.MessageOrBuilder;
import net.morimekta.proto.ProtoList;
import net.morimekta.proto.ProtoMap;
import java.util.List;
import java.util.Optional;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static net.morimekta.proto.utils.FieldUtil.fieldPathToFields;
import static net.morimekta.proto.utils.FieldUtil.getDefaultTypeValue;
import static net.morimekta.proto.utils.ValueUtil.toJavaValue;
/**
* Utility for handing the content of messages.
*/
public final class MessageUtil {
/**
* Look up a key in the message structure. If the key is not found, return the default value for that field, and
* iterate over the fields until the last one.
* <p>
* This differs form {@link #optionalInMessage(MessageOrBuilder, List)} by handling the fields'
* default values.
* <p>
* <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
* constant field enums, in that case you should use optional of the getter and map until you have the last value,
* which should always return the same, but is compile-time type safe. E.g.:
*
* <pre>{@code
* Optional.ofNullable(message.getFirst())
* .map(First::getSecond)
* .map(Second::getThird)
* .orElse(myDefault);
* }</pre>
*
* @param message The message to look up into.
* @param lookupPath Field to get in order.
* @param <T> The expected leaf value type.
* @return The value found or null.
* @throws IllegalArgumentException When unable to get value from message.
*/
public static <T> Optional<T> getInMessage(MessageOrBuilder message, String lookupPath) {
return getInMessage(message, fieldPathToFields(message.getDescriptorForType(), lookupPath));
}
/**
* Look up a key in the message structure. If the key is not found, return the default value for that field, and
* iterate over the fields until the last one.
* <p>
* This differs form {@link #optionalInMessage(MessageOrBuilder, List)} by handling the fields'
* default values.
* <p>
* <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
* constant field enums, in that case you should use optional of the getter and map until you have the last value,
* which should always return the same, but is compile-time type safe. E.g.:
*
* <pre>{@code
* Optional.ofNullable(message.getFirst())
* .map(First::getSecond)
* .map(Second::getThird)
* .orElse(myDefault);
* }</pre>
*
* @param message The message to look up into.
* @param fields Field to get in order.
* @param <T> The expected leaf value type.
* @return The value found or null.
* @throws IllegalArgumentException When unable to get value from message.
*/
@SuppressWarnings("unchecked")
public static <T> Optional<T> getInMessage(MessageOrBuilder message, List<FieldDescriptor> fields) {
requireNonNull(message, "message == null");
requireNonNull(fields, "fields == null");
if (fields.isEmpty()) {
throw new IllegalArgumentException("No fields arguments");
}
FieldDescriptor field = fields.get(0);
// must do this to circumvent false default values on V2 messages.
Object value = (field.hasDefaultValue() ||
(field.isRepeated() ? message.getRepeatedFieldCount(field) > 0 : message.hasField(field)))
? message.getField(field)
: null;
if (value == null) {
value = getDefaultTypeValue(field);
}
if (fields.size() > 1) {
if (field.getType() != FieldDescriptor.Type.MESSAGE) {
throw new IllegalArgumentException("Intermediate field '" + field.getFullName() + "' is not a message");
}
return getInMessage((MessageOrBuilder) value, fields.subList(1, fields.size()));
} else if (field.isRepeated()) {
if (field.isMapField()) {
return Optional.of((T) new ProtoMap<>(message, field));
} else {
return Optional.of((T) new ProtoList<>(message, field));
}
} else {
return ofNullable((T) toJavaValue(field, value));
}
}
/**
* Get a field value from a message with optional chaining. If the field is not set, or any message in the chain
* leading up to the last message is missing, it will return an empty optional, otherwise the leaf field value. Note
* that this will only check for MESSAGE type if the message is present and needs to be looked up in.
* <p>
* This differs from {@link #getInMessage(MessageOrBuilder, List)} by explicitly
* <b>NOT</b> handling fields' default values.
* <p>
* <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
* constant field enums, in that case you should use the optional getter and flatMap until you have the last value,
* which should always return the same, but is compile-time type safe. E.g.:
*
* <pre>{@code
* message.optionalFirst()
* .flatMap(First::optionalSecond)
* .flatMap(Second::optionalThird)
* .orElse(myDefault);
* }</pre>
*
* @param message The message to start looking up field values in.
* @param lookupPath Fields to look up in the message.
* @param <T> The expected leaf value type.
* @return Optional field value.
*/
public static <T> Optional<T> optionalInMessage(MessageOrBuilder message,
String lookupPath) {
return optionalInMessage(message, fieldPathToFields(message.getDescriptorForType(), lookupPath));
}
/**
* Get a field value from a message with optional chaining. If the field is not set, or any message in the chain
* leading up to the last message is missing, it will return an empty optional, otherwise the leaf field value. Note
* that this will only check for MESSAGE type if the message is present and needs to be looked up in.
* <p>
* This differs from {@link #getInMessage(MessageOrBuilder, List)} by explicitly
* <b>NOT</b> handling fields' default values.
* <p>
* <b>NOTE:</b> This method should <b>NOT</b> be used directly in code with
* constant field enums, in that case you should use the optional getter and flatMap until you have the last value,
* which should always return the same, but is compile-time type safe. E.g.:
*
* <pre>{@code
* message.optionalFirst()
* .flatMap(First::optionalSecond)
* .flatMap(Second::optionalThird)
* .orElse(myDefault);
* }</pre>
*
* @param message The message to start looking up field values in.
* @param fields Fields to look up in the message.
* @param <T> The expected leaf value type.
* @return Optional field value.
*/
@SuppressWarnings("unchecked")
public static <T> Optional<T> optionalInMessage(MessageOrBuilder message,
List<FieldDescriptor> fields) {
if (fields.size() == 0) {
throw new IllegalArgumentException("No fields arguments");
}
if (message == null) {
return Optional.empty();
}
FieldDescriptor field = fields.get(0);
if (fields.size() > 1) {
if (field.getType() != FieldDescriptor.Type.MESSAGE || field.isRepeated()) {
throw new IllegalArgumentException("Intermediate field '" + field.getFullName() + "' is not a message");
}
if (!message.hasField(field)) {
return Optional.empty();
}
return optionalInMessage((MessageOrBuilder) message.getField(field), fields.subList(1, fields.size()));
} else if (field.isRepeated()) {
if (message.getRepeatedFieldCount(field) == 0) {
return Optional.empty();
} else if (field.isMapField()) {
return Optional.of((T) new ProtoMap<>(message, field));
} else {
return Optional.of((T) new ProtoList<>(message, field));
}
} else if (!message.hasField(field)) {
return Optional.empty();
} else {
return Optional.ofNullable((T) toJavaValue(field, message.getField(field)));
}
}
}