ExternalConstraintsModulePostProcessor.java

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

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

import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.ISequence;
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression.ResultType;
import gov.nist.secauto.metaschema.core.metapath.StaticContext;
import gov.nist.secauto.metaschema.core.metapath.item.IItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IModuleNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
import gov.nist.secauto.metaschema.core.model.IDefinition;
import gov.nist.secauto.metaschema.core.model.IModule;
import gov.nist.secauto.metaschema.core.model.IModuleLoader;
import gov.nist.secauto.metaschema.core.model.constraint.impl.ConstraintComposingVisitor;
import gov.nist.secauto.metaschema.core.model.xml.ModuleLoader;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

/**
 * A module loading post processor that integrates applicable external
 * constraints into a given module when loaded.
 *
 * @see ModuleLoader#ModuleLoader(List)
 */
public class ExternalConstraintsModulePostProcessor implements IModuleLoader.IModulePostProcessor {
  private static final Logger LOGGER = LogManager.getLogger(ExternalConstraintsModulePostProcessor.class);
  @NonNull
  private final List<IConstraintSet> registeredConstraintSets;

  /**
   * Create a new post processor.
   *
   * @param additionalConstraintSets
   *          the external constraint sets to apply
   */
  public ExternalConstraintsModulePostProcessor(@NonNull Collection<IConstraintSet> additionalConstraintSets) {
    this.registeredConstraintSets = ObjectUtils.notNull(additionalConstraintSets.stream()
        .flatMap(set -> Stream.concat(
            Stream.of(set),
            set.getImportedConstraintSets().stream()))
        .distinct()
        .collect(Collectors.toUnmodifiableList()));
  }

  /**
   * Get the external constraint sets associated with this post processor.
   *
   * @return the list of constraint sets
   */
  protected List<IConstraintSet> getRegisteredConstraintSets() {
    return registeredConstraintSets;
  }

  @Override
  public void processModule(IModule module) {
    ConstraintComposingVisitor visitor = new ConstraintComposingVisitor();
    IModuleNodeItem moduleItem = INodeItemFactory.instance().newModuleNodeItem(module);

    StaticContext staticContext = StaticContext.builder()
        .defaultModelNamespace(module.getXmlNamespace())
        .build();
    DynamicContext dynamicContext = new DynamicContext(staticContext);

    for (IConstraintSet set : getRegisteredConstraintSets()) {
      assert set != null;
      applyConstraints(module, moduleItem, set, visitor, dynamicContext);
    }
  }

  private static void applyConstraints(
      @NonNull IModule module,
      @NonNull IModuleNodeItem moduleItem,
      @NonNull IConstraintSet set,
      @NonNull ConstraintComposingVisitor visitor,
      @NonNull DynamicContext dynamicContext) {
    for (ITargetedConstraints targeted : set.getTargetedConstraintsForModule(module)) {
      // apply targeted constraints
      String targetExpression = targeted.getTargetExpression();
      MetapathExpression metapath = MetapathExpression.compile(targetExpression, dynamicContext.getStaticContext());
      ISequence<?> items = metapath.evaluateAs(moduleItem, ResultType.SEQUENCE, dynamicContext);
      assert items != null;

      // first build a map to ensure the constraint is only applied once to each
      // underlying definition
      Map<IDefinition, IDefinitionNodeItem<?, ?>> definitions = items.stream()
          .filter(item -> filterNonDefinitionItem(item, metapath))
          .map(item -> (IDefinitionNodeItem<?, ?>) item)
          .collect(Collectors.toMap(IDefinitionNodeItem::getDefinition, Function.identity(), (v1, v2) -> v1,
              LinkedHashMap::new));

      // apply the constraints
      definitions.values().forEach(item -> {
        item.accept(visitor, targeted);
      });
    }
  }

  private static boolean filterNonDefinitionItem(IItem item, @NonNull MetapathExpression metapath) {
    boolean retval = item instanceof IDefinitionNodeItem;
    if (!retval) {
      LOGGER.atError().log(
          "Found non-definition item '{}' while applying external constraints using target expression '{}'.",
          item.toString(),
          metapath.getPath());
    }
    return retval;
  }
}