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)));
        }
    }
}