CField.java

/*
 * Copyright 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.reflect.contained;

import net.morimekta.providence.PMessage;
import net.morimekta.providence.descriptor.PDescriptor;
import net.morimekta.providence.descriptor.PDescriptorProvider;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PInterfaceDescriptor;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.providence.descriptor.PRequirement;
import net.morimekta.providence.descriptor.PStructDescriptor;
import net.morimekta.providence.descriptor.PStructDescriptorProvider;
import net.morimekta.providence.descriptor.PValueProvider;
import net.morimekta.util.Strings;
import net.morimekta.util.collect.UnmodifiableMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static net.morimekta.providence.descriptor.PAnnotation.JSON_NAME;

/**
 * Description of a single contained field. Part of the message descriptor.
 *
 * @param <M> The message type.
 */
public class CField<M extends PMessage<M>> implements PField<M>, CAnnotatedDescriptor {
    public static final CField<?>[] EMPTY_ARRAY = new CField[0];

    private final String                       comment;
    private final int                          id;
    private final PRequirement                 requirement;
    private final PDescriptorProvider          typeProvider;
    private final String                       name;
    private final String                       pojoName;
    private final PValueProvider<?>            defaultValue;
    private final Map<String, String>          annotations;
    private final PStructDescriptorProvider<?> argumentsProvider;
    private final PDescriptorProvider          implementing;

    private PMessageDescriptor<M> messageType;

    public CField(@Nullable String docs,
                  int id,
                  @Nonnull PRequirement requirement,
                  @Nonnull String name,
                  @Nonnull PDescriptorProvider typeProvider,
                  @Nullable PStructDescriptorProvider<?> argumentsProvider,
                  @Nullable PValueProvider<?> defaultValue,
                  @Nullable Map<String, String> annotations,
                  @Nullable PDescriptorProvider implementing) {
        this.comment = docs;
        this.id = id;
        this.requirement = requirement;
        this.typeProvider = typeProvider;
        this.argumentsProvider = argumentsProvider;
        this.name = name;
        this.defaultValue = defaultValue;
        this.annotations = annotations == null ? UnmodifiableMap.mapOf() : UnmodifiableMap.copyOf(annotations);
        this.implementing = implementing;

        String tmp = Strings.camelCase(name);
        tmp = tmp.substring(0, 1).toLowerCase(Locale.US) + tmp.substring(1);
        if (this.annotations.containsKey(JSON_NAME.tag)) {
            tmp = this.annotations.get(JSON_NAME.tag);
        }
        this.pojoName = tmp;
    }

    @Override
    public String getDocumentation() {
        return comment;
    }

    @Override
    public int getId() {
        return id;
    }

    @Nonnull
    @Override
    public PRequirement getRequirement() {
        return requirement;
    }

    @Nonnull
    @Override
    public PDescriptor getDescriptor() {
        return typeProvider.descriptor();
    }

    @Override
    public PStructDescriptor<?> getArgumentsType() {
        return argumentsProvider == null ? argumentsFromInterface() : argumentsProvider.descriptor();
    }

    @Nonnull
    @Override
    public String getName() {
        return name;
    }

    @Nonnull
    @Override
    public String getPojoName() {
        return pojoName;
    }

    @Override
    public boolean hasDefaultValue() {
        return defaultValue != null || hasDefaultFromInterface();
    }

    @Override
    public Object getDefaultValue() {
        try {
            return defaultValue != null ? defaultValue.get() : defaultFromInterfaceOrType();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalStateException("Unable to parse default value " + getName(), e);
        }
    }

    @Nonnull
    @Override
    public Set<String> getAnnotations() {
        if (implementing != null) {
            Set<String> union = new HashSet<>(annotations.keySet());
            @SuppressWarnings("unchecked")
            CField<M> field = (CField<M>) getImplementing().findFieldByName(getName());
            if (field != null) {
                union.addAll(field.getAnnotations());
            }
            return union;
        }
        return annotations.keySet();
    }

    @Override
    public boolean hasAnnotation(@Nonnull String name) {
        if (annotations.containsKey(name)) return true;
        if (implementing != null) {
            CField<?> field = (CField<?>) getImplementing().findFieldByName(getName());
            if (field != null) {
                return field.hasAnnotation(name);
            }
        }
        return false;
    }

    @Override
    public String getAnnotationValue(@Nonnull String name) {
        if (annotations.containsKey(name)) {
            return annotations.get(name);
        } else if (implementing != null) {
            CField<?> field = (CField<?>) getImplementing().findFieldByName(getName());
            if (field != null) {
                return field.getAnnotationValue(name);
            }
        }
        return null;
    }

    @Nonnull
    @Override
    public PMessageDescriptor<M> onMessageType() {
        return Objects.requireNonNull(messageType, "Not set message type of CField");
    }

    void setMessageType(@Nonnull PMessageDescriptor<M> descriptor) {
        this.messageType = descriptor;
    }

    @Override
    public String toString() {
        return PField.asString(this);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof CField)) {
            return false;
        }
        CField<?> other = (CField<?>) o;
        return id == other.id &&
               requirement == other.requirement &&
               // We cannot test that the types are deep-equals as it may have circular
               // containment.
               equalsQualifiedName(getDescriptor(), other.getDescriptor()) &&
               name.equals(other.name) &&
               Objects.equals(defaultValue, other.defaultValue);
    }

    @Override
    public int hashCode() {
        return Objects.hash(CField.class, id, requirement, name, getDefaultValue());
    }

    private PInterfaceDescriptor<?> getImplementing() {
        return (PInterfaceDescriptor<?>) implementing.descriptor();
    }

    private boolean hasDefaultFromInterface() {
        if (implementing == null) return false;
        PField<?> ifField = getImplementing().findFieldByName(getName());
        if (ifField == null) return false;
        return ifField.hasDefaultValue();
    }

    private Object defaultFromInterfaceOrType() {
        if (implementing == null) return getDescriptor().getDefaultValue();
        PField ifField = getImplementing().findFieldByName(getName());
        if (ifField == null) return getDescriptor().getDefaultValue();
        return ifField.getDefaultValue();
    }

    private PStructDescriptor argumentsFromInterface() {
        if (implementing == null) return null;
        PField ifField = getImplementing().findFieldByName(getName());
        if (ifField == null) return null;
        return ifField.getArgumentsType();
    }

    /**
     * Check if the two descriptors has the same qualified name, i..e
     * symbolically represent the same type.
     *
     * @param a The first type.
     * @param b The second type.
     * @return If the two types are the same.
     */
    private static boolean equalsQualifiedName(PDescriptor a, PDescriptor b) {
        return (a != null) && (b != null) && (a.getQualifiedName()
                                               .equals(b.getQualifiedName()));
    }
}