InstanceModelChoiceGroup.java

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

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

import gov.nist.secauto.metaschema.core.model.AbstractChoiceGroupInstance;
import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.model.IContainerModelSupport;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.CustomCollectors;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
import gov.nist.secauto.metaschema.databind.model.IBoundModule;
import gov.nist.secauto.metaschema.databind.model.IGroupAs;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundChoiceGroup;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundGroupedAssembly;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundGroupedField;
import gov.nist.secauto.metaschema.databind.model.annotations.GroupAs;
import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.namespace.QName;

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

/**
 * Implements a Metaschema module choice group instance bound to a Java field.
 */
@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class InstanceModelChoiceGroup
    extends AbstractChoiceGroupInstance<
        IBoundDefinitionModelAssembly,
        IBoundInstanceModelGroupedNamed,
        IBoundInstanceModelGroupedField,
        IBoundInstanceModelGroupedAssembly>
    implements IBoundInstanceModelChoiceGroup,
    IFeatureBoundContainerModelChoiceGroup, IFeatureInstanceModelGroupAs<IBoundObject> {
  @NonNull
  private final Field javaField;
  @NonNull
  private final BoundChoiceGroup annotation;
  @NonNull
  private final Lazy<IModelInstanceCollectionInfo<IBoundObject>> collectionInfo;
  @NonNull
  private final IGroupAs groupAs;
  @NonNull
  private final Lazy<ChoiceGroupModelContainerSupport> modelContainer;
  @NonNull
  private final Lazy<Map<Class<?>, IBoundInstanceModelGroupedNamed>> classToInstanceMap;
  @NonNull
  private final Lazy<Map<QName, IBoundInstanceModelGroupedNamed>> qnameToInstanceMap;
  @NonNull
  private final Lazy<Map<String, IBoundInstanceModelGroupedNamed>> discriminatorToInstanceMap;

  /**
   * Construct a new Metaschema module choice group instance.
   *
   * @param javaField
   *          the Java field bound to this instance
   * @param parent
   *          the definition containing this instance
   * @return the instance
   */
  @NonNull
  public static InstanceModelChoiceGroup newInstance(
      @NonNull Field javaField,
      @NonNull IBoundDefinitionModelAssembly parent) {
    BoundChoiceGroup annotation = ModelUtil.getAnnotation(javaField, BoundChoiceGroup.class);
    IGroupAs groupAs = ModelUtil.resolveDefaultGroupAs(annotation.groupAs(), parent.getContainingModule());
    if (annotation.maxOccurs() == -1 || annotation.maxOccurs() > 1) {
      if (IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
        throw new IllegalStateException(String.format("Field '%s' on class '%s' is missing the '%s' annotation.",
            javaField.getName(),
            parent.getBoundClass().getName(),
            GroupAs.class.getName()));
      }
    } else if (!IGroupAs.SINGLETON_GROUP_AS.equals(groupAs)) {
      // max is 1 and a groupAs is set
      throw new IllegalStateException(
          String.format(
              "Field '%s' on class '%s' has the '%s' annotation, but maxOccurs=1. A groupAs must not be specfied.",
              javaField.getName(),
              parent.getBoundClass().getName(),
              GroupAs.class.getName()));
    }
    return new InstanceModelChoiceGroup(javaField, annotation, groupAs, parent);
  }

  @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
  private InstanceModelChoiceGroup(
      @NonNull Field javaField,
      @NonNull BoundChoiceGroup annotation,
      @NonNull IGroupAs groupAs,
      @NonNull IBoundDefinitionModelAssembly parent) {
    super(parent);
    this.javaField = javaField;
    this.annotation = annotation;
    this.groupAs = groupAs;
    this.collectionInfo = ObjectUtils.notNull(Lazy.lazy(() -> IModelInstanceCollectionInfo.of(this)));
    this.modelContainer = ObjectUtils.notNull(Lazy.lazy(() -> new ChoiceGroupModelContainerSupport(
        this.annotation.assemblies(),
        this.annotation.fields(),
        this)));
    this.classToInstanceMap = ObjectUtils.notNull(Lazy.lazy(() -> Collections.unmodifiableMap(
        getNamedModelInstances().stream()
            .map(instance -> instance)
            .collect(Collectors.toMap(
                item -> item.getDefinition().getBoundClass(),
                CustomCollectors.identity())))));
    this.qnameToInstanceMap = ObjectUtils.notNull(Lazy.lazy(() -> Collections.unmodifiableMap(
        getNamedModelInstances().stream()
            .collect(Collectors.toMap(
                IBoundInstanceModelGroupedNamed::getXmlQName,
                CustomCollectors.identity())))));
    this.discriminatorToInstanceMap = ObjectUtils.notNull(Lazy.lazy(() -> Collections.unmodifiableMap(
        getNamedModelInstances().stream()
            .collect(Collectors.toMap(
                IBoundInstanceModelGroupedNamed::getEffectiveDisciminatorValue,
                CustomCollectors.identity())))));
  }

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

  @Override
  public Field getField() {
    return javaField;
  }

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

  @SuppressWarnings("null")
  @Override
  public IModelInstanceCollectionInfo<IBoundObject> getCollectionInfo() {
    return collectionInfo.get();
  }

  /**
   * Get the mapping of XML qualified names bound to a distinct grouped model
   * instance.
   *
   * @return the mapping
   */
  @SuppressWarnings("null")
  @NonNull
  protected Map<QName, IBoundInstanceModelGroupedNamed> getQNameToInstanceMap() {
    return qnameToInstanceMap.get();
  }

  /**
   * Get the mapping of Java classes bound to a distinct grouped model instance.
   *
   * @return the mapping
   */
  @SuppressWarnings("null")
  @NonNull
  protected Map<Class<?>, IBoundInstanceModelGroupedNamed> getClassToInstanceMap() {
    return classToInstanceMap.get();
  }

  /**
   * Get the mapping of JSON discriminator values bound to a distinct grouped
   * model instance.
   *
   * @return the mapping
   */
  @SuppressWarnings("null")
  @NonNull
  protected Map<String, IBoundInstanceModelGroupedNamed> getDiscriminatorToInstanceMap() {
    return discriminatorToInstanceMap.get();
  }

  @Override
  @Nullable
  public IBoundInstanceModelGroupedNamed getGroupedModelInstance(@NonNull Class<?> clazz) {
    return getClassToInstanceMap().get(clazz);
  }

  @Override
  @Nullable
  public IBoundInstanceModelGroupedNamed getGroupedModelInstance(@NonNull QName name) {
    return getQNameToInstanceMap().get(name);
  }

  @Override
  public IBoundInstanceModelGroupedNamed getGroupedModelInstance(String discriminator) {
    return getDiscriminatorToInstanceMap().get(discriminator);
  }

  @Override
  public IGroupAs getGroupAs() {
    return groupAs;
  }

  @SuppressWarnings("null")
  @Override
  public ChoiceGroupModelContainerSupport getModelContainer() {
    return modelContainer.get();
  }

  @Override
  public IBoundDefinitionModelAssembly getOwningDefinition() {
    return getParentContainer();
  }

  @Override
  public IBoundModule getContainingModule() {
    return getOwningDefinition().getContainingModule();
  }

  @Override
  public int getMinOccurs() {
    return getAnnotation().minOccurs();
  }

  @Override
  public int getMaxOccurs() {
    return getAnnotation().maxOccurs();
  }

  @Override
  public String getJsonDiscriminatorProperty() {
    return getAnnotation().discriminator();
  }

  @Override
  public String getJsonKeyFlagInstanceName() {
    return getAnnotation().jsonKey();
  }

  @Override
  public IBoundInstanceFlag getItemJsonKey(Object item) {
    String jsonKeyFlagName = getJsonKeyFlagInstanceName();
    IBoundInstanceFlag retval = null;

    if (jsonKeyFlagName != null) {
      Class<?> clazz = item.getClass();

      IBoundInstanceModelGroupedNamed itemInstance = getClassToInstanceMap().get(clazz);
      retval = itemInstance.getDefinition().getFlagInstanceByName(
          new QName(itemInstance.getXmlNamespace(), jsonKeyFlagName));
    }
    return retval;
  }

  private static class ChoiceGroupModelContainerSupport
      implements IContainerModelSupport<
          IBoundInstanceModelGroupedNamed,
          IBoundInstanceModelGroupedNamed,
          IBoundInstanceModelGroupedField,
          IBoundInstanceModelGroupedAssembly> {

    @NonNull
    private final Map<QName, IBoundInstanceModelGroupedNamed> namedModelInstances;
    @NonNull
    private final Map<QName, IBoundInstanceModelGroupedField> fieldInstances;
    @NonNull
    private final Map<QName, IBoundInstanceModelGroupedAssembly> assemblyInstances;

    public ChoiceGroupModelContainerSupport(
        @NonNull BoundGroupedAssembly[] assemblies,
        @NonNull BoundGroupedField[] fields,
        @NonNull IBoundInstanceModelChoiceGroup container) {
      this.assemblyInstances = CollectionUtil.unmodifiableMap(ObjectUtils.notNull(Arrays.stream(assemblies)
          .map(instance -> {
            assert instance != null;
            return IBoundInstanceModelGroupedAssembly.newInstance(instance,
                container);
          })
          .collect(Collectors.toMap(
              IBoundInstanceModelGroupedAssembly::getXmlQName,
              Function.identity(),
              CustomCollectors.useLastMapper(),
              LinkedHashMap::new))));
      this.fieldInstances = CollectionUtil.unmodifiableMap(ObjectUtils.notNull(Arrays.stream(fields)
          .map(instance -> {
            assert instance != null;
            return IBoundInstanceModelGroupedField.newInstance(instance, container);
          })
          .collect(Collectors.toMap(
              IBoundInstanceModelGroupedField::getXmlQName,
              Function.identity(),
              CustomCollectors.useLastMapper(),
              LinkedHashMap::new))));
      this.namedModelInstances = CollectionUtil.unmodifiableMap(ObjectUtils.notNull(Stream.concat(
          this.assemblyInstances.entrySet().stream(),
          this.fieldInstances.entrySet().stream())
          .collect(Collectors.toMap(
              Entry::getKey,
              Entry::getValue,
              CustomCollectors.useLastMapper(),
              LinkedHashMap::new))));
    }

    @SuppressWarnings("null")
    @Override
    public Collection<IBoundInstanceModelGroupedNamed> getModelInstances() {
      return namedModelInstances.values();
    }

    @Override
    public Map<QName, IBoundInstanceModelGroupedNamed> getNamedModelInstanceMap() {
      return namedModelInstances;
    }

    @Override
    public Map<QName, IBoundInstanceModelGroupedField> getFieldInstanceMap() {
      return fieldInstances;
    }

    @Override
    public Map<QName, IBoundInstanceModelGroupedAssembly> getAssemblyInstanceMap() {
      return assemblyInstances;
    }
  }
}