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 }