1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.model.constraint;
7   
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import java.util.Collections;
12  import java.util.HashMap;
13  import java.util.HashSet;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.Set;
17  import java.util.stream.Collectors;
18  
19  import javax.xml.namespace.QName;
20  
21  import dev.metaschema.core.metapath.DynamicContext;
22  import dev.metaschema.core.metapath.IMetapathExpression;
23  import dev.metaschema.core.metapath.item.IItem;
24  import dev.metaschema.core.metapath.item.ISequence;
25  import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
26  import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
27  import dev.metaschema.core.model.IDefinition;
28  import dev.metaschema.core.model.IModelElementVisitor;
29  import dev.metaschema.core.model.ISource;
30  import dev.metaschema.core.qname.IEnhancedQName;
31  import dev.metaschema.core.util.CollectionUtil;
32  import dev.metaschema.core.util.ObjectUtils;
33  import edu.umd.cs.findbugs.annotations.NonNull;
34  
35  /**
36   * The default implementation of a constraint set sourced from an external
37   * constraint resource.
38   */
39  public class ScopedConstraintSet implements IConstraintSet {
40    private static final Logger LOGGER = LogManager.getLogger(ScopedConstraintSet.class);
41    @NonNull
42    private final ISource source;
43    @NonNull
44    private final Set<IConstraintSet> importedConstraintSets;
45    @NonNull
46    private final Map<IEnhancedQName, List<IScopedContraints>> scopedContraints;
47    @NonNull
48    private final Set<IDefinition> previouslyTargetedDefinitions = new HashSet<>();
49  
50    /**
51     * Construct a new constraint set.
52     *
53     * @param source
54     *          the resource the constraint was provided from
55     * @param scopedContraints
56     *          a set of constraints qualified by a scope path
57     * @param importedConstraintSets
58     *          constraint sets imported by this constraint set
59     */
60    @SuppressWarnings("null")
61    public ScopedConstraintSet(
62        @NonNull ISource source,
63        @NonNull List<IScopedContraints> scopedContraints,
64        @NonNull Set<IConstraintSet> importedConstraintSets) {
65      this.source = source;
66      this.scopedContraints = scopedContraints.stream()
67          .collect(
68              Collectors.collectingAndThen(
69                  Collectors.groupingBy(
70                      scope -> IEnhancedQName.of(scope.getModuleNamespace().toString(), scope.getModuleShortName()),
71                      Collectors.toUnmodifiableList()),
72                  Collections::unmodifiableMap));
73      this.importedConstraintSets = CollectionUtil.unmodifiableSet(importedConstraintSets);
74    }
75  
76    /**
77     * Get the resource the constraint was provided from.
78     *
79     * @return the resource
80     */
81    @Override
82    public ISource getSource() {
83      return source;
84    }
85  
86    /**
87     * Get the set of Metaschema scoped constraints to apply by a {@link QName}
88     * formed from the Metaschema namespace and short name.
89     *
90     * @return the mapping of QName to scoped constraints
91     */
92    @NonNull
93    public Map<IEnhancedQName, List<IScopedContraints>> getScopedContraints() {
94      return scopedContraints;
95    }
96  
97    @Override
98    public Set<IConstraintSet> getImportedConstraintSets() {
99      return importedConstraintSets;
100   }
101 
102   @Override
103   public void applyConstraintsForModule(
104       IModuleNodeItem moduleItem,
105       IModelElementVisitor<ITargetedConstraints, Void> visitor) {
106     IEnhancedQName qname = moduleItem.getModule().getQName();
107     List<IScopedContraints> scopes = getScopedContraints().getOrDefault(qname, CollectionUtil.emptyList());
108 
109     @SuppressWarnings("PMD.UseConcurrentHashMap")
110     Map<IDefinition, Set<ITargetedConstraints>> definitionConstraints = new HashMap<>();
111 
112     DynamicContext dynamicContext = new DynamicContext(getSource().getStaticContext());
113 
114     for (IScopedContraints scoped : scopes) {
115       for (ITargetedConstraints targeted : scoped.getTargetedContraints()) {
116         for (IMetapathExpression metapath : targeted.getTargets()) {
117           ISequence<? extends IDefinitionNodeItem<?, ?>> items = ISequence.of(ObjectUtils.notNull(
118               metapath.evaluate(moduleItem, dynamicContext).stream()
119                   .filter(item -> filterNonDefinitionItem(item, metapath))
120                   .map(item -> (IDefinitionNodeItem<?, ?>) item)))
121               .reusable();
122           assert items != null;
123 
124           Set<IDefinition> targetedDefinitions = items.stream()
125               .map(IDefinitionNodeItem::getDefinition)
126               .filter(definition -> !previouslyTargetedDefinitions.contains(definition))
127               .collect(Collectors.toUnmodifiableSet());
128 
129           targetedDefinitions.forEach(definition -> {
130             definitionConstraints.compute(definition, (key, value) -> {
131               Set<ITargetedConstraints> targets = value == null ? new HashSet<>() : value;
132               targets.add(targeted);
133               return targets;
134             });
135           });
136         }
137       }
138     }
139 
140     for (Map.Entry<IDefinition, Set<ITargetedConstraints>> entry : definitionConstraints.entrySet()) {
141       IDefinition definition = entry.getKey();
142       for (ITargetedConstraints constraints : entry.getValue()) {
143         definition.accept(visitor, constraints);
144       }
145     }
146     previouslyTargetedDefinitions.addAll(ObjectUtils.notNull(definitionConstraints.keySet()));
147   }
148 
149   private static boolean filterNonDefinitionItem(IItem item, @NonNull IMetapathExpression metapath) {
150     boolean retval = item instanceof IDefinitionNodeItem;
151     if (!retval) {
152       LOGGER.atError().log(
153           "Found non-definition item '{}' while applying external constraints using target expression '{}'.",
154           item.toString(),
155           metapath.getPath());
156     }
157     return retval;
158   }
159 }