package org.limewire.util; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.regex.Pattern; /** * Predicate parser that parses a string encoded in Reverse Polish Notation * and evaluates it to true or false. * <p> * An optional StringLookup object can be provided the different terms * are looked up for values. */ public class RPNParser { /** Map from recognized operands to their implementations. */ private static final Map<String, Predicate> predicateByOperand; private static final Set<String> experimentalPredicates; static { Map<String, Predicate> pMap = new HashMap<String, Predicate>(); Set<String> eSet = new HashSet<String>(); pMap.put("==", new EqualsPredicate()); pMap.put("<", new LessPredicate()); pMap.put(">", new GreaterPredicate()); pMap.put("NOT", new NOTPredicate()); pMap.put("OR", new ORPredicate()); pMap.put("AND", new ANDPredicate()); pMap.put("CONTAINS", new ContainsPredicate()); pMap.put("MATCHES", new MatchesPredicate()); eSet.add("MATCHES"); predicateByOperand = Collections.unmodifiableMap(pMap); experimentalPredicates = Collections.unmodifiableSet(eSet); } /** * Interface that provides lookup based on string. */ public static interface StringLookup { /** * @return value for certain key. * A return value of null means there is no such key. */ public String lookup(String key); } /** expression to evaluate */ private final String[] expression; /** Whether experimental predicates are allowed */ private final boolean experimental; /** Stack used for parsing */ private final Stack<String> stack = new Stack<String>(); public RPNParser(String... expression) { this(true, expression); } /** * Creates a new parser. * @param expression the expression to parse * @param experimental if experimental predicates are allowed */ public RPNParser(boolean experimental, String... expression) { this.expression = expression; this.experimental = experimental; } /** * @return true or false. * @throws IllegalArgumentException if either the input or * the values returned by the lookups are not valid. */ public boolean evaluate() { return evaluate(new StringLookup() { public String lookup(String key) { return key; } }); } /** * @return true or false. * @throws IllegalArgumentException if either the input or * the values returned by the lookups are not valid. */ public boolean evaluate(StringLookup lookup) { for (String r : expression) { if (r == null) throw new IllegalArgumentException("null input"); if (!predicateByOperand.containsKey(r)) { String val = lookup.lookup(r); stack.push(val != null ? val : r); } else if (experimental || !experimentalPredicates.contains(r)) evaluateOp(r); } if (stack.size() != 1) // this illegal state can only be caused by illegal argument throw new IllegalArgumentException(stack.size()+" elements at end of parse"); return Boolean.valueOf(stack.pop()); } /** * Evaluates an operand based on the contents of the stack * and pushes either "True" or "False" on the stack. */ private void evaluateOp(String operand) { Predicate p = predicateByOperand.get(operand); if (stack.size() < p.numOperands()) throw new IllegalArgumentException("not enough operands"+p.numOperands()); String[] strings = new String[p.numOperands()]; for (int i = strings.length-1; i>=0; i--) strings[i] = stack.pop(); stack.push(Boolean.toString(p.evaluate(strings))); } /** * A class representing a boolean predicate. */ private static abstract class Predicate { /** * @return true or false depending on how the operands evaluate */ public abstract boolean evaluate(String... operands); /** * @return the number of operands this predicate needs. */ public int numOperands() { return 2; } } /** * Predicate for the == operation. */ static class EqualsPredicate extends Predicate { @Override public boolean evaluate(String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return operands[0].equals(operands[1]); } } /** * Predicate for the || operation. */ static class ORPredicate extends Predicate { @Override public boolean evaluate(String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return strictBoolean(operands[0]) || strictBoolean(operands[1]); } } /** * Predicate for the && operation. */ static class ANDPredicate extends Predicate { @Override public boolean evaluate(String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return strictBoolean(operands[0]) && strictBoolean(operands[1]); } } /** * Predicate for the ! operation. */ static class NOTPredicate extends Predicate { @Override public boolean evaluate(String... operands) { if (operands.length != 1) throw new IllegalArgumentException(); return !strictBoolean(operands[0]); } @Override public int numOperands() { return 1; } } /** * Predicate for the > operation. */ static class GreaterPredicate extends Predicate { @Override public boolean evaluate (String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return Double.valueOf(operands[0]) > Double.valueOf(operands[1]); } } /** * Predicate for the < operation. */ static class LessPredicate extends Predicate { @Override public boolean evaluate (String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return Double.valueOf(operands[0]) < Double.valueOf(operands[1]); } } /** * Predicate for the String.contains operation */ static class ContainsPredicate extends Predicate { @Override public boolean evaluate (String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return operands[0].toLowerCase().contains(operands[1].toLowerCase()); } } /** * Predicate for matching a pattern. */ static class MatchesPredicate extends Predicate { @Override public boolean evaluate (String... operands) { if (operands.length != 2) throw new IllegalArgumentException(); return Pattern.matches(operands[0],operands[1]); } } /** * Stricter version of Boolean.valueOf. */ static boolean strictBoolean(String s) { if (s == null) throw new IllegalArgumentException(); if (s.equalsIgnoreCase("true")) return true; if (s.equalsIgnoreCase("false")) return false; throw new IllegalArgumentException(); } }