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 }