ProtoMessageOrBuilder.java

/*
 * Copyright 2022 Proto Utils 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;

import com.google.protobuf.Descriptors;
import com.google.protobuf.MessageOrBuilder;
import net.morimekta.proto.utils.ValueUtil;
import net.morimekta.strings.Stringable;

import java.util.Objects;
import java.util.Optional;

import static java.util.Objects.requireNonNull;
import static net.morimekta.proto.utils.FieldUtil.getDefaultFieldValue;
import static net.morimekta.proto.utils.ValueUtil.toJavaValue;

/**
 * Base class for proto message or its builder.
 */
public abstract class ProtoMessageOrBuilder
        implements Stringable {
    private final MessageOrBuilder       messageOrBuilder;
    private final Descriptors.Descriptor descriptor;

    /**
     * @param messageOrBuilder The message or builder to wrap.
     */
    public ProtoMessageOrBuilder(MessageOrBuilder messageOrBuilder) {
        this.messageOrBuilder = requireNonNull(messageOrBuilder, "message == null");
        this.descriptor = messageOrBuilder.getDescriptorForType();
    }

    /**
     * @return The wrapped message or builder.
     */
    public MessageOrBuilder getMessage() {
        return messageOrBuilder;
    }

    /**
     * @return The wrapped message
     */
    public Descriptors.Descriptor getDescriptor() {
        return descriptor;
    }

    /**
     * @param field The field descriptor.
     * @return If the message has the field set.
     */
    public boolean has(Descriptors.FieldDescriptor field) {
        requireNonNull(field, "field == null");
        return hasInternal(field);
    }

    /**
     * Get the field value as if calling the <code>getMyField()</code> method, or
     * the <code>getMyFieldList()</code> method for repeated fields. It will return
     * a non-null value if at all possible, only exception is for non-set proto2
     * enums without a 0 value.
     *
     * @param field The field descriptor.
     * @param <T>   The field value type.
     * @return The field value.
     */
    public abstract <T> T get(Descriptors.FieldDescriptor field);

    /**
     * Get the value of a field if present, and an empty optional if it is not. This
     * should be using the message's field presence. Note that the presence calculation
     * may be different depending on the proto syntax version and various optionality
     * options set on the field.
     * <p>
     * The optional can be used to see if a value should be serialized or not. If the
     * optional is empty the {@link #has(Descriptors.FieldDescriptor)} method should
     * also return false for the same field.
     *
     * @param field The field descriptor.
     * @param <T>   The field value type.
     * @return Optional field value.
     */
    public abstract <T> Optional<T> optional(Descriptors.FieldDescriptor field);

    // --- Stringable ---

    @Override
    public String asString() {
        return ValueUtil.asString(messageOrBuilder);
    }

    // --- Object ---

    @Override
    public String toString() {
        return getDescriptor().getFullName() + asString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ProtoMessageOrBuilder that = (ProtoMessageOrBuilder) o;
        return Objects.equals(descriptor, that.descriptor) &&
               Objects.equals(messageOrBuilder, that.messageOrBuilder);
    }

    @Override
    public int hashCode() {
        return Objects.hash(messageOrBuilder, descriptor);
    }

    // --- Protected ---

    /**
     * @param field The field descriptor.
     * @return If the field is present on the message.
     */
    protected boolean hasInternal(Descriptors.FieldDescriptor field) {
        if (field.isRepeated()) {
            return messageOrBuilder.getRepeatedFieldCount(field) > 0;
        } else {
            return messageOrBuilder.hasField(field);
        }
    }

    /**
     * @param field The field descriptor.
     * @param <T>   The field value type.
     * @return The field value.
     */
    @SuppressWarnings("unchecked")
    protected <T> T getInternal(Descriptors.FieldDescriptor field) {
        return (T) optionalInternal(field)
                .orElseGet(() -> getDefaultFieldValue(field));
    }

    /**
     * @param field The field descriptor.
     * @param <T>   The field value type.
     * @return The optional value for the field.
     */
    @SuppressWarnings("unchecked")
    protected <T> Optional<T> optionalInternal(Descriptors.FieldDescriptor field) {
        if (hasInternal(field)) {
            return Optional.ofNullable((T) toJavaValue(field, messageOrBuilder.getField(field)));
        }
        return Optional.empty();
    }
}