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