1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.metapath;
7   
8   import gov.nist.secauto.metaschema.core.metapath.antlr.FailingErrorListener;
9   import gov.nist.secauto.metaschema.core.metapath.antlr.Metapath10;
10  import gov.nist.secauto.metaschema.core.metapath.antlr.Metapath10Lexer;
11  import gov.nist.secauto.metaschema.core.metapath.antlr.ParseTreePrinter;
12  import gov.nist.secauto.metaschema.core.metapath.cst.BuildCSTVisitor;
13  import gov.nist.secauto.metaschema.core.metapath.cst.CSTPrinter;
14  import gov.nist.secauto.metaschema.core.metapath.cst.IExpression;
15  import gov.nist.secauto.metaschema.core.metapath.cst.path.ContextItem;
16  import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils;
17  import gov.nist.secauto.metaschema.core.metapath.function.library.FnBoolean;
18  import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
19  import gov.nist.secauto.metaschema.core.metapath.item.IItem;
20  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
21  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDecimalItem;
22  import gov.nist.secauto.metaschema.core.metapath.item.atomic.INumericItem;
23  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
24  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
25  
26  import org.antlr.v4.runtime.CharStreams;
27  import org.antlr.v4.runtime.CommonTokenStream;
28  import org.antlr.v4.runtime.DefaultErrorStrategy;
29  import org.antlr.v4.runtime.Parser;
30  import org.antlr.v4.runtime.misc.ParseCancellationException;
31  import org.antlr.v4.runtime.tree.ParseTree;
32  import org.apache.logging.log4j.LogManager;
33  import org.apache.logging.log4j.Logger;
34  
35  import java.io.ByteArrayOutputStream;
36  import java.io.IOException;
37  import java.io.PrintStream;
38  import java.math.BigDecimal;
39  import java.nio.charset.StandardCharsets;
40  
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  import edu.umd.cs.findbugs.annotations.Nullable;
43  
44  /**
45   * Supports compiling and executing Metapath expressions.
46   */
47  @SuppressWarnings({
48      "PMD.CouplingBetweenObjects" // necessary since this class aggregates functionality
49  })
50  public class MetapathExpression {
51  
52    public enum ResultType {
53      /**
54       * The result is expected to be a {@link BigDecimal} value.
55       */
56      NUMBER,
57      /**
58       * The result is expected to be a {@link String} value.
59       */
60      STRING,
61      /**
62       * The result is expected to be a {@link Boolean} value.
63       */
64      BOOLEAN,
65      /**
66       * The result is expected to be an {@link ISequence} value.
67       */
68      SEQUENCE,
69      /**
70       * The result is expected to be an {@link INodeItem} value.
71       */
72      // TODO: audit use of this value, replace with ITEM where appropriate
73      NODE,
74      /**
75       * The result is expected to be an {@link IItem} value.
76       */
77      ITEM;
78    }
79  
80    private static final Logger LOGGER = LogManager.getLogger(MetapathExpression.class);
81  
82    @NonNull
83    public static final MetapathExpression CONTEXT_NODE
84        = new MetapathExpression(".", ContextItem.instance(), StaticContext.instance());
85  
86    @NonNull
87    private final String path;
88    @NonNull
89    private final IExpression expression;
90    @NonNull
91    private final StaticContext staticContext;
92  
93    /**
94     * Compiles a Metapath expression string.
95     *
96     * @param path
97     *          the metapath expression
98     * @return the compiled expression object
99     * @throws MetapathException
100    *           if an error occurred while compiling the Metapath expression
101    */
102   @NonNull
103   public static MetapathExpression compile(@NonNull String path) {
104     return compile(path, StaticContext.instance());
105   }
106 
107   /**
108    * Compiles a Metapath expression string using the provided static context.
109    *
110    * @param path
111    *          the metapath expression
112    * @param context
113    *          the static evaluation context
114    * @return the compiled expression object
115    * @throws MetapathException
116    *           if an error occurred while compiling the Metapath expression
117    */
118   @NonNull
119   public static MetapathExpression compile(@NonNull String path, @NonNull StaticContext context) {
120     @NonNull MetapathExpression retval;
121     if (".".equals(path)) {
122       retval = CONTEXT_NODE;
123     } else {
124       try {
125         Metapath10Lexer lexer = new Metapath10Lexer(CharStreams.fromString(path));
126         lexer.removeErrorListeners();
127         lexer.addErrorListener(new FailingErrorListener());
128 
129         CommonTokenStream tokens = new CommonTokenStream(lexer);
130         Metapath10 parser = new Metapath10(tokens);
131         parser.removeErrorListeners();
132         parser.addErrorListener(new FailingErrorListener());
133         parser.setErrorHandler(new DefaultErrorStrategy() {
134 
135           @Override
136           public void sync(Parser recognizer) {
137             // disable
138           }
139         });
140 
141         ParseTree tree = ObjectUtils.notNull(parser.expr());
142 
143         if (LOGGER.isDebugEnabled()) {
144           try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
145             try (PrintStream ps = new PrintStream(os, true, StandardCharsets.UTF_8)) {
146               ParseTreePrinter printer = new ParseTreePrinter(ps);
147               printer.print(tree, Metapath10.ruleNames);
148               ps.flush();
149             }
150             LOGGER.atDebug().log(String.format("Metapath AST:%n%s", os.toString(StandardCharsets.UTF_8)));
151           } catch (IOException ex) {
152             LOGGER.atError().withThrowable(ex).log("An unexpected error occurred while closing the steam.");
153           }
154         }
155 
156         IExpression expr = new BuildCSTVisitor(context).visit(tree);
157 
158         if (LOGGER.isDebugEnabled()) {
159           LOGGER.atDebug().log(String.format("Metapath CST:%n%s", CSTPrinter.toString(expr)));
160         }
161         retval = new MetapathExpression(path, expr, context);
162       } catch (MetapathException | ParseCancellationException ex) {
163         String msg = String.format("Unable to compile Metapath '%s'", path);
164         LOGGER.atError().withThrowable(ex).log(msg);
165         throw new StaticMetapathException(StaticMetapathException.INVALID_PATH_GRAMMAR, msg, ex);
166       }
167     }
168     return retval;
169   }
170 
171   /**
172    * Construct a new Metapath expression.
173    *
174    * @param path
175    *          the Metapath as a string
176    * @param expr
177    *          the Metapath as a compiled abstract syntax tree (AST)
178    * @param staticContext
179    *          the static evaluation context
180    */
181   protected MetapathExpression(
182       @NonNull String path,
183       @NonNull IExpression expr,
184       @NonNull StaticContext staticContext) {
185     this.path = path;
186     this.expression = expr;
187     this.staticContext = staticContext;
188   }
189 
190   /**
191    * Get the original Metapath expression as a string.
192    *
193    * @return the expression
194    */
195   @NonNull
196   public String getPath() {
197     return path;
198   }
199 
200   /**
201    * Get the compiled abstract syntax tree (AST) representation of the Metapath.
202    *
203    * @return the Metapath AST
204    */
205   @NonNull
206   protected IExpression getASTNode() {
207     return expression;
208   }
209 
210   /**
211    * Get the static context used to compile this Metapath.
212    *
213    * @return the static context
214    */
215   @NonNull
216   protected StaticContext getStaticContext() {
217     return staticContext;
218   }
219 
220   @Override
221   public String toString() {
222     return CSTPrinter.toString(getASTNode());
223   }
224 
225   /**
226    * Evaluate this Metapath expression without a specific focus. The required
227    * result type will be determined by the {@code resultType} argument.
228    *
229    * @param <T>
230    *          the expected result type
231    * @param resultType
232    *          the type of result to produce
233    * @return the converted result
234    * @throws TypeMetapathException
235    *           if the provided sequence is incompatible with the requested result
236    *           type
237    * @throws MetapathException
238    *           if an error occurred during evaluation
239    * @see #toResultType(ISequence, ResultType)
240    */
241   @Nullable
242   public <T> T evaluateAs(@NonNull ResultType resultType) {
243     return evaluateAs(null, resultType);
244   }
245 
246   /**
247    * Evaluate this Metapath expression using the provided {@code focus} as the
248    * initial evaluation context. The required result type will be determined by
249    * the {@code resultType} argument.
250    *
251    * @param <T>
252    *          the expected result type
253    * @param focus
254    *          the outer focus of the expression
255    * @param resultType
256    *          the type of result to produce
257    * @return the converted result
258    * @throws TypeMetapathException
259    *           if the provided sequence is incompatible with the requested result
260    *           type
261    * @throws MetapathException
262    *           if an error occurred during evaluation
263    * @see #toResultType(ISequence, ResultType)
264    */
265   @Nullable
266   public <T> T evaluateAs(
267       @Nullable IItem focus,
268       @NonNull ResultType resultType) {
269     ISequence<?> result = evaluate(focus);
270     return toResultType(result, resultType);
271   }
272 
273   /**
274    * Evaluate this Metapath expression using the provided {@code focus} as the
275    * initial evaluation context. The specific result type will be determined by
276    * the {@code resultType} argument.
277    * <p>
278    * This variant allow for reuse of a provided {@code dynamicContext}.
279    *
280    * @param <T>
281    *          the expected result type
282    * @param focus
283    *          the outer focus of the expression
284    * @param resultType
285    *          the type of result to produce
286    * @param dynamicContext
287    *          the dynamic context to use for evaluation
288    * @return the converted result
289    * @throws TypeMetapathException
290    *           if the provided sequence is incompatible with the requested result
291    *           type
292    * @throws MetapathException
293    *           if an error occurred during evaluation
294    * @see #toResultType(ISequence, ResultType)
295    */
296   @Nullable
297   public <T> T evaluateAs(
298       @Nullable IItem focus,
299       @NonNull ResultType resultType,
300       @NonNull DynamicContext dynamicContext) {
301     ISequence<?> result = evaluate(focus, dynamicContext);
302     return toResultType(result, resultType);
303   }
304 
305   /**
306    * Converts the provided {@code sequence} to the requested {@code resultType}.
307    * <p>
308    * The {@code resultType} determines the returned result, which is derived from
309    * the evaluation result sequence, as follows:
310    * <ul>
311    * <li>BOOLEAN - the effective boolean result is produced using
312    * {@link FnBoolean#fnBoolean(ISequence)}.</li>
313    * <li>NODE - the first result item in the sequence is returned.</li>
314    * <li>NUMBER - the sequence is cast to a number using
315    * {@link IDecimalItem#cast(IAnyAtomicItem)}.</li>
316    * <li>SEQUENCE - the evaluation result sequence.</li>
317    * <li>STRING - the string value of the first result item in the sequence.</li>
318    * </ul>
319    *
320    * @param <T>
321    *          the requested return value
322    * @param sequence
323    *          the sequence to convert
324    * @param resultType
325    *          the type of result to produce
326    * @return the converted result
327    * @throws TypeMetapathException
328    *           if the provided sequence is incompatible with the requested result
329    *           type
330    */
331   @SuppressWarnings({ "PMD.NullAssignment", "PMD.CyclomaticComplexity" }) // for readability
332   @Nullable
333   protected <T> T toResultType(@NonNull ISequence<?> sequence, @NonNull ResultType resultType) {
334     Object result;
335     switch (resultType) {
336     case BOOLEAN:
337       result = FnBoolean.fnBoolean(sequence).toBoolean();
338       break;
339     case ITEM:
340     case NODE:
341       result = sequence.getFirstItem(true);
342       break;
343     case NUMBER:
344       INumericItem numeric = FunctionUtils.toNumeric(sequence, true);
345       result = numeric == null ? null : numeric.asDecimal();
346       break;
347     case SEQUENCE:
348       result = sequence;
349       break;
350     case STRING:
351       IAnyAtomicItem item = FnData.fnData(sequence).getFirstItem(true);
352       result = item == null ? "" : item.asString();
353       break;
354     default:
355       throw new InvalidTypeMetapathException(null, String.format("unsupported result type '%s'", resultType.name()));
356     }
357 
358     @SuppressWarnings("unchecked") T retval = (T) result;
359     return retval;
360   }
361 
362   /**
363    * Evaluate this Metapath expression without a specific focus.
364    *
365    * @param <T>
366    *          the type of items contained in the resulting sequence
367    * @return a sequence of Metapath items representing the result of the
368    *         evaluation
369    * @throws MetapathException
370    *           if an error occurred during evaluation
371    */
372   @NonNull
373   public <T extends IItem> ISequence<T> evaluate() {
374     return evaluate((IItem) null);
375   }
376 
377   /**
378    * Evaluate this Metapath expression using the provided {@code focus} as the
379    * initial evaluation context.
380    *
381    * @param <T>
382    *          the type of items contained in the resulting sequence
383    * @param focus
384    *          the outer focus of the expression
385    * @return a sequence of Metapath items representing the result of the
386    *         evaluation
387    * @throws MetapathException
388    *           if an error occurred during evaluation
389    */
390   @SuppressWarnings("unchecked")
391   @NonNull
392   public <T extends IItem> ISequence<T> evaluate(
393       @Nullable IItem focus) {
394     return (ISequence<T>) evaluate(focus, new DynamicContext(getStaticContext()));
395   }
396 
397   /**
398    * Evaluate this Metapath expression using the provided {@code focus} as the
399    * initial evaluation context.
400    * <p>
401    * This variant allow for reuse of a provided {@code dynamicContext}.
402    *
403    * @param <T>
404    *          the type of items contained in the resulting sequence
405    * @param focus
406    *          the outer focus of the expression
407    * @param dynamicContext
408    *          the dynamic context to use for evaluation
409    * @return a sequence of Metapath items representing the result of the
410    *         evaluation
411    * @throws MetapathException
412    *           if an error occurred during evaluation
413    */
414   @SuppressWarnings("unchecked")
415   @NonNull
416   public <T extends IItem> ISequence<T> evaluate(
417       @Nullable IItem focus,
418       @NonNull DynamicContext dynamicContext) {
419     try {
420       return (ISequence<T>) getASTNode().accept(dynamicContext, ISequence.of(focus));
421     } catch (MetapathException ex) { // NOPMD - intentional
422       throw new MetapathException(
423           String.format("An error occurred while evaluating the expression '%s'.", getPath()), ex);
424     }
425   }
426 }