ExpressionParseTree.java
/*
* 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.catalina.ssi;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.res.StringManager;
/**
* Represents a parsed expression.
*
* @author Paul Speed
*/
public class ExpressionParseTree {
private static final StringManager sm = StringManager.getManager(ExpressionParseTree.class);
/**
* Contains the current set of completed nodes. This is a workspace for the parser. Needs to be LinkedList since it
* can contain {@code null}s.
*/
private final LinkedList<Node> nodeStack = new LinkedList<>();
/**
* Contains operator nodes that don't yet have values. This is a workspace for the parser. Needs to be LinkedList
* since it can contain {@code null}s.
*/
private final LinkedList<OppNode> oppStack = new LinkedList<>();
/**
* The root node after the expression has been parsed.
*/
private Node root;
/**
* The SSIMediator to use when evaluating the expressions.
*/
private final SSIMediator ssiMediator;
/**
* Creates a new parse tree for the specified expression.
*
* @param expr The expression string
* @param ssiMediator Used to evaluated the expressions
*
* @throws ParseException a parsing error occurred
*/
public ExpressionParseTree(String expr, SSIMediator ssiMediator) throws ParseException {
this.ssiMediator = ssiMediator;
parseExpression(expr);
}
/**
* Evaluates the tree and returns true or false. The specified SSIMediator is used to resolve variable references.
*
* @return the evaluation result
*
* @throws SSIStopProcessingException If an error occurs evaluating the tree
*/
public boolean evaluateTree() throws SSIStopProcessingException {
try {
return root.evaluate();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
throw new SSIStopProcessingException(t);
}
}
/**
* Pushes a new operator onto the opp stack, resolving existing opps as needed.
*
* @param node The operator node
*/
private void pushOpp(OppNode node) {
// If node is null then it's just a group marker
if (node == null) {
oppStack.add(0, node);
return;
}
while (true) {
if (oppStack.size() == 0) {
break;
}
OppNode top = oppStack.get(0);
// If the top is a spacer then don't pop
// anything
if (top == null) {
break;
}
// If the top node has a lower precedence then
// let it stay
if (top.getPrecedence() < node.getPrecedence()) {
break;
}
// Remove the top node
oppStack.remove(0);
// Let it fill its branches
top.popValues(nodeStack);
// Stick it on the resolved node stack
nodeStack.add(0, top);
}
// Add the new node to the opp stack
oppStack.add(0, node);
}
/**
* Resolves all pending opp nodes on the stack until the next group marker is reached.
*/
private void resolveGroup() {
OppNode top = null;
while ((top = oppStack.remove(0)) != null) {
// Let it fill its branches
top.popValues(nodeStack);
// Stick it on the resolved node stack
nodeStack.add(0, top);
}
}
/**
* Parses the specified expression into a tree of parse nodes.
*
* @param expr The expression to parse
*
* @throws ParseException a parsing error occurred
*/
private void parseExpression(String expr) throws ParseException {
StringNode currStringNode = null;
// We cheat a little and start an artificial
// group right away. It makes finishing easier.
pushOpp(null);
ExpressionTokenizer et = new ExpressionTokenizer(expr);
while (et.hasMoreTokens()) {
int token = et.nextToken();
if (token != ExpressionTokenizer.TOKEN_STRING) {
currStringNode = null;
}
switch (token) {
case ExpressionTokenizer.TOKEN_STRING:
if (currStringNode == null) {
currStringNode = new StringNode(et.getTokenValue());
nodeStack.add(0, currStringNode);
} else {
// Add to the existing
currStringNode.value.append(' ');
currStringNode.value.append(et.getTokenValue());
}
break;
case ExpressionTokenizer.TOKEN_AND:
pushOpp(new AndNode());
break;
case ExpressionTokenizer.TOKEN_OR:
pushOpp(new OrNode());
break;
case ExpressionTokenizer.TOKEN_NOT:
pushOpp(new NotNode());
break;
case ExpressionTokenizer.TOKEN_EQ:
pushOpp(new EqualNode());
break;
case ExpressionTokenizer.TOKEN_NOT_EQ:
pushOpp(new NotNode());
// Sneak the regular node in. The NOT will
// be resolved when the next opp comes along.
oppStack.add(0, new EqualNode());
break;
case ExpressionTokenizer.TOKEN_RBRACE:
// Closeout the current group
resolveGroup();
break;
case ExpressionTokenizer.TOKEN_LBRACE:
// Push a group marker
pushOpp(null);
break;
case ExpressionTokenizer.TOKEN_GE:
pushOpp(new NotNode());
// Similar strategy to NOT_EQ above, except this
// is NOT less than
oppStack.add(0, new LessThanNode());
break;
case ExpressionTokenizer.TOKEN_LE:
pushOpp(new NotNode());
// Similar strategy to NOT_EQ above, except this
// is NOT greater than
oppStack.add(0, new GreaterThanNode());
break;
case ExpressionTokenizer.TOKEN_GT:
pushOpp(new GreaterThanNode());
break;
case ExpressionTokenizer.TOKEN_LT:
pushOpp(new LessThanNode());
break;
case ExpressionTokenizer.TOKEN_END:
break;
}
}
// Finish off the rest of the opps
resolveGroup();
if (nodeStack.size() == 0) {
throw new ParseException(sm.getString("expressionParseTree.noNodes"), et.getIndex());
}
if (nodeStack.size() > 1) {
throw new ParseException(sm.getString("expressionParseTree.extraNodes"), et.getIndex());
}
if (oppStack.size() != 0) {
throw new ParseException(sm.getString("expressionParseTree.unusedOpCodes"), et.getIndex());
}
root = nodeStack.get(0);
}
/**
* A node in the expression parse tree.
*/
private abstract static class Node {
/**
* @return {@code true} if the node evaluates to true.
*/
public abstract boolean evaluate();
}
/**
* A node the represents a String value
*/
private class StringNode extends Node {
StringBuilder value;
String resolved = null;
StringNode(String value) {
this.value = new StringBuilder(value);
}
/**
* Resolves any variable references and returns the value string.
*
* @return the value string
*/
public String getValue() {
if (resolved == null) {
resolved = ssiMediator.substituteVariables(value.toString());
}
return resolved;
}
/**
* Returns true if the string is not empty.
*/
@Override
public boolean evaluate() {
return !(getValue().length() == 0);
}
@Override
public String toString() {
return value.toString();
}
}
private static final int PRECEDENCE_NOT = 5;
private static final int PRECEDENCE_COMPARE = 4;
private static final int PRECEDENCE_LOGICAL = 1;
/**
* A node implementation that represents an operation.
*/
private abstract static class OppNode extends Node {
/**
* The left branch.
*/
Node left;
/**
* The right branch.
*/
Node right;
/**
* @return a precedence level suitable for comparison to other OppNode preference levels.
*/
public abstract int getPrecedence();
/**
* Lets the node pop its own branch nodes off the front of the specified list. The default pulls two.
*
* @param values The list from which to pop the values
*/
public void popValues(List<Node> values) {
right = values.remove(0);
left = values.remove(0);
}
}
private static final class NotNode extends OppNode {
@Override
public boolean evaluate() {
return !left.evaluate();
}
@Override
public int getPrecedence() {
return PRECEDENCE_NOT;
}
/**
* Overridden to pop only one value.
*/
@Override
public void popValues(List<Node> values) {
left = values.remove(0);
}
@Override
public String toString() {
return left + " NOT";
}
}
private static final class AndNode extends OppNode {
@Override
public boolean evaluate() {
if (!left.evaluate()) {
return false;
}
return right.evaluate();
}
@Override
public int getPrecedence() {
return PRECEDENCE_LOGICAL;
}
@Override
public String toString() {
return left + " " + right + " AND";
}
}
private static final class OrNode extends OppNode {
@Override
public boolean evaluate() {
if (left.evaluate()) {
return true;
}
return right.evaluate();
}
@Override
public int getPrecedence() {
return PRECEDENCE_LOGICAL;
}
@Override
public String toString() {
return left + " " + right + " OR";
}
}
private abstract class CompareNode extends OppNode {
protected int compareBranches() {
String val1 = ((StringNode) left).getValue();
String val2 = ((StringNode) right).getValue();
int val2Len = val2.length();
if (val2Len > 1 && val2.charAt(0) == '/' && val2.charAt(val2Len - 1) == '/') {
// Treat as a regular expression
String expr = val2.substring(1, val2Len - 1);
ssiMediator.clearMatchGroups();
try {
Pattern pattern = Pattern.compile(expr);
// Regular expressions will only ever be used with EqualNode
// so return zero for equal and non-zero for not equal
Matcher matcher = pattern.matcher(val1);
if (matcher.find()) {
ssiMediator.populateMatchGroups(matcher);
return 0;
} else {
return -1;
}
} catch (PatternSyntaxException pse) {
ssiMediator.log(sm.getString("expressionParseTree.invalidExpression", expr), pse);
return 0;
}
}
return val1.compareTo(val2);
}
}
private final class EqualNode extends CompareNode {
@Override
public boolean evaluate() {
return (compareBranches() == 0);
}
@Override
public int getPrecedence() {
return PRECEDENCE_COMPARE;
}
@Override
public String toString() {
return left + " " + right + " EQ";
}
}
private final class GreaterThanNode extends CompareNode {
@Override
public boolean evaluate() {
return (compareBranches() > 0);
}
@Override
public int getPrecedence() {
return PRECEDENCE_COMPARE;
}
@Override
public String toString() {
return left + " " + right + " GT";
}
}
private final class LessThanNode extends CompareNode {
@Override
public boolean evaluate() {
return (compareBranches() < 0);
}
@Override
public int getPrecedence() {
return PRECEDENCE_COMPARE;
}
@Override
public String toString() {
return left + " " + right + " LT";
}
}
}