DefinitionField.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.databind.model.impl;

import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
import gov.nist.secauto.metaschema.core.model.IAttributable;
import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.model.constraint.AssemblyConstraintSet;
import gov.nist.secauto.metaschema.core.model.constraint.IModelConstrained;
import gov.nist.secauto.metaschema.core.model.constraint.IValueConstrained;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.IBindingContext;
import gov.nist.secauto.metaschema.databind.io.BindingException;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
import gov.nist.secauto.metaschema.databind.model.IBoundModule;
import gov.nist.secauto.metaschema.databind.model.IBoundProperty;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundFieldValue;
import gov.nist.secauto.metaschema.databind.model.annotations.Ignore;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import nl.talsmasoftware.lazy4j.Lazy;

/**
 * Implements a Metaschema module global field definition bound to a Java class.
 */
@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class DefinitionField
    extends AbstractBoundDefinitionModelComplex<MetaschemaField>
    implements IBoundDefinitionModelFieldComplex {
  @NonNull
  private final FieldValue fieldValue;
  @Nullable
  private IBoundInstanceFlag jsonValueKeyFlagInstance;
  @NonNull
  private final Lazy<FlagContainerSupport> flagContainer;
  @NonNull
  private final Lazy<IValueConstrained> constraints;
  @NonNull
  private final Lazy<Map<String, IBoundProperty<?>>> jsonProperties;
  @NonNull
  private final Lazy<Map<IAttributable.Key, Set<String>>> properties;

  /**
   * Collect all fields that are part of the model for this class.
   *
   * @param clazz
   *          the class
   * @return the field value instances if found or {@code null} otherwise
   */
  @Nullable
  private static Field getFieldValueField(Class<?> clazz) {
    Field[] fields = clazz.getDeclaredFields();

    Field retval = null;
    for (Field field : fields) {
      if (!field.isAnnotationPresent(BoundFieldValue.class) || field.isAnnotationPresent(Ignore.class)) {
        // skip this field, since it is ignored
        continue;
      }
      retval = field;
    }

    if (retval == null) {
      Class<?> superClass = clazz.getSuperclass();
      if (superClass != null) {
        // get instances from superclass
        retval = getFieldValueField(superClass);
      }
    }
    return retval;
  }

  /**
   * Construct a new Metaschema module field definition.
   *
   * @param clazz
   *          the Java class the definition is bound to
   * @param annotation
   *          the binding annotation associated with this class
   * @param module
   *          the module containing this class
   * @param bindingContext
   *          the Metaschema binding context managing this class used to lookup
   *          binding information
   * @return the instance
   */
  @NonNull
  public static DefinitionField newInstance(
      @NonNull Class<? extends IBoundObject> clazz,
      @NonNull MetaschemaField annotation,
      @NonNull IBoundModule module,
      @NonNull IBindingContext bindingContext) {
    return new DefinitionField(clazz, annotation, module, bindingContext);
  }

  private DefinitionField(
      @NonNull Class<? extends IBoundObject> clazz,
      @NonNull MetaschemaField annotation,
      @NonNull IBoundModule module,
      @NonNull IBindingContext bindingContext) {
    super(clazz, annotation, module, bindingContext);
    Field field = getFieldValueField(getBoundClass());
    if (field == null) {
      throw new IllegalArgumentException(
          String.format("Class '%s' is missing the '%s' annotation on one of its fields.",
              clazz.getName(),
              BoundFieldValue.class.getName())); // NOPMD false positive
    }
    this.fieldValue = new FieldValue(field, BoundFieldValue.class, bindingContext);
    this.flagContainer = ObjectUtils.notNull(Lazy.lazy(() -> new FlagContainerSupport(this, this::handleFlagInstance)));
    this.constraints = ObjectUtils.notNull(Lazy.lazy(() -> {
      IModelConstrained retval = new AssemblyConstraintSet();
      ValueConstraints valueAnnotation = getAnnotation().valueConstraints();
      ConstraintSupport.parse(valueAnnotation, module.getSource(), retval);
      return retval;
    }));
    this.jsonProperties = ObjectUtils.notNull(Lazy.lazy(() -> {
      IBoundInstanceFlag jsonValueKey = getJsonValueKeyFlagInstance();
      Predicate<IBoundInstanceFlag> flagFilter = jsonValueKey == null ? null : flag -> !flag.equals(jsonValueKey);
      return getJsonProperties(flagFilter);
    }));
    this.properties = ObjectUtils.notNull(
        Lazy.lazy(() -> CollectionUtil.unmodifiableMap(ObjectUtils.notNull(
            Arrays.stream(annotation.properties())
                .map(ModelUtil::toPropertyEntry)
                .collect(
                    Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new))))));
  }

  /**
   * A callback used to identify the JSON value key flag.
   *
   * @param instance
   *          a flag instance
   */
  protected void handleFlagInstance(@NonNull IBoundInstanceFlag instance) {
    if (instance.isJsonValueKey()) {
      this.jsonValueKeyFlagInstance = instance;
    }
  }

  @Override
  @NonNull
  public FieldValue getFieldValue() {
    return fieldValue;
  }

  @Override
  public IBoundInstanceFlag getJsonValueKeyFlagInstance() {
    // lazy load flags
    getFlagContainer();
    return jsonValueKeyFlagInstance;
  }

  @Override
  protected void deepCopyItemInternal(IBoundObject fromObject, IBoundObject toObject) throws BindingException {
    // copy the flags
    super.deepCopyItemInternal(fromObject, toObject);

    getFieldValue().deepCopy(fromObject, toObject);
  }

  // ------------------------------------------
  // - Start annotation driven code - CPD-OFF -
  // ------------------------------------------

  @Override
  @SuppressWarnings("null")
  @NonNull
  public FlagContainerSupport getFlagContainer() {
    return flagContainer.get();
  }

  @Override
  @NonNull
  public IValueConstrained getConstraintSupport() {
    return ObjectUtils.notNull(constraints.get());
  }

  @Override
  public Map<String, IBoundProperty<?>> getJsonProperties() {
    return ObjectUtils.notNull(jsonProperties.get());
  }

  @Override
  @Nullable
  public String getFormalName() {
    return ModelUtil.resolveNoneOrValue(getAnnotation().formalName());
  }

  @Override
  @Nullable
  public MarkupLine getDescription() {
    return ModelUtil.resolveToMarkupLine(getAnnotation().description());
  }

  @Override
  @NonNull
  public String getName() {
    return getAnnotation().name();
  }

  @Override
  public Map<Key, Set<String>> getProperties() {
    return ObjectUtils.notNull(properties.get());
  }

  @Override
  @Nullable
  public Integer getIndex() {
    return ModelUtil.resolveDefaultInteger(getAnnotation().index());
  }

  @Override
  @Nullable
  public MarkupMultiline getRemarks() {
    return ModelUtil.resolveToMarkupMultiline(getAnnotation().description());
  }

  /**
   * Implements a field definition value bound to a Java field.
   */
  protected class FieldValue
      implements IBoundFieldValue {
    @NonNull
    private final Field javaField;
    @NonNull
    private final BoundFieldValue annotation;
    @NonNull
    private final IDataTypeAdapter<?> javaTypeAdapter;
    @Nullable
    private final Object defaultValue;

    /**
     * Construct a new field value binding.
     *
     * @param javaField
     *          the Java field the field value is bound to
     * @param annotationClass
     *          the field value binding annotation Java class
     * @param bindingContext
     *          the Metaschema binding context managing this class
     */
    protected FieldValue(
        @NonNull Field javaField,
        @NonNull Class<BoundFieldValue> annotationClass,
        @NonNull IBindingContext bindingContext) {
      this.javaField = javaField;
      this.annotation = ModelUtil.getAnnotation(javaField, annotationClass);
      this.javaTypeAdapter = ModelUtil.getDataTypeAdapter(
          this.annotation.typeAdapter(),
          bindingContext);
      this.defaultValue = ModelUtil.resolveDefaultValue(this.annotation.defaultValue(), this.javaTypeAdapter);
    }

    /**
     * Get the bound Java field.
     *
     * @return the bound Java field
     */
    @Override
    @NonNull
    public Field getField() {
      return javaField;
    }

    /**
     * Get the binding Java annotation.
     *
     * @return the binding Java annotation
     */
    @NonNull
    public BoundFieldValue getAnnotation() {
      return annotation;
    }

    @Override
    public IBoundDefinitionModelFieldComplex getParentFieldDefinition() {
      return DefinitionField.this;
    }

    @Override
    public String getJsonValueKeyName() {
      String name = ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
      return name == null ? getJavaTypeAdapter().getDefaultJsonValueKey() : name;
    }

    @Override
    public String getJsonValueKeyFlagName() {
      return ModelUtil.resolveNoneOrValue(getAnnotation().valueKeyName());
    }

    @Override
    public Object getDefaultValue() {
      return defaultValue;
    }

    @Override
    public IDataTypeAdapter<?> getJavaTypeAdapter() {
      return javaTypeAdapter;
    }

    @Override
    public Object getEffectiveDefaultValue() {
      return getDefaultValue();
    }

    @Override
    public String getJsonName() {
      return getEffectiveJsonValueKeyName();
    }
  }
  // ----------------------------------------
  // - End annotation driven code - CPD-OFF -
  // ----------------------------------------
}