001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.metapath;
028
029import gov.nist.secauto.metaschema.core.metapath.antlr.metapath10Lexer;
030import gov.nist.secauto.metaschema.core.metapath.antlr.metapath10Parser;
031import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils;
032import gov.nist.secauto.metaschema.core.metapath.function.library.FnBoolean;
033import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
034import gov.nist.secauto.metaschema.core.metapath.item.IItem;
035import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
036import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDecimalItem;
037import gov.nist.secauto.metaschema.core.metapath.item.atomic.INumericItem;
038import gov.nist.secauto.metaschema.core.util.ObjectUtils;
039
040import org.antlr.v4.runtime.CharStreams;
041import org.antlr.v4.runtime.CommonTokenStream;
042import org.antlr.v4.runtime.misc.ParseCancellationException;
043import org.antlr.v4.runtime.tree.ParseTree;
044import org.apache.logging.log4j.LogManager;
045import org.apache.logging.log4j.Logger;
046
047import java.io.ByteArrayOutputStream;
048import java.io.IOException;
049import java.io.PrintStream;
050import java.nio.charset.StandardCharsets;
051import java.util.Arrays;
052
053import edu.umd.cs.findbugs.annotations.NonNull;
054import edu.umd.cs.findbugs.annotations.Nullable;
055
056public class MetapathExpression {
057
058  public enum ResultType {
059    NUMBER,
060    STRING,
061    BOOLEAN,
062    SEQUENCE,
063    NODE;
064  }
065
066  private static final Logger LOGGER = LogManager.getLogger(MetapathExpression.class);
067
068  @NonNull
069  public static final MetapathExpression CONTEXT_NODE = new MetapathExpression(".", ContextItem.instance());
070
071  private final String path;
072  @NonNull
073  private final IExpression node;
074
075  /**
076   * Compiles a Metapath expression string.
077   *
078   * @param path
079   *          the metapath expression
080   * @return the compiled expression object
081   * @throws MetapathException
082   *           if an error occurred while compiling the Metapath expression
083   */
084  @NonNull
085  public static MetapathExpression compile(@NonNull String path) {
086    @NonNull MetapathExpression retval;
087    if (".".equals(path)) {
088      retval = CONTEXT_NODE;
089    } else {
090      try {
091        metapath10Lexer lexer = new metapath10Lexer(CharStreams.fromString(path));
092        CommonTokenStream tokens = new CommonTokenStream(lexer);
093        metapath10Parser parser = new metapath10Parser(tokens);
094        parser.removeErrorListeners();
095        parser.addErrorListener(new FailingErrorListener());
096
097        ParseTree tree = ObjectUtils.notNull(parser.expr());
098
099        if (LOGGER.isDebugEnabled()) {
100          try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
101            try (PrintStream ps = new PrintStream(os, true, StandardCharsets.UTF_8)) {
102              CSTPrinter printer = new CSTPrinter(ps);
103              printer.print(tree, Arrays.asList(metapath10Parser.ruleNames));
104              ps.flush();
105            }
106            LOGGER.atDebug().log(String.format("Metapath CST:%n%s", os.toString(StandardCharsets.UTF_8)));
107          } catch (IOException ex) {
108            LOGGER.atError().withThrowable(ex).log("An unexpected error occured while closing the steam.");
109          }
110        }
111
112        IExpression expr = new BuildAstVisitor().visit(tree);
113
114        if (LOGGER.isDebugEnabled()) {
115          LOGGER.atDebug().log(String.format("Metapath AST:%n%s", ASTPrinter.instance().visit(expr)));
116        }
117        retval = new MetapathExpression(path, expr);
118      } catch (MetapathException | ParseCancellationException ex) {
119        String msg = String.format("Unable to compile Metapath '%s'", path);
120        LOGGER.atError().withThrowable(ex).log(msg);
121        throw new MetapathException(msg, ex);
122      }
123    }
124    return retval;
125  }
126
127  /**
128   * Construct a new Metapath expression.
129   *
130   * @param path
131   *          the Metapath as a string
132   * @param expr
133   *          the Metapath as a compiled abstract syntax tree (AST)
134   */
135  protected MetapathExpression(@NonNull String path, @NonNull IExpression expr) {
136    this.path = path;
137    this.node = expr;
138  }
139
140  /**
141   * Get the original Metapath expression as a string.
142   *
143   * @return the expression
144   */
145  public String getPath() {
146    return path;
147  }
148
149  /**
150   * Get the compiled abstract syntax tree (AST) representation of the Metapath.
151   *
152   * @return the Metapath AST
153   */
154  @NonNull
155  protected IExpression getASTNode() {
156    return node;
157  }
158
159  @Override
160  public String toString() {
161    return ASTPrinter.instance().visit(getASTNode());
162  }
163
164  /**
165   * Evaluate this Metapath expression without a specific focus. The required
166   * result type will be determined by the {@code resultType} argument.
167   *
168   * @param <T>
169   *          the expected result type
170   * @param resultType
171   *          the type of result to produce
172   * @return the converted result
173   * @throws TypeMetapathException
174   *           if the provided sequence is incompatible with the requested result
175   *           type
176   * @throws MetapathException
177   *           if an error occurred during evaluation
178   * @see #toResultType(ISequence, ResultType)
179   */
180  @Nullable
181  public <T> T evaluateAs(@NonNull ResultType resultType) {
182    return evaluateAs(null, resultType);
183  }
184
185  /**
186   * Evaluate this Metapath expression using the provided {@code focus} as the
187   * initial evaluation context. The required result type will be determined by
188   * the {@code resultType} argument.
189   *
190   * @param <T>
191   *          the expected result type
192   * @param focus
193   *          the outer focus of the expression
194   * @param resultType
195   *          the type of result to produce
196   * @return the converted result
197   * @throws TypeMetapathException
198   *           if the provided sequence is incompatible with the requested result
199   *           type
200   * @throws MetapathException
201   *           if an error occurred during evaluation
202   * @see #toResultType(ISequence, ResultType)
203   */
204  @Nullable
205  public <T> T evaluateAs(
206      @Nullable IItem focus,
207      @NonNull ResultType resultType) {
208    ISequence<?> result = evaluate(focus);
209    return toResultType(result, resultType);
210  }
211
212  /**
213   * Evaluate this Metapath expression using the provided {@code focus} as the
214   * initial evaluation context. The specific result type will be determined by
215   * the {@code resultType} argument.
216   * <p>
217   * This variant allow for reuse of a provided {@code dynamicContext}.
218   *
219   * @param <T>
220   *          the expected result type
221   * @param focus
222   *          the outer focus of the expression
223   * @param resultType
224   *          the type of result to produce
225   * @param dynamicContext
226   *          the dynamic context to use for evaluation
227   * @return the converted result
228   * @throws TypeMetapathException
229   *           if the provided sequence is incompatible with the requested result
230   *           type
231   * @throws MetapathException
232   *           if an error occurred during evaluation
233   * @see #toResultType(ISequence, ResultType)
234   */
235  @Nullable
236  public <T> T evaluateAs(
237      @NonNull IItem focus,
238      @NonNull ResultType resultType,
239      @NonNull DynamicContext dynamicContext) {
240    ISequence<?> result = evaluate(focus, dynamicContext);
241    return toResultType(result, resultType);
242  }
243
244  /**
245   * Converts the provided {@code sequence} to the requested {@code resultType}.
246   * <p>
247   * The {@code resultType} determines the returned result, which is derived from
248   * the evaluation result sequence, as follows:
249   * <ul>
250   * <li>BOOLEAN - the effective boolean result is produced using
251   * {@link FnBoolean#fnBoolean(ISequence)}.</li>
252   * <li>NODE - the first result item in the sequence is returned.</li>
253   * <li>NUMBER - the sequence is cast to a number using
254   * {@link IDecimalItem#cast(IAnyAtomicItem)}.</li>
255   * <li>SEQUENCE - the evaluation result sequence.</li>
256   * <li>STRING - the string value of the first result item in the sequence.</li>
257   * </ul>
258   *
259   * @param <T>
260   *          the requested return value
261   * @param sequence
262   *          the sequence to convert
263   * @param resultType
264   *          the type of result to produce
265   * @return the converted result
266   * @throws TypeMetapathException
267   *           if the provided sequence is incompatible with the requested result
268   *           type
269   */
270  @SuppressWarnings("PMD.NullAssignment") // for readability
271  @Nullable
272  protected <T> T toResultType(@NonNull ISequence<?> sequence, @NonNull ResultType resultType) {
273    Object result;
274    switch (resultType) {
275    case BOOLEAN:
276      result = FnBoolean.fnBoolean(sequence).toBoolean();
277      break;
278    case NODE:
279      result = FunctionUtils.getFirstItem(sequence, true);
280      break;
281    case NUMBER:
282      INumericItem numeric = FunctionUtils.toNumeric(sequence, true);
283      result = numeric == null ? null : numeric.asDecimal();
284      break;
285    case SEQUENCE:
286      result = sequence;
287      break;
288    case STRING:
289      IItem item = FunctionUtils.getFirstItem(sequence, true);
290      result = item == null ? "" : FnData.fnDataItem(item).asString();
291      break;
292    default:
293      throw new InvalidTypeMetapathException(null, String.format("unsupported result type '%s'", resultType.name()));
294    }
295
296    @SuppressWarnings("unchecked") T retval = (T) result;
297    return retval;
298  }
299
300  /**
301   * Evaluate this Metapath expression without a specific focus.
302   *
303   * @param <T>
304   *          the type of items contained in the resulting sequence
305   * @return a sequence of Metapath items representing the result of the
306   *         evaluation
307   * @throws MetapathException
308   *           if an error occurred during evaluation
309   */
310  @NonNull
311  public <T extends IItem> ISequence<T> evaluate() {
312    return evaluate(null);
313  }
314
315  /**
316   * Evaluate this Metapath expression using the provided {@code focus} as the
317   * initial evaluation context.
318   *
319   * @param <T>
320   *          the type of items contained in the resulting sequence
321   * @param focus
322   *          the outer focus of the expression
323   * @return a sequence of Metapath items representing the result of the
324   *         evaluation
325   * @throws MetapathException
326   *           if an error occurred during evaluation
327   */
328  @SuppressWarnings("unchecked")
329  @NonNull
330  public <T extends IItem> ISequence<T> evaluate(
331      @Nullable IItem focus) {
332    return (ISequence<T>) evaluate(
333        focus,
334        StaticContext.builder()
335            .build().newDynamicContext());
336  }
337
338  /**
339   * Evaluate this Metapath expression using the provided {@code focus} as the
340   * initial evaluation context.
341   * <p>
342   * This variant allow for reuse of a provided {@code dynamicContext}.
343   *
344   * @param <T>
345   *          the type of items contained in the resulting sequence
346   * @param focus
347   *          the outer focus of the expression
348   * @param dynamicContext
349   *          the dynamic context to use for evaluation
350   * @return a sequence of Metapath items representing the result of the
351   *         evaluation
352   * @throws MetapathException
353   *           if an error occurred during evaluation
354   */
355  @SuppressWarnings("unchecked")
356  @NonNull
357  public <T extends IItem> ISequence<T> evaluate(
358      @Nullable IItem focus,
359      @NonNull DynamicContext dynamicContext) {
360    try {
361      return (ISequence<T>) getASTNode().accept(dynamicContext, ISequence.of(focus));
362    } catch (MetapathException ex) { // NOPMD - intentional
363      throw new MetapathException(
364          String.format("An error occurred while evaluating the expression '%s'.", getPath()), ex);
365    }
366  }
367}