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 }