All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.search.query.QueryTreeWalker Maven / Gradle / Ivy

/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.appengine.api.search.query;

import com.google.appengine.api.search.SearchQueryException;
import com.google.common.collect.ImmutableSet;
import org.antlr.runtime.tree.Tree;

/**
 * The walking of the query tree. This class takes care of visiting
 * a tree resulting from parsing a query. As it traverses the tree
 * it calls appropriate methods of the visitor, set at the construction
 * time. The class uses a depth-first search, visiting all children
 * of a node, before visiting the node. The visit is done by calling
 * an appropriate method of the visitor. Typical code should match
 * the following pattern:
 * 
{@code
 * class MyVisitor implements QueryTreeVisitor {
 *   ...
 * }
 * class MyContext extends QueryTreeContext {
 *   ...
 *   @Override
 *   protected MyContext newChildContext() {
 *     return new MyContext();
 *   }
 * }
 *
 * MyContext context = new MyContext();
 * QueryTreeWalker walker = new QueryTreeWalker(new MyVisitor());
 * Tree root = parser.query(queryStr);
 * walker.walk(root, context);
 * // retrieve whatever information you need from context
 * }
* * @param the context used by the visitor */ public class QueryTreeWalker> { private final QueryTreeVisitor visitor; /** * Creates a new query walker that calls the given {@code visitor}. * * @param visitor the visitor to be called by this walker */ public QueryTreeWalker(QueryTreeVisitor visitor) { this.visitor = visitor; } /** * @param tree the tree to be walked * @param context the context in which the tree is walked */ public void walk(Tree tree, T context) throws QueryTreeException { tree = flatten(tree, null); validate(tree); walkInternal(tree, context); } /** * Walks the tree and updates the context. This is a depth-first search, * always exploring children of the {@code tree} in the order of their * index 0 ... n - 1, where n is the number of children. Once all children * of the {@code node} are visited, the appropriate visitor's method * is called * * @param tree the tree to be walked * @param context the context of the visit */ private void walkInternal(Tree tree, T context) { switch (tree.getType()) { case QueryLexer.CONJUNCTION: context.setInDisjunction(false); walkChildren(tree, context); visitor.visitConjunction(tree, context); postBooleanExpression(context); break; case QueryLexer.DISJUNCTION: context.setInDisjunction(true); walkChildren(tree, context); visitor.visitDisjunction(tree, context); postBooleanExpression(context); break; case QueryLexer.SEQUENCE: context.setInDisjunction(false); walkChildren(tree, context); visitor.visitSequence(tree, context); postBooleanExpression(context); break; case QueryLexer.NEGATION: walkChildren(tree, context); visitor.visitNegation(tree, context); postBooleanExpression(context); break; case QueryLexer.HAS: walkChildren(tree, context); visitor.visitContains(tree, context); postBooleanExpression(context); break; case QueryLexer.EQ: walkChildren(tree, context); visitor.visitEqual(tree, context); postBooleanExpression(context); break; case QueryLexer.NE: throw new SearchQueryException("!= comparison operator is not available"); case QueryLexer.LESSTHAN: walkChildren(tree, context); visitor.visitLessThan(tree, context); postBooleanExpression(context); break; case QueryLexer.LE: walkChildren(tree, context); visitor.visitLessOrEqual(tree, context); postBooleanExpression(context); break; case QueryLexer.GT: walkChildren(tree, context); visitor.visitGreaterThan(tree, context); postBooleanExpression(context); break; case QueryLexer.GE: walkChildren(tree, context); visitor.visitGreaterOrEqual(tree, context); postBooleanExpression(context); break; case QueryLexer.FUZZY: walkInternal(tree.getChild(0), context); visitor.visitFuzzy(tree, context); // Type and kind are set by the visitor. break; case QueryLexer.LITERAL: walkInternal(tree.getChild(0), context); visitor.visitLiteral(tree, context); // Type and kind are set by the visitor. break; case QueryLexer.VALUE: visitor.visitValue(tree, context); // Type and kind are set by the visitor. break; case QueryLexer.FUNCTION: walkChildren(tree.getChild(1), context); visitor.visitFunction(tree, context); // Type and kind are set by the visitor. break; case QueryLexer.GLOBAL: visitor.visitGlobal(tree, context); break; default: visitor.visitOther(tree, context); } } protected void postBooleanExpression(T context) { context.setReturnType(QueryTreeContext.Type.BOOL); context.setKind(QueryTreeContext.Kind.EXPRESSION); } private void walkChildren(Tree parent, T context) { for (int i = 0; i < parent.getChildCount(); ++i) { walkInternal(parent.getChild(i), context.addChild()); } } private static final ImmutableSet QUERY_FUNCTION_NAMES = ImmutableSet.of("distance", "geopoint"); /** * Basic stateless query tree validation. * * @param tree parsed query tree to validate * @throws QueryTreeException if the tree is invalid */ private static void validate(Tree tree) throws QueryTreeException { for (int i = 0; i < tree.getChildCount(); ++i) { validate(tree.getChild(i)); } switch (tree.getType()) { case QueryLexer.FUNCTION: Tree name = tree.getChild(0); if (!QUERY_FUNCTION_NAMES.contains(name.getText())) { throw new QueryTreeException("unknown function '" + name.getText() + "'", name.getCharPositionInLine()); } break; default: break; } } public static Tree simplify(Tree tree) { for (int i = 0; i < tree.getChildCount(); ++i) { Tree child = tree.getChild(i); Tree optimized = simplify(child); if (child != optimized) { tree.setChild(i, optimized); } } switch (tree.getType()) { case QueryLexer.CONJUNCTION: case QueryLexer.DISJUNCTION: case QueryLexer.SEQUENCE: if (tree.getChildCount() == 1) { return tree.getChild(0); } break; } return tree; } /** * Flattens the tree by pushing down the field name. For example, if * the tree looks like this: *
{@code
   *                EQ
   *             /      \
   *        VALUE         EQ
   *        /    \      /    \
   *      TEXT  field GLOBAL (N)
   * }
* Then we will output tree that looks like this: *
{@code
   *                EQ
   *             /      \
   *        VALUE       (N)
   *        /    \
   *      TEXT  field
   * }
* Here (N) is an arbitrary node. We also drop EQ if it * is in front of conjunction or disjunction. We do not drop it for * other comparators, as we want parsing to fail for foo < (1 2). */ private static Tree flatten(Tree tree, Tree restriction) throws QueryTreeException { if (tree.getType() == QueryLexer.VALUE) { return tree; } if (tree.getType() == QueryLexer.HAS || tree.getType() == QueryLexer.EQ) { Tree lhs = tree.getChild(0); if (lhs.getType() == QueryLexer.VALUE) { String myField = lhs.getChild(1).getText(); if (restriction == null) { restriction = lhs; } else { String otherField = restriction.getChild(1).getText(); if (!myField.equals(otherField)) { throw new QueryTreeException( String.format("Restriction on %s and %s", otherField, myField), lhs.getChild(1).getCharPositionInLine()); } } } Tree rhs = tree.getChild(1); Tree flattened = flatten(rhs, restriction); if (flattened.getType() == QueryLexer.HAS || flattened.getType() == QueryLexer.EQ || flattened.getType() == QueryLexer.CONJUNCTION || flattened.getType() == QueryLexer.DISJUNCTION || flattened.getType() == QueryLexer.SEQUENCE) { return flattened; } if (flattened != rhs) { tree.setChild(1, flattened); } if (restriction != lhs) { tree.setChild(0, restriction); } return tree; } for (int i = 0; i < tree.getChildCount(); ++i) { Tree original = tree.getChild(i); Tree flattened = flatten(tree.getChild(i), restriction); if (original != flattened) { tree.setChild(i, flattened); } } return tree; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy