blob: a415898eea48b83c88b05c859b2988b61daaf261 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* http://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 org.apache.accumulo.examples.wikisearch.parser;
import java.io.StringReader;
import org.apache.accumulo.examples.wikisearch.parser.QueryParser.EvaluationContext;
import org.apache.accumulo.examples.wikisearch.parser.QueryParser.FunctionResult;
import org.apache.accumulo.examples.wikisearch.parser.QueryParser.LiteralResult;
import org.apache.accumulo.examples.wikisearch.parser.QueryParser.ObjectHolder;
import org.apache.accumulo.examples.wikisearch.parser.QueryParser.QueryTerm;
import org.apache.accumulo.examples.wikisearch.parser.QueryParser.TermResult;
import org.apache.commons.jexl2.parser.ASTAdditiveNode;
import org.apache.commons.jexl2.parser.ASTAdditiveOperator;
import org.apache.commons.jexl2.parser.ASTAmbiguous;
import org.apache.commons.jexl2.parser.ASTAndNode;
import org.apache.commons.jexl2.parser.ASTArrayAccess;
import org.apache.commons.jexl2.parser.ASTArrayLiteral;
import org.apache.commons.jexl2.parser.ASTAssignment;
import org.apache.commons.jexl2.parser.ASTBitwiseAndNode;
import org.apache.commons.jexl2.parser.ASTBitwiseComplNode;
import org.apache.commons.jexl2.parser.ASTBitwiseOrNode;
import org.apache.commons.jexl2.parser.ASTBitwiseXorNode;
import org.apache.commons.jexl2.parser.ASTBlock;
import org.apache.commons.jexl2.parser.ASTConstructorNode;
import org.apache.commons.jexl2.parser.ASTDivNode;
import org.apache.commons.jexl2.parser.ASTEQNode;
import org.apache.commons.jexl2.parser.ASTERNode;
import org.apache.commons.jexl2.parser.ASTEmptyFunction;
import org.apache.commons.jexl2.parser.ASTFalseNode;
import org.apache.commons.jexl2.parser.ASTFloatLiteral;
import org.apache.commons.jexl2.parser.ASTForeachStatement;
import org.apache.commons.jexl2.parser.ASTFunctionNode;
import org.apache.commons.jexl2.parser.ASTGENode;
import org.apache.commons.jexl2.parser.ASTGTNode;
import org.apache.commons.jexl2.parser.ASTIdentifier;
import org.apache.commons.jexl2.parser.ASTIfStatement;
import org.apache.commons.jexl2.parser.ASTIntegerLiteral;
import org.apache.commons.jexl2.parser.ASTJexlScript;
import org.apache.commons.jexl2.parser.ASTLENode;
import org.apache.commons.jexl2.parser.ASTLTNode;
import org.apache.commons.jexl2.parser.ASTMapEntry;
import org.apache.commons.jexl2.parser.ASTMapLiteral;
import org.apache.commons.jexl2.parser.ASTMethodNode;
import org.apache.commons.jexl2.parser.ASTModNode;
import org.apache.commons.jexl2.parser.ASTMulNode;
import org.apache.commons.jexl2.parser.ASTNENode;
import org.apache.commons.jexl2.parser.ASTNRNode;
import org.apache.commons.jexl2.parser.ASTNotNode;
import org.apache.commons.jexl2.parser.ASTNullLiteral;
import org.apache.commons.jexl2.parser.ASTOrNode;
import org.apache.commons.jexl2.parser.ASTReference;
import org.apache.commons.jexl2.parser.ASTSizeFunction;
import org.apache.commons.jexl2.parser.ASTSizeMethod;
import org.apache.commons.jexl2.parser.ASTStringLiteral;
import org.apache.commons.jexl2.parser.ASTTernaryNode;
import org.apache.commons.jexl2.parser.ASTTrueNode;
import org.apache.commons.jexl2.parser.ASTUnaryMinusNode;
import org.apache.commons.jexl2.parser.ASTWhileStatement;
import org.apache.commons.jexl2.parser.JexlNode;
import org.apache.commons.jexl2.parser.ParseException;
import org.apache.commons.jexl2.parser.Parser;
import org.apache.commons.jexl2.parser.ParserVisitor;
import org.apache.commons.jexl2.parser.SimpleNode;
import com.google.common.collect.Multimap;
/**
* Class that parses the query and returns a tree of TreeNode's. This class rolls up clauses that
* are below like conjunctions (AND, OR) for the purposes of creating intersecting iterators.
*
*/
public class TreeBuilder implements ParserVisitor {
class RootNode extends JexlNode {
public RootNode(int id) {
super(id);
}
public RootNode(Parser p, int id) {
super(p, id);
}
}
private TreeNode rootNode = null;
private TreeNode currentNode = null;
private boolean currentlyInCheckChildren = false;
public TreeBuilder(String query) throws ParseException {
Parser p = new Parser(new StringReader(";"));
ASTJexlScript script = p.parse(new StringReader(query), null);
// Check to see if the child node is an AND or OR. If not, then
// there must be just a single value in the query expression
rootNode = new TreeNode();
rootNode.setType(RootNode.class);
currentNode = rootNode;
EvaluationContext ctx = new EvaluationContext();
script.childrenAccept(this, ctx);
}
public TreeBuilder(ASTJexlScript script) {
// Check to see if the child node is an AND or OR. If not, then
// there must be just a single value in the query expression
rootNode = new TreeNode();
rootNode.setType(RootNode.class);
currentNode = rootNode;
EvaluationContext ctx = new EvaluationContext();
script.childrenAccept(this, ctx);
}
public TreeNode getRootNode() {
return this.rootNode;
}
@Override
public Object visit(SimpleNode node, Object data) {
return null;
}
@Override
public Object visit(ASTJexlScript node, Object data) {
return null;
}
@Override
public Object visit(ASTBlock node, Object data) {
return null;
}
@Override
public Object visit(ASTAmbiguous node, Object data) {
return null;
}
@Override
public Object visit(ASTIfStatement node, Object data) {
return null;
}
@Override
public Object visit(ASTWhileStatement node, Object data) {
return null;
}
@Override
public Object visit(ASTForeachStatement node, Object data) {
return null;
}
@Override
public Object visit(ASTAssignment node, Object data) {
return null;
}
@Override
public Object visit(ASTTernaryNode node, Object data) {
return null;
}
/**
* @return false if any of the nodes equals the fail class or contain a NOT in the subtree
*/
private boolean nodeCheck(JexlNode node, Class<?> failClass) {
if (node.getClass().equals(failClass) || node.getClass().equals(ASTNotNode.class)) {
return false;
} else {
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
if (!nodeCheck(node.jjtGetChild(i), failClass)) {
return false;
}
}
}
return true;
}
/**
* Checks to see if all of the child nodes are of the same type (AND/OR) and if so then aggregates
* all of the child terms. If not returns null.
*
* @return Map of field names to query terms or null
*/
private Multimap<String,QueryTerm> checkChildren(JexlNode parent, EvaluationContext ctx) {
// If the current node is an AND, then make sure that there is no
// OR descendant node, and vice versa. If this is true, then we call
// roll up all of the descendent values.
this.currentlyInCheckChildren = true;
Multimap<String,QueryTerm> rolledUpTerms = null;
boolean result = false;
if (parent.getClass().equals(ASTOrNode.class)) {
for (int i = 0; i < parent.jjtGetNumChildren(); i++) {
result = nodeCheck(parent.jjtGetChild(i), ASTAndNode.class);
if (!result) {
break;
}
}
} else {
for (int i = 0; i < parent.jjtGetNumChildren(); i++) {
result = nodeCheck(parent.jjtGetChild(i), ASTOrNode.class);
if (!result) {
break;
}
}
}
if (result) {
// Set current node to a fake node and
// roll up the children from this node using the visitor pattern.
TreeNode rollupFakeNode = new TreeNode();
TreeNode previous = this.currentNode;
this.currentNode = rollupFakeNode;
// Run the visitor with the fake node.
parent.childrenAccept(this, ctx);
// Get the terms from the fake node
rolledUpTerms = this.currentNode.getTerms();
// Reset the current node pointer
this.currentNode = previous;
}
this.currentlyInCheckChildren = false;
return rolledUpTerms;
}
@Override
public Object visit(ASTOrNode node, Object data) {
boolean previouslyInOrContext = false;
EvaluationContext ctx = null;
if (null != data && data instanceof EvaluationContext) {
ctx = (EvaluationContext) data;
previouslyInOrContext = ctx.inOrContext;
} else {
ctx = new EvaluationContext();
}
ctx.inOrContext = true;
// Are we being called from the checkChildren method? If so, then we
// are rolling up terms. If not, then we need to call check children.
if (currentlyInCheckChildren) {
// Process both sides of this node.
node.jjtGetChild(0).jjtAccept(this, data);
node.jjtGetChild(1).jjtAccept(this, data);
} else {
// Create a new OR node under the current node.
TreeNode orNode = new TreeNode();
orNode.setType(ASTOrNode.class);
orNode.setParent(this.currentNode);
this.currentNode.getChildren().add(orNode);
Multimap<String,QueryTerm> terms = checkChildren(node, ctx);
if (terms == null) {
// Then there was no rollup, set the current node to the orNode
// and process the children. Be sure to set the current Node to
// the or node in between calls because we could be processing
// an AND node below and the current node will have been switched.
// Process both sides of this node.
currentNode = orNode;
node.jjtGetChild(0).jjtAccept(this, data);
currentNode = orNode;
node.jjtGetChild(1).jjtAccept(this, data);
} else {
// There was a rollup, don't process the children and set the terms
// on the or node.
orNode.setTerms(terms);
}
}
// reset the state
if (null != data && !previouslyInOrContext) {
ctx.inOrContext = false;
}
return null;
}
@Override
public Object visit(ASTAndNode node, Object data) {
boolean previouslyInAndContext = false;
EvaluationContext ctx = null;
if (null != data && data instanceof EvaluationContext) {
ctx = (EvaluationContext) data;
previouslyInAndContext = ctx.inAndContext;
} else {
ctx = new EvaluationContext();
}
ctx.inAndContext = true;
// Are we being called from the checkChildren method? If so, then we
// are rolling up terms. If not, then we need to call check children.
if (currentlyInCheckChildren) {
// Process both sides of this node.
node.jjtGetChild(0).jjtAccept(this, data);
node.jjtGetChild(1).jjtAccept(this, data);
} else {
// Create a new And node under the current node.
TreeNode andNode = new TreeNode();
andNode.setType(ASTAndNode.class);
andNode.setParent(this.currentNode);
this.currentNode.getChildren().add(andNode);
Multimap<String,QueryTerm> terms = checkChildren(node, ctx);
if (terms == null) {
// Then there was no rollup, set the current node to the orNode
// and process the children. Be sure to set the current Node to
// the and node in between calls because we could be processing
// an OR node below and the current node will have been switched.
// Process both sides of this node.
currentNode = andNode;
node.jjtGetChild(0).jjtAccept(this, data);
currentNode = andNode;
node.jjtGetChild(1).jjtAccept(this, data);
} else {
// There was a rollup, don't process the children and set the terms
// on the or node.
andNode.setTerms(terms);
}
}
if (null != data && !previouslyInAndContext) {
ctx.inAndContext = false;
}
return null;
}
@Override
public Object visit(ASTBitwiseOrNode node, Object data) {
return null;
}
@Override
public Object visit(ASTBitwiseXorNode node, Object data) {
return null;
}
@Override
public Object visit(ASTBitwiseAndNode node, Object data) {
return null;
}
@Override
public Object visit(ASTEQNode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = false;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTNENode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = true;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTLTNode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = false;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTGTNode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = false;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTLENode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = false;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTGENode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = false;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTERNode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = false;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, JexlOperatorConstants.getOperator(node.getClass()),
value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTNRNode node, Object data) {
StringBuilder fieldName = new StringBuilder();
ObjectHolder value = new ObjectHolder();
// Process both sides of this node.
Object left = node.jjtGetChild(0).jjtAccept(this, data);
Object right = node.jjtGetChild(1).jjtAccept(this, data);
// Ignore functions in the query
if (left instanceof FunctionResult || right instanceof FunctionResult) {
return null;
}
decodeResults(left, right, fieldName, value);
// We need to check to see if we are in a NOT context. If so,
// then we need to reverse the negation.
boolean negated = true;
if (null != data && data instanceof EvaluationContext) {
EvaluationContext ctx = (EvaluationContext) data;
if (ctx.inNotContext) {
negated = !negated;
}
}
QueryTerm term = new QueryTerm(negated, "!~", value.getObject());
this.currentNode.getTerms().put(fieldName.toString(), term);
return null;
}
@Override
public Object visit(ASTAdditiveNode node, Object data) {
return null;
}
@Override
public Object visit(ASTAdditiveOperator node, Object data) {
return null;
}
@Override
public Object visit(ASTMulNode node, Object data) {
return null;
}
@Override
public Object visit(ASTDivNode node, Object data) {
return null;
}
@Override
public Object visit(ASTModNode node, Object data) {
return null;
}
@Override
public Object visit(ASTUnaryMinusNode node, Object data) {
return null;
}
@Override
public Object visit(ASTBitwiseComplNode node, Object data) {
return null;
}
@Override
public Object visit(ASTNotNode node, Object data) {
boolean previouslyInNotContext = false;
EvaluationContext ctx = null;
if (null != data && data instanceof EvaluationContext) {
ctx = (EvaluationContext) data;
previouslyInNotContext = ctx.inNotContext;
} else {
ctx = new EvaluationContext();
}
ctx.inNotContext = true;
// Create a new node in the tree to represent the NOT
// Create a new And node under the current node.
TreeNode notNode = new TreeNode();
notNode.setType(ASTNotNode.class);
notNode.setParent(this.currentNode);
this.currentNode.getChildren().add(notNode);
this.currentNode = notNode;
// Process both sides of this node.
node.jjtGetChild(0).jjtAccept(this, ctx);
// reset the state
if (null != data && !previouslyInNotContext) {
ctx.inNotContext = false;
}
return null;
}
@Override
public Object visit(ASTIdentifier node, Object data) {
return new TermResult(node.image);
}
@Override
public Object visit(ASTNullLiteral node, Object data) {
return new LiteralResult(node.image);
}
@Override
public Object visit(ASTTrueNode node, Object data) {
return new LiteralResult(node.image);
}
@Override
public Object visit(ASTFalseNode node, Object data) {
return new LiteralResult(node.image);
}
@Override
public Object visit(ASTIntegerLiteral node, Object data) {
return new LiteralResult(node.image);
}
@Override
public Object visit(ASTFloatLiteral node, Object data) {
return new LiteralResult(node.image);
}
@Override
public Object visit(ASTStringLiteral node, Object data) {
return new LiteralResult("'" + node.image + "'");
}
@Override
public Object visit(ASTArrayLiteral node, Object data) {
return null;
}
@Override
public Object visit(ASTMapLiteral node, Object data) {
return null;
}
@Override
public Object visit(ASTMapEntry node, Object data) {
return null;
}
@Override
public Object visit(ASTEmptyFunction node, Object data) {
return null;
}
@Override
public Object visit(ASTSizeFunction node, Object data) {
return null;
}
@Override
public Object visit(ASTFunctionNode node, Object data) {
// objectNode 0 is the prefix
// objectNode 1 is the identifier , the others are parameters.
// process the remaining arguments
FunctionResult fr = new FunctionResult();
int argc = node.jjtGetNumChildren() - 2;
for (int i = 0; i < argc; i++) {
// Process both sides of this node.
Object result = node.jjtGetChild(i + 2).jjtAccept(this, data);
if (result instanceof TermResult) {
TermResult tr = (TermResult) result;
fr.getTerms().add(tr);
}
}
return fr;
}
@Override
public Object visit(ASTMethodNode node, Object data) {
return null;
}
@Override
public Object visit(ASTSizeMethod node, Object data) {
return null;
}
@Override
public Object visit(ASTConstructorNode node, Object data) {
return null;
}
@Override
public Object visit(ASTArrayAccess node, Object data) {
return null;
}
@Override
public Object visit(ASTReference node, Object data) {
return node.jjtGetChild(0).jjtAccept(this, data);
}
private void decodeResults(Object left, Object right, StringBuilder fieldName,
ObjectHolder holder) {
if (left instanceof TermResult) {
TermResult tr = (TermResult) left;
fieldName.append((String) tr.value);
// Then the right has to be the value
if (right instanceof LiteralResult) {
holder.setObject(((LiteralResult) right).value);
} else {
throw new IllegalArgumentException("Object mismatch");
}
} else if (right instanceof TermResult) {
TermResult tr = (TermResult) right;
fieldName.append((String) tr.value);
if (left instanceof LiteralResult) {
holder.setObject(((LiteralResult) left).value);
} else {
throw new IllegalArgumentException("Object mismatch");
}
} else {
throw new IllegalArgumentException("No Term specified in query");
}
}
}