1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.model.constraint;
7   
8   import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
9   import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression;
10  import gov.nist.secauto.metaschema.core.metapath.MetapathException;
11  import gov.nist.secauto.metaschema.core.metapath.item.IItem;
12  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
13  import gov.nist.secauto.metaschema.core.model.constraint.impl.DefaultIndex;
14  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
15  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
16  
17  import java.util.ArrayList;
18  import java.util.List;
19  import java.util.regex.Matcher;
20  import java.util.regex.Pattern;
21  import java.util.stream.Collectors;
22  
23  import edu.umd.cs.findbugs.annotations.NonNull;
24  import edu.umd.cs.findbugs.annotations.Nullable;
25  
26  /**
27   * An index that can support the {@link IIndexConstraint},
28   * {@link IIndexHasKeyConstraint}, and {@link IUniqueConstraint}.
29   */
30  public interface IIndex {
31  
32    /**
33     * Construct a new index using the provided key field components to generate
34     * keys.
35     *
36     * @param keyFields
37     *          the key field components to use to generate keys by default
38     * @return the new index
39     */
40    @NonNull
41    static IIndex newInstance(@NonNull List<? extends IKeyField> keyFields) {
42      return new DefaultIndex(keyFields);
43    }
44  
45    /**
46     * Check if a key contains information other than {@code null} Strings.
47     *
48     * @param key
49     *          the key to check
50     * @return {@code true} if the series of key values contains only {@code null}
51     *         values, or {@code false} otherwise
52     */
53    static boolean isAllNulls(@NonNull Iterable<String> key) {
54      for (String value : key) {
55        if (value != null) {
56          return false; // NOPMD readability
57        }
58      }
59      return true;
60    }
61  
62    /**
63     * Retrieve the key field components used to generate a key for this index.
64     *
65     * @return the key field components
66     */
67    @NonNull
68    List<IKeyField> getKeyFields();
69  
70    /**
71     * Store the provided item using the provided key.
72     *
73     * @param item
74     *          the item to store
75     * @param key
76     *          the key to store the item with
77     * @return the previous item stored in the index using the key, or {@code null}
78     *         otherwise
79     */
80    @Nullable
81    INodeItem put(@NonNull INodeItem item, @NonNull List<String> key);
82  
83    /**
84     * Retrieve the item from the index that matches the provided key.
85     *
86     * @param key
87     *          the key to use for lookup
88     * @return the item with the matching key or {@code null} if no matching item
89     *         was found
90     */
91    INodeItem get(List<String> key);
92  
93    /**
94     * Construct a key by evaluating the provided key field components against the
95     * provided item.
96     *
97     * @param item
98     *          the item to generate the key from
99     * @param keyFields
100    *          the key field components used to generate the key
101    * @param dynamicContext
102    *          the Metapath evaluation context
103    * @return a new key
104    * @throws IllegalArgumentException
105    *           if a key field has a configured pattern that fails to match the key
106    *           item value or if the pattern is malformed
107    * @throws MetapathException
108    *           if the evaluation of a key field's metapath resulted in an
109    *           unexpected error
110    */
111   @NonNull
112   static List<String> toKey(
113       @NonNull INodeItem item,
114       @NonNull List<? extends IKeyField> keyFields,
115       @NonNull DynamicContext dynamicContext) {
116     return CollectionUtil.unmodifiableList(
117         ObjectUtils.notNull(keyFields.stream()
118             .map(keyField -> {
119               assert keyField != null;
120               return buildKeyItem(item, keyField, dynamicContext);
121             })
122             .collect(Collectors.toCollection(ArrayList::new))));
123   }
124 
125   /**
126    * Evaluates the provided key field component against the item to generate a key
127    * value.
128    *
129    * @param item
130    *          the item to generate the key value from
131    * @param keyField
132    *          the key field component used to generate the key value
133    * @param dynamicContext
134    *          the Metapath evaluation context
135    * @return the key value or {@code null} if the evaluation resulted in no value
136    * @throws IllegalArgumentException
137    *           if the key field has a configured pattern that fails to match the
138    *           key item value or if the pattern is malformed
139    * @throws MetapathException
140    *           if the evaluation of the key metapath resulted in an unexpected
141    *           error
142    */
143   @Nullable
144   private static String buildKeyItem(
145       @NonNull INodeItem item,
146       @NonNull IKeyField keyField,
147       @NonNull DynamicContext dynamicContext) {
148     IMetapathExpression keyMetapath = keyField.getTarget();
149 
150     IItem keyItem = keyMetapath.evaluateAs(item, IMetapathExpression.ResultType.ITEM, dynamicContext);
151 
152     String keyValue = null;
153     if (keyItem != null) {
154       keyValue = keyItem.toAtomicItem().asString();
155       assert keyValue != null;
156       Pattern pattern = keyField.getPattern();
157       if (pattern != null) {
158         keyValue = applyPattern(keyMetapath, keyValue, pattern);
159       }
160     } // else empty key
161     return keyValue;
162   }
163 
164   /**
165    * Apply the key value pattern, if configured, to generate the final key value.
166    * <p>
167    * The provided pattern is expected to have a single matching group, which will
168    * contain the final key value on match
169    *
170    * @param keyItem
171    *          the node item used to form the key field
172    * @param pattern
173    *          the key field pattern configuration from the constraint
174    * @param keyValue
175    *          the current key value
176    * @return the final key value
177    * @throws IllegalArgumentException
178    *           if the provided key value does not match the provided pattern or if
179    *           the pattern is malformed
180    */
181   @Nullable
182   private static String applyPattern(
183       @NonNull IMetapathExpression keyMetapath,
184       @NonNull String keyValue,
185       @NonNull Pattern pattern) {
186     Matcher matcher = pattern.matcher(keyValue);
187     if (!matcher.matches()) {
188       // TODO: use a different exception type?
189       throw new IllegalArgumentException(
190           String.format("Key field declares the pattern '%s' which does not match the value '%s' of node '%s'",
191               pattern.pattern(), keyValue, keyMetapath));
192     }
193 
194     if (matcher.groupCount() != 1) {
195       throw new IllegalArgumentException(
196           String.format("The first group was not a match for value '%s' of node '%s' for key field pattern '%s'",
197               keyValue, keyMetapath, pattern.pattern()));
198     }
199 
200     String result = matcher.group(1);
201 
202     return result == null || result.isEmpty() ? null : result;
203   }
204 }