AbstractCSTVisitorBase.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.core.metapath.cst;

import gov.nist.secauto.metaschema.core.metapath.StaticContext;
import gov.nist.secauto.metaschema.core.metapath.StaticMetapathException;
import gov.nist.secauto.metaschema.core.metapath.antlr.AbstractAstVisitor;
import gov.nist.secauto.metaschema.core.metapath.antlr.Metapath10.EqnameContext;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;

import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.namespace.QName;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

@SuppressWarnings({
    "PMD.CouplingBetweenObjects"
})
public abstract class AbstractCSTVisitorBase
    extends AbstractAstVisitor<IExpression> {

  private static final Pattern QUALIFIED_NAME_PATTERN = Pattern.compile("^Q\\{([^}]*)\\}(.+)$");

  /**
   * Get the QName for an
   * <a href="https://www.w3.org/TR/xpath-31/#dt-expanded-qname">expanded
   * QName</a>.
   *
   * @param eqname
   *          the expanded QName
   * @param context
   *          the Metapath evaluation static context
   * @param requireNamespace
   *          if {@code true} require the resulting QName to have a namespace, or
   *          {@code false} otherwise
   * @return the QName
   * @throws StaticMetapathException
   *           if the expanded QName prefix is not bound or if the resulting
   *           namespace is invalid
   */
  @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity" })
  @NonNull
  static QName toQName(@NonNull EqnameContext eqname, @NonNull StaticContext context, boolean requireNamespace) {
    String namespaceUri;
    String localName;
    TerminalNode node;
    if ((node = eqname.URIQualifiedName()) != null) {
      // BracedURILiteral - Q{uri}name -
      // https://www.w3.org/TR/xpath-31/#doc-xpath31-BracedURILiteral
      Matcher matcher = QUALIFIED_NAME_PATTERN.matcher(node.getText());
      if (matcher.matches()) {
        namespaceUri = matcher.group(1);
        localName = matcher.group(2);
      } else {
        // the syntax should always match above, since ANTLR is parsing it
        throw new IllegalStateException();
      }
    } else {
      String prefix;
      String[] tokens = eqname.getText().split(":", 2);
      if (tokens.length == 2) {
        // lexical QName with prefix - prefix:name
        // https://www.w3.org/TR/xpath-31/#dt-qname
        prefix = ObjectUtils.notNull(tokens[0]);
        localName = tokens[1];
      } else {
        // lexical QName without prefix - name
        // https://www.w3.org/TR/xpath-31/#dt-qname
        prefix = "";
        localName = tokens[0];
      }
      namespaceUri = context.lookupNamespaceForPrefix(prefix);
      if (namespaceUri == null && requireNamespace) {
        throw new StaticMetapathException(
            StaticMetapathException.PREFIX_NOT_EXPANDABLE,
            String.format("The static context does not have a namespace URI configured for prefix '%s'.", prefix));
      }
    }

    QName retval;
    if (namespaceUri == null) {
      retval = new QName(localName);
    } else {
      if ("http://www.w3.org/2000/xmlns/".equals(namespaceUri)) {
        throw new StaticMetapathException(StaticMetapathException.NAMESPACE_MISUSE,
            "The namespace of an expanded QName cannot be: http://www.w3.org/2000/xmlns/");
      }
      retval = new QName(namespaceUri, localName);
    }
    return retval;
  }

  @SuppressWarnings("null")
  @Override
  @NonNull
  public IExpression visit(ParseTree tree) {
    assert tree != null;
    return super.visit(tree);
  }

  /**
   * Parse the provided context as an n-ary phrase.
   *
   * @param <CONTEXT>
   *          the Java type of the antlr context to parse
   * @param <T>
   *          the Java type of the child expressions produced by this parser
   * @param <R>
   *          the Java type of the outer expression produced by the parser
   * @param context
   *          the antlr context to parse
   * @param startIndex
   *          the child index to start parsing on
   * @param step
   *          the increment to advance while parsing child expressions
   * @param parser
   *          a binary function used to produce child expressions
   * @param supplier
   *          a function used to produce the other expression
   * @return the outer expression or {@code null} if no children exist to parse
   */
  @Nullable
  protected <CONTEXT extends ParserRuleContext, T extends IExpression, R extends IExpression>
      R nairyToCollection(
          @NonNull CONTEXT context,
          int startIndex,
          int step,
          @NonNull BiFunction<CONTEXT, Integer, T> parser,
          @NonNull Function<List<T>, R> supplier) {
    int numChildren = context.getChildCount();

    R retval = null;
    if (startIndex < numChildren) {
      List<T> children = new ArrayList<>((numChildren - startIndex) / step);
      for (int idx = startIndex; idx < numChildren; idx += step) {
        T result = parser.apply(context, idx);
        children.add(result);
      }
      retval = supplier.apply(children);
    }
    return retval;
  }

  /**
   * Parse the provided context as an n-ary phrase, which will be one of the
   * following.
   * <ol>
   * <li>A single <code>expr</code> for which that expr will be returned</li>
   * <li><code>left (operator right)*</code> for which a collection of the left
   * and right members will be returned based on what is provided by the
   * supplier.</li>
   * </ol>
   *
   * @param <CONTEXT>
   *          the context type to parse
   * @param <NODE>
   *          the type of expression
   * @param context
   *          the context instance
   * @param supplier
   *          a supplier that will instantiate an expression based on the provided
   *          parsed collection
   * @return the left expression or the supplied expression for a collection
   */
  @NonNull
  protected <CONTEXT extends ParserRuleContext, NODE extends IExpression> IExpression
      handleNAiryCollection(
          @NonNull CONTEXT context,
          @NonNull Function<List<NODE>, IExpression> supplier) {
    return handleNAiryCollection(context, 1, 2, (ctx, idx) -> {
      // skip operator, since we know what it is
      ParseTree tree = ctx.getChild(idx + 1);
      @SuppressWarnings({ "unchecked", "null" })
      @NonNull NODE node = (NODE) tree.accept(this);
      return node;
    }, supplier);
  }

  /**
   * Parse the provided context as an n-ary phrase, which will be one of the
   * following.
   * <ol>
   * <li><code>expr</code> for which the expr will be returned.</li>
   * <li><code>left</code> plus a number of additional recurring tokens as defined
   * by the <em>step</em>.</li>
   * </ol>
   * <p>
   * In the second case, the supplier will be used to generate an expression from
   * the collection of tuples.
   *
   * @param <CONTEXT>
   *          the context type to parse
   * @param <EXPRESSION>
   *          the child expression type
   * @param context
   *          the context instance
   * @param startIndex
   *          the starting context child position
   * @param step
   *          the amount to advance the loop over the context children
   * @param parser
   *          a binary function used to parse the context children
   * @param supplier
   *          a supplier that will instantiate an expression based on the provided
   *          collection
   * @return the left expression or the supplied expression for a collection
   */
  @NonNull
  protected <CONTEXT extends ParserRuleContext, EXPRESSION extends IExpression> IExpression
      handleNAiryCollection(
          @NonNull CONTEXT context,
          int startIndex,
          int step,
          @NonNull BiFunction<CONTEXT, Integer, EXPRESSION> parser,
          @NonNull Function<List<EXPRESSION>, IExpression> supplier) {
    int numChildren = context.getChildCount();

    if (numChildren == 0) {
      throw new IllegalStateException("there should always be a child expression");
    } else if (startIndex > numChildren) {
      throw new IllegalStateException("Start index is out of bounds");
    }

    ParseTree leftTree = context.getChild(0);
    @SuppressWarnings({ "unchecked", "null" })
    @NonNull EXPRESSION leftResult = (EXPRESSION) leftTree.accept(this);

    IExpression retval;
    if (numChildren == 1) {
      retval = leftResult;
    } else {
      List<EXPRESSION> children = new ArrayList<>(numChildren - 1 / step);
      children.add(leftResult);
      for (int i = startIndex; i < numChildren; i = i + step) {
        EXPRESSION result = parser.apply(context, i);
        children.add(result);
      }
      IExpression result = ObjectUtils.notNull(supplier.apply(children));
      retval = result;
    }
    return retval;
  }

  /**
   * Parse the provided context as a simple n-ary phrase, which will be one of the
   * following.
   * <ol>
   * <li><code>expr</code> for which the expr will be returned</li>
   * <li><code>left (operator right)*</code> for which a collection of the left
   * and right members will be returned based on what is provided by the supplier.
   * </ol>
   * <p>
   * In the second case, the supplier will be used to generate an expression from
   * the collection of tuples.
   *
   * @param <CONTEXT>
   *          the context type to parse
   * @param context
   *          the context instance
   * @param startingIndex
   *          the index of the first child expression, which must be a
   *          non-negative value that is less than the number of children
   * @param step
   *          the amount to advance the loop over the context children
   * @param parser
   *          a trinary function used to parse the context children and supply a
   *          result
   * @return the left expression or the supplied expression
   */
  protected <CONTEXT extends ParserRuleContext> IExpression handleGroupedNAiry(
      @NonNull CONTEXT context,
      int startingIndex,
      int step,
      @NonNull ITriFunction<CONTEXT, Integer, IExpression, IExpression> parser) {
    int numChildren = context.getChildCount();
    if (startingIndex >= numChildren) {
      throw new IndexOutOfBoundsException(
          String.format("The starting index '%d' exceeds the child count '%d'",
              startingIndex,
              numChildren));
    }

    IExpression retval = null;
    if (numChildren > 0) {
      ParseTree leftTree = context.getChild(startingIndex);
      retval = ObjectUtils.notNull(leftTree.accept(this));

      for (int i = startingIndex + 1; i < numChildren; i = i + step) {
        retval = parser.apply(context, i, retval);
      }
    }
    return retval;
  }

  @FunctionalInterface
  interface ITriFunction<T, U, V, R> {

    R apply(T argT, U argU, V argV);

    default <W> ITriFunction<T, U, V, W> andThen(Function<? super R, ? extends W> after) {
      Objects.requireNonNull(after);
      return (T t, U u, V v) -> after.apply(apply(t, u, v));
    }
  }
}