XmlConstraintLoader.java

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

package gov.nist.secauto.metaschema.core.model.xml;

import gov.nist.secauto.metaschema.core.metapath.MetapathException;
import gov.nist.secauto.metaschema.core.metapath.StaticContext;
import gov.nist.secauto.metaschema.core.model.AbstractLoader;
import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
import gov.nist.secauto.metaschema.core.model.ISource;
import gov.nist.secauto.metaschema.core.model.MetaschemaException;
import gov.nist.secauto.metaschema.core.model.constraint.AssemblyConstraintSet;
import gov.nist.secauto.metaschema.core.model.constraint.AssemblyTargetedConstraints;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintSet;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultScopedContraints;
import gov.nist.secauto.metaschema.core.model.constraint.FieldTargetedConstraints;
import gov.nist.secauto.metaschema.core.model.constraint.FlagTargetedConstraints;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
import gov.nist.secauto.metaschema.core.model.constraint.IModelConstrained;
import gov.nist.secauto.metaschema.core.model.constraint.IScopedContraints;
import gov.nist.secauto.metaschema.core.model.constraint.ITargetedConstraints;
import gov.nist.secauto.metaschema.core.model.constraint.IValueConstrained;
import gov.nist.secauto.metaschema.core.model.constraint.ValueConstraintSet;
import gov.nist.secauto.metaschema.core.model.xml.impl.ConstraintXmlSupport;
import gov.nist.secauto.metaschema.core.model.xml.impl.XmlObjectParser;
import gov.nist.secauto.metaschema.core.model.xml.impl.XmlObjectParser.Handler;
import gov.nist.secauto.metaschema.core.model.xml.xmlbeans.METASCHEMACONSTRAINTSDocument;
import gov.nist.secauto.metaschema.core.model.xml.xmlbeans.METASCHEMACONSTRAINTSDocument.METASCHEMACONSTRAINTS.Scope;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.impl.values.XmlValueNotSupportedException;

import java.io.IOException;
import java.net.URI;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
 * Provides methods to load a constraint set expressed in XML.
 * <p>
 * Loaded constraint instances are cached to avoid the need to load them for
 * every use. Any constraint set imported is also loaded and cached
 * automatically.
 */
@SuppressWarnings("PMD.CouplingBetweenObjects")
public class XmlConstraintLoader
    extends AbstractLoader<List<IConstraintSet>>
    implements IConstraintLoader {

  @SuppressWarnings("PMD.UseConcurrentHashMap")
  @NonNull
  private static final Map<QName,
      Handler<Pair<ISource, List<ITargetedConstraints>>>> SCOPE_OBJECT_MAPPING = ObjectUtils.notNull(
          Map.ofEntries(
              Map.entry(XmlModuleConstants.ASSEMBLY_QNAME, XmlConstraintLoader::handleScopedAssembly),
              Map.entry(XmlModuleConstants.FIELD_QNAME, XmlConstraintLoader::handleScopedField),
              Map.entry(XmlModuleConstants.FLAG_QNAME, XmlConstraintLoader::handleScopedFlag)));

  @NonNull
  private static final XmlObjectParser<Pair<ISource, List<ITargetedConstraints>>> SCOPE_PARSER
      = new XmlObjectParser<>(SCOPE_OBJECT_MAPPING) {

        @Override
        protected Handler<Pair<ISource, List<ITargetedConstraints>>> identifyHandler(XmlCursor cursor, XmlObject obj) {
          Handler<Pair<ISource, List<ITargetedConstraints>>> retval;
          if (obj instanceof Scope.Assembly) {
            retval = XmlConstraintLoader::handleScopedAssembly;
          } else if (obj instanceof Scope.Field) {
            retval = XmlConstraintLoader::handleScopedField;
          } else if (obj instanceof Scope.Flag) {
            retval = XmlConstraintLoader::handleScopedFlag;
          } else {
            throw new IllegalStateException(String.format("Unhandled element type '%s'.", obj.getClass().getName()));
          }
          return retval;
        }

      };

  @Override
  protected List<IConstraintSet> parseResource(@NonNull URI resource, @NonNull Deque<URI> visitedResources)
      throws IOException {

    // parse this metaschema
    METASCHEMACONSTRAINTSDocument xmlObject = parseConstraintSet(resource);

    // now check if this constraint set imports other constraint sets
    int size = xmlObject.getMETASCHEMACONSTRAINTS().sizeOfImportArray();
    Set<IConstraintSet> importedConstraints;
    if (size == 0) {
      importedConstraints = CollectionUtil.emptySet();
    } else {
      try {
        importedConstraints = new LinkedHashSet<>();
        for (METASCHEMACONSTRAINTSDocument.METASCHEMACONSTRAINTS.Import imported : xmlObject.getMETASCHEMACONSTRAINTS()
            .getImportList()) {
          URI importedResource = URI.create(imported.getHref());
          importedResource = ObjectUtils.notNull(resource.resolve(importedResource));
          importedConstraints.addAll(loadInternal(importedResource, visitedResources));
        }
      } catch (MetaschemaException ex) {
        throw new IOException(ex);
      }
    }

    // now create this constraint set
    return CollectionUtil.singletonList(new DefaultConstraintSet(
        resource,
        parseScopedConstraints(xmlObject, resource),
        importedConstraints));
  }

  /**
   * Parse the provided XML resource as a Metaschema constraints.
   *
   * @param resource
   *          the resource to parse
   * @return the XMLBeans representation of the Metaschema contraints
   * @throws IOException
   *           if a parsing error occurred
   */
  @NonNull
  private static METASCHEMACONSTRAINTSDocument parseConstraintSet(@NonNull URI resource) throws IOException {
    try {
      XmlOptions options = new XmlOptions();
      options.setBaseURI(resource);
      options.setLoadLineNumbers();
      return ObjectUtils.notNull(METASCHEMACONSTRAINTSDocument.Factory.parse(resource.toURL(), options));
    } catch (XmlException ex) {
      throw new IOException(ex);
    }
  }

  /**
   * Parse individual constraint definitions from the provided XMLBeans object.
   *
   * @param xmlObject
   *          the XMLBeans object
   * @param resource
   *          the resource containing the constraint content
   * @return the scoped constraint definitions
   */
  @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // intentional
  @NonNull
  protected List<IScopedContraints> parseScopedConstraints(
      @NonNull METASCHEMACONSTRAINTSDocument xmlObject,
      @NonNull URI resource) {
    List<IScopedContraints> scopedConstraints = new LinkedList<>();

    StaticContext.Builder builder = StaticContext.builder()
        .baseUri(resource);

    METASCHEMACONSTRAINTSDocument.METASCHEMACONSTRAINTS constraints = xmlObject.getMETASCHEMACONSTRAINTS();

    constraints.getNamespaceBindingList().stream()
        .forEach(binding -> builder.namespace(
            ObjectUtils.notNull(binding.getPrefix()), ObjectUtils.notNull(binding.getUri())));

    builder.useWildcardWhenNamespaceNotDefaulted(true);

    ISource source = ISource.externalSource(builder.build());

    for (Scope scope : constraints.getScopeList()) {
      assert scope != null;

      List<ITargetedConstraints> targetedConstraints = new LinkedList<>(); // NOPMD - intentional
      try {
        SCOPE_PARSER.parse(scope, Pair.of(source, targetedConstraints));
      } catch (MetapathException | XmlValueNotSupportedException ex) {
        if (ex.getCause() instanceof MetapathException) {
          throw new MetapathException(
              String.format("Unable to compile a Metapath in '%s'. %s",
                  source.getSource(),
                  ex.getLocalizedMessage()),
              ex);
        }
        throw ex;
      }

      URI namespace = ObjectUtils.notNull(URI.create(scope.getMetaschemaNamespace()));
      String shortName = ObjectUtils.requireNonNull(scope.getMetaschemaShortName());

      scopedConstraints.add(new DefaultScopedContraints(
          namespace,
          shortName,
          CollectionUtil.unmodifiableList(targetedConstraints)));
    }
    return CollectionUtil.unmodifiableList(scopedConstraints);
  }

  private static void handleScopedAssembly( // NOPMD false positive
      @NonNull XmlObject obj,
      Pair<ISource, List<ITargetedConstraints>> state) {
    Scope.Assembly assembly = (Scope.Assembly) obj;

    IModelConstrained constraints = new AssemblyConstraintSet();
    ConstraintXmlSupport.parse(constraints, assembly, ObjectUtils.notNull(state.getLeft()));

    state.getRight().add(new AssemblyTargetedConstraints(
        ObjectUtils.requireNonNull(assembly.getTarget()),
        constraints));
  }

  private static void handleScopedField( // NOPMD false positive
      @NonNull XmlObject obj,
      Pair<ISource, List<ITargetedConstraints>> state) {
    Scope.Field field = (Scope.Field) obj;

    IValueConstrained constraints = new ValueConstraintSet();
    ConstraintXmlSupport.parse(constraints, field, ObjectUtils.notNull(state.getLeft()));

    state.getRight().add(new FieldTargetedConstraints(
        ObjectUtils.requireNonNull(field.getTarget()),
        constraints));
  }

  private static void handleScopedFlag( // NOPMD false positive
      @NonNull XmlObject obj,
      Pair<ISource, List<ITargetedConstraints>> state) {
    Scope.Flag flag = (Scope.Flag) obj;

    IValueConstrained constraints = new ValueConstraintSet();
    ConstraintXmlSupport.parse(constraints, flag, ObjectUtils.notNull(state.getLeft()));

    state.getRight().add(new FlagTargetedConstraints(
        ObjectUtils.requireNonNull(flag.getTarget()),
        constraints));
  }
}