1 /*
2 * SPDX-FileCopyrightText: none
3 * SPDX-License-Identifier: CC0-1.0
4 */
5
6 package dev.metaschema.core.metapath;
7
8 import java.math.BigDecimal;
9
10 import dev.metaschema.core.metapath.function.FunctionUtils;
11 import dev.metaschema.core.metapath.function.library.FnBoolean;
12 import dev.metaschema.core.metapath.impl.LazyCompilationMetapathExpression;
13 import dev.metaschema.core.metapath.impl.MetapathExpression;
14 import dev.metaschema.core.metapath.item.IItem;
15 import dev.metaschema.core.metapath.item.ISequence;
16 import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
17 import dev.metaschema.core.metapath.item.atomic.INumericItem;
18 import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
19 import dev.metaschema.core.metapath.type.TypeMetapathException;
20 import dev.metaschema.core.util.ObjectUtils;
21 import edu.umd.cs.findbugs.annotations.NonNull;
22 import edu.umd.cs.findbugs.annotations.Nullable;
23
24 /**
25 * Supports compiling and executing Metapath expressions.
26 */
27 public interface IMetapathExpression extends IExpression {
28
29 /**
30 * Identifies the expected type for a Metapath evaluation result.
31 */
32 enum ResultType {
33 /**
34 * The result is expected to be a {@link BigDecimal} value.
35 */
36 NUMBER(BigDecimal.class, sequence -> {
37 IItem item = sequence.getFirstItem(true);
38 if (item == null) {
39 return null;
40 }
41 IAnyAtomicItem atomicItem = ISequence.getFirstItem(item.atomize(), true);
42 if (atomicItem == null) {
43 throw new InvalidTypeMetapathException(item, "Unable to cast to numeric: atomization returned null");
44 }
45 INumericItem numeric = FunctionUtils.castToNumeric(atomicItem);
46 return numeric.asDecimal();
47 }),
48 /**
49 * The result is expected to be a {@link String} value.
50 */
51 STRING(String.class, sequence -> {
52 IAnyAtomicItem item = ISequence.of(sequence.atomize()).getFirstItem(true);
53 return item == null ? "" : item.asString();
54 }),
55 /**
56 * The result is expected to be a {@link Boolean} value.
57 */
58 BOOLEAN(Boolean.class, sequence -> FnBoolean.fnBoolean(sequence).toBoolean()),
59 /**
60 * The result is expected to be an {@link IItem} value.
61 */
62 ITEM(IItem.class, sequence -> sequence.getFirstItem(true));
63
64 @NonNull
65 private final Class<?> clazz;
66 private final ConversionFunction converter;
67
68 ResultType(@NonNull Class<?> clazz, @NonNull ConversionFunction converter) {
69 this.clazz = clazz;
70 this.converter = converter;
71 }
72
73 /**
74 * Get the expected class for the result type.
75 *
76 * @return the expected class
77 *
78 */
79 @NonNull
80 public Class<?> expectedClass() {
81 return clazz;
82 }
83
84 /**
85 * Convert the provided sequence to the expected type.
86 *
87 * @param <T>
88 * the Java type of the expected return value
89 * @param sequence
90 * the Metapath result sequence to convert
91 * @return the converted sequence as the expected type
92 * @throws TypeMetapathException
93 * if the provided sequence is incompatible with the expected result
94 * type
95 */
96 // TODO: trace exceptions thrown to ensure the Javadoc matches; should be static
97 // type error
98 @Nullable
99 public <T> T convert(@NonNull ISequence<?> sequence) {
100 try {
101 return ObjectUtils.asNullableType(converter.convert(sequence));
102 } catch (ClassCastException ex) {
103 throw new InvalidTypeMetapathException(
104 null,
105 String.format("Unable to cast type '%s' to expected result type '%s'.",
106 expectedClass().getName(),
107 name()),
108 ex);
109 }
110 }
111
112 @FunctionalInterface
113 interface ConversionFunction {
114 @Nullable
115 Object convert(@NonNull ISequence<?> sequence);
116 }
117 }
118
119 /**
120 * Get the Metapath expression identifying the current context node.
121 *
122 * @return the context expression
123 */
124 @NonNull
125 static IMetapathExpression contextNode() {
126 return MetapathExpression.CONTEXT_METAPATH;
127 }
128
129 /**
130 * Compile a Metapath expression string.
131 *
132 * @param path
133 * the metapath expression
134 * @return the compiled expression object
135 * @throws InvalidMetapathGrammarException
136 * if an error occurred while compiling the Metapath expression
137 */
138 @NonNull
139 static IMetapathExpression compile(@NonNull String path) {
140 return compile(path, StaticContext.instance());
141 }
142
143 /**
144 * Compiles a Metapath expression string using the provided static context.
145 *
146 * @param path
147 * the metapath expression
148 * @param staticContext
149 * the static evaluation context
150 * @return the compiled expression object
151 * @throws InvalidMetapathGrammarException
152 * if an error occurred while compiling the Metapath expression
153 */
154 @NonNull
155 static IMetapathExpression compile(@NonNull String path, @NonNull StaticContext staticContext) {
156 return MetapathExpression.compile(path, staticContext);
157 }
158
159 /**
160 * Gets a new Metapath expression that is compiled on use.
161 * <p>
162 * Lazy compilation may cause additional {@link MetapathException} errors at
163 * evaluation time, since compilation errors are not raised until evaluation.
164 *
165 * @param path
166 * the metapath expression
167 * @param staticContext
168 * the static evaluation context
169 * @return the expression object
170 */
171 @NonNull
172 static IMetapathExpression lazyCompile(@NonNull String path, @NonNull StaticContext staticContext) {
173 return new LazyCompilationMetapathExpression(path, staticContext);
174 }
175
176 /**
177 * Get the original Metapath expression as a string.
178 *
179 * @return the expression
180 */
181 @Override
182 @NonNull
183 String getPath();
184
185 /**
186 * Get the static context used to compile this Metapath.
187 *
188 * @return the static context
189 */
190 @NonNull
191 StaticContext getStaticContext();
192
193 /**
194 * Evaluate this Metapath expression without a specific focus. The required
195 * result type will be determined by the {@code resultType} argument.
196 *
197 * @param <T>
198 * the expected result type
199 * @param resultType
200 * the type of result to produce
201 * @return the converted result
202 * @throws TypeMetapathException
203 * if the provided sequence is incompatible with the requested result
204 * type
205 * @throws MetapathException
206 * if an error occurred during evaluation
207 * @see ResultType#convert(ISequence)
208 */
209 @Nullable
210 default <T> T evaluateAs(@NonNull ResultType resultType) {
211 return evaluateAs(null, resultType);
212 }
213
214 /**
215 * Evaluate this Metapath expression using the provided {@code focus} as the
216 * initial evaluation context. The required result type will be determined by
217 * the {@code resultType} argument.
218 *
219 * @param <T>
220 * the expected result type
221 * @param focus
222 * the focus of the expression
223 * @param resultType
224 * the type of result to produce
225 * @return the converted result
226 * @throws TypeMetapathException
227 * if the provided sequence is incompatible with the requested result
228 * type
229 * @throws MetapathException
230 * if an error occurred during evaluation
231 * @see ResultType#convert(ISequence)
232 */
233 @Nullable
234 default <T> T evaluateAs(
235 @Nullable IItem focus,
236 @NonNull ResultType resultType) {
237 ISequence<?> result = evaluate(focus);
238 return resultType.convert(result);
239 }
240
241 /**
242 * Evaluate this Metapath expression using the provided {@code focus} as the
243 * initial evaluation context. The specific result type will be determined by
244 * the {@code resultType} argument.
245 * <p>
246 * This variant allow for reuse of a provided {@code dynamicContext}.
247 *
248 * @param <T>
249 * the expected result type
250 * @param focus
251 * the outer focus of the expression
252 * @param resultType
253 * the type of result to produce
254 * @param dynamicContext
255 * the dynamic context to use for evaluation
256 * @return the converted result
257 * @throws TypeMetapathException
258 * if the provided sequence is incompatible with the requested result
259 * type
260 * @throws MetapathException
261 * if an error occurred during evaluation
262 * @see ResultType#convert(ISequence)
263 */
264 @Nullable
265 default <T> T evaluateAs(
266 @Nullable IItem focus,
267 @NonNull ResultType resultType,
268 @NonNull DynamicContext dynamicContext) {
269 ISequence<?> result = evaluate(focus, dynamicContext);
270 return resultType.convert(result);
271 }
272
273 /**
274 * Evaluate this Metapath expression without a specific focus.
275 *
276 * @param <T>
277 * the type of items contained in the resulting sequence
278 * @return a sequence of Metapath items representing the result of the
279 * evaluation
280 * @throws MetapathException
281 * if an error occurred during evaluation
282 */
283 @NonNull
284 default <T extends IItem> ISequence<T> evaluate() {
285 return evaluate((IItem) null);
286 }
287
288 /**
289 * Evaluate this Metapath expression using the provided {@code focus} as the
290 * initial evaluation context.
291 *
292 * @param <T>
293 * the type of items contained in the resulting sequence
294 * @param focus
295 * the outer focus of the expression
296 * @return a sequence of Metapath items representing the result of the
297 * evaluation
298 * @throws MetapathException
299 * if an error occurred during evaluation
300 */
301 @NonNull
302 default <T extends IItem> ISequence<T> evaluate(
303 @Nullable IItem focus) {
304 return evaluate(focus, new DynamicContext(getStaticContext()));
305 }
306
307 /**
308 * Evaluate this Metapath expression using the provided {@code focus} as the
309 * initial evaluation context.
310 * <p>
311 * This variant allow for reuse of a provided {@code dynamicContext}.
312 *
313 * @param <T>
314 * the type of items contained in the resulting sequence
315 * @param focus
316 * the outer focus of the expression
317 * @param dynamicContext
318 * the dynamic context to use for evaluation
319 * @return a sequence of Metapath items representing the result of the
320 * evaluation
321 * @throws MetapathException
322 * if an error occurred during evaluation
323 */
324 @NonNull
325 <T extends IItem> ISequence<T> evaluate(
326 @Nullable IItem focus,
327 @NonNull DynamicContext dynamicContext);
328
329 }