package org.radargun.config; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Stack; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.utils.Tokenizer; /** * Class with only static methods used for evaluation of expressions. Does: * - replace ${property.name} with the actual property value (from System.getProperty()) * - replace ${property.name : default.value} with property value or default.value if the property is not defined * - evaluate infix expressions inside #{ expression } block, available operators are: * '+' (addition), '-' (subtraction), '*' (multiplying), '/' (division), '%' (modulo operation), * '..' (range generation), ',' (adding to list), '(' and ')' as usual parentheses. * * Examples: * #{ 1..3,5 } -> 1,2,3,5 * #{ ( ${x} + 5 ) * 6 } with -Dx=2 -> 42 * foo${y}bar with -Dy=goo -> foogoobar * * @author Radim Vansa <rvansa@redhat.com> */ public final class Evaluator { private static final Log log = LogFactory.getLog(Evaluator.class); private Evaluator() {} /** * Parse string possibly containing expressions and properties and convert the value to integer. */ public static int parseInt(String string) { return Integer.parseInt(parseString(string)); } /** * Parse string possibly containing expressions and properties. */ public static String parseString(String string) { if (string == null) return null; StringBuilder sb = new StringBuilder(); int currentIndex = 0; while (currentIndex < string.length()) { int propertyIndex = string.indexOf("${", currentIndex); int expressionIndex = string.indexOf("#{", currentIndex); int nextIndex = propertyIndex < 0 ? (expressionIndex < 0 ? string.length() : expressionIndex) : (expressionIndex < 0 ? propertyIndex : Math.min(expressionIndex, propertyIndex)); sb.append(string.substring(currentIndex, nextIndex)); currentIndex = nextIndex + 2; if (nextIndex == propertyIndex) { nextIndex = string.indexOf('}', currentIndex); if (nextIndex < 0) { throw new IllegalArgumentException(string); } sb.append(evalProperty(string, currentIndex, nextIndex)); currentIndex = nextIndex + 1; } else if (nextIndex == expressionIndex) { Stack<Operator> operators = new Stack<>(); Stack<Value> operands = new Stack<>(); Tokenizer tokenizer = new Tokenizer(string, Operator.symbols(), true, false, currentIndex); boolean closed = false; // we set this to true because if '-' is on the beginning, is interpreted as sign boolean lastTokenIsOperator = true; boolean negativeSign = false; while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); Operator op = Operator.from(token); if (op == null) { operands.push(new Value(negativeSign ? "-" + token : token)); lastTokenIsOperator = false; continue; } else if (op.isWhite()) { // do not set lastTokenIsOperator continue; } else if (op == Operator.OPENVAR) { if (!tokenizer.hasMoreTokens()) throw new IllegalArgumentException(string); StringBuilder var = new StringBuilder(); while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken(); if ((op = Operator.from(token)) == null || op.isWhite()) { var.append(token); } else { break; } } if (op != Operator.CLOSEVAR) { throw new IllegalArgumentException("Expected '}' but found " + token + " in " + string); } operands.push(evalProperty(var.toString(), 0, var.length())); lastTokenIsOperator = false; continue; } else if (op == Operator.CLOSEVAR) { // end of expression to be evaluated closed = true; break; } else if (op.isFunction()) { operators.push(op); } else if (op == Operator.OPENPAR) { operators.push(op); } else if (op == Operator.CLOSEPAR) { while ((op = operators.pop()) != Operator.OPENPAR) { op.exec(operands); if (operators.isEmpty()) throw new IllegalStateException("Cannot find matching '('"); } while (!operators.isEmpty() && operators.peek().isFunction()) { op = operators.pop(); op.exec(operands); } } else if (op == Operator.MINUS && lastTokenIsOperator) { negativeSign = true; } else { while (true) { if (operators.isEmpty() || operators.peek() == Operator.OPENPAR || operators.peek().precedence() < op.precedence()) { operators.push(op); break; } operators.pop().exec(operands); } lastTokenIsOperator = true; } } if (!closed) { throw new IllegalArgumentException("Expression is missing closing '}': " + string); } while (!operators.empty()) { operators.pop().exec(operands); } sb.append(operands.pop()); if (!operands.empty()) { throw new IllegalArgumentException(operands.size() + " operands not processed: top=" + operands.pop() + " all=" + operands); } currentIndex = tokenizer.getPosition(); } } return sb.toString(); } private static Value evalProperty(String string, int startIndex, int endIndex) { int colonIndex = string.indexOf(':', startIndex); String property; Value value = null, def = null; if (colonIndex < 0 || colonIndex > endIndex) { property = string.substring(startIndex, endIndex).trim(); } else { property = string.substring(startIndex, colonIndex).trim(); def = new Value(string.substring(colonIndex + 1, endIndex).trim()); } String strValue = System.getProperty(property); if (strValue != null && !strValue.isEmpty()) { value = new Value(strValue.trim()); } else { if (property.startsWith("env.")) { String env = System.getenv(property.substring(4)); if (env != null && !env.isEmpty()) { value = new Value(env.trim()); } } else if (property.startsWith("random.")) { value = random(property); } } if (value != null) { return value; } else if (def != null) { return def; } else { log.debugf("Failed to resolve property ${%s}, defined properties are: ", property); for (Map.Entry<Object, Object> prop : System.getProperties().entrySet()) { log.debugf("${%s} -> '%s'", prop.getKey(), prop.getValue()); } for (Map.Entry<String, String> env : System.getenv().entrySet()) { log.debugf("${env.%s} -> '%s'", env.getKey(), env.getValue()); } throw new IllegalArgumentException("Property '" + property + "' not defined!"); } } private static Value random(String type) { Random random = new Random(); if (type.equals("random.int")) { return new Value(random.nextInt() & Integer.MAX_VALUE); } else if (type.equals("random.long")) { return new Value(random.nextLong() & Long.MAX_VALUE); } else if (type.equals("random.double")) { return new Value(random.nextDouble()); } else if (type.equals("random.boolean")) { return new Value(String.valueOf(random.nextBoolean())); } else { return null; } } private static Value range(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { long from = first.getLong(); long to = second.getLong(); List<Value> values = new ArrayList((int) Math.abs(from - to)); long inc = from <= to ? 1 : -1; for (long i = from; from <= to ? i <= to : i >= to; i += inc) values.add(new Value(i)); return new Value(values); } else { throw new IllegalArgumentException(first + " .. " + second); } } private static Value multiply(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { return new Value(first.getLong() * second.getLong()); } else if (first.type.canBeDouble() && second.type.canBeDouble()) { return new Value(first.getDouble() * second.getDouble()); } else { throw new IllegalArgumentException(first + " * " + second); } } private static Value minus(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { return new Value(first.getLong() - second.getLong()); } else if (first.type.canBeDouble() && second.type.canBeDouble()) { return new Value(first.getDouble() - second.getDouble()); } else { throw new IllegalArgumentException(first + " - " + second); } } private static Value plus(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { return new Value(first.getLong() + second.getLong()); } else if (first.type.canBeDouble() && second.type.canBeDouble()) { return new Value(first.getDouble() + second.getDouble()); } else { throw new IllegalArgumentException(first + " + " + second); } } private static Value div(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { return new Value(first.getLong() / second.getLong()); } else if (first.type.canBeDouble() && second.type.canBeDouble()) { return new Value(first.getDouble() / second.getDouble()); } else { throw new IllegalArgumentException(first + " / " + second); } } private static Value modulo(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { return new Value(first.getLong() % second.getLong()); } else { throw new IllegalArgumentException(first + " % " + second); } } private static Value power(Value first, Value second) { if (first.type.canBeLong() && second.type.canBeLong()) { long base = first.getLong(); long power = second.getLong(); long value = 1; if (power < 0) { return new Value(Math.pow(base, power)); } for (long i = power; i > 0; --i) { value *= base; } return new Value(value); } else if (first.type.canBeDouble() && second.type.canBeDouble()) { return new Value(Math.pow(first.getDouble(), second.getDouble())); } else { throw new IllegalArgumentException(first + "^" + second); } } private static Value concat(Value first, Value second) { List<Value> list = new ArrayList(); if (first.type == ValueType.LIST) { list.addAll(first.getList()); } else { list.add(first); } if (second.type == ValueType.LIST) { list.addAll(second.getList()); } else { list.add(second); } return new Value(list); } private static Value max(Value value) { if (value.type == ValueType.LIST) { Value max = null; for (Value v : value.getList()) { if (max == null) { max = v; } else if (max.type.canBeLong() && v.type.canBeLong()) { max = max.getLong() >= v.getLong() ? max : v; } else if (max.type.canBeDouble() && v.type.canBeDouble()) { max = max.getDouble() >= v.getDouble() ? max : v; } else { throw new IllegalArgumentException("max(" + value + ")"); } } if (max == null) { throw new IllegalArgumentException("max of 0 values"); } return max; } else { log.warn("Computing max from single value"); return value; } } private static Value min(Value value) { if (value.type == ValueType.LIST) { Value min = null; for (Value v : value.getList()) { if (min == null) { min = v; } else if (min.type.canBeLong() && v.type.canBeLong()) { min = min.getLong() <= v.getLong() ? min : v; } else if (min.type.canBeDouble() && v.type.canBeDouble()) { min = min.getDouble() <= v.getDouble() ? min : v; } else { throw new IllegalArgumentException("min(" + value + ")"); } } if (min == null) { throw new IllegalArgumentException("min of 0 values"); } return min; } else { log.warn("Computing min from single value"); return value; } } private static Value floor(Value value) { if (value.type.canBeLong()) { return value; } else if (value.type.canBeDouble()) { return new Value((long) Math.floor(value.getDouble())); } else { throw new IllegalArgumentException("floor(" + value + ")"); } } private static Value ceil(Value value) { if (value.type.canBeLong()) { return value; } else if (value.type.canBeDouble()) { return new Value((long) Math.ceil(value.getDouble())); } else { throw new IllegalArgumentException("ceil(" + value + ")"); } } private static Value abs(Value value) { if (value.type.canBeLong()) { return new Value(Math.abs(value.getLong())); } else if (value.type.canBeDouble()) { return new Value(Math.abs(value.getDouble())); } else { throw new IllegalArgumentException("abs(" + value + ")"); } } private static Value listGet(Value first, Value second) { if (!second.type.canBeLong()) { throw new IllegalArgumentException("Invalid index argument " + second + ", natural number expected"); } List<Value> valueList = ValueType.LIST == first.type ? first.getList() : convertToList(first.objectValue); if (valueList.size() > 1) { if (valueList.size() <= second.getLong()) { throw new IllegalArgumentException("Out of bounds index value " + second.getLong() + ", list size " + valueList.size()); } return new Value(valueList.get((int) second.getLong()).toString()); } else { if (second.getLong() != 0) { throw new IllegalArgumentException("Out of bounds index value " + second.getLong() + ", list size " + 1); } return new Value(first.objectValue.toString()); } } private static Value listSize(Value first) { if (first.type.canBeLong) { return new Value(1); } else { int length = first.objectValue.toString().split(",").length; if (length == 0) { throw new IllegalArgumentException("Invalid argument " + first + " provided, list value expected"); } return new Value(length); } } private static List<Value> convertToList(Object value) { String[] strings = value.toString().split(","); List<Value> valueList = new ArrayList<>(strings.length); for (String string : strings) { valueList.add(new Value(string.trim())); } return valueList; } protected static Value toDouble(Value value) { if (value.type.canBeDouble()) return new Value(value.getDouble()); throw new IllegalArgumentException(value.toString()); } private static Value gcd(Value value) { if (value.type != ValueType.LIST) throw new IllegalArgumentException(value.toString()); ArrayList<Long> list = new ArrayList<>(); for (Value v : value.getList()) { if (!v.type.canBeLong) throw new IllegalArgumentException(v.toString() + " in gcd " + value.toString()); list.add(Math.abs(v.getLong())); } if (list.isEmpty()) throw new IllegalStateException(); long a = list.get(0); for (int index = 1; index < list.size(); ++index) { long b = Math.abs(list.get(index)); if (b > a) { long tmp = a; a = b; b = tmp; } for (; ; ) { if (b == 0) break; long tmp = b; b = a % b; a = tmp; } } return new Value(a); } private enum ValueType { STRING(false, false), LONG(true, true), DOUBLE(false, true), LIST(false, false); private final boolean canBeLong; private final boolean canBeDouble; ValueType(boolean canBeLong, boolean canBeDouble) { this.canBeLong = canBeLong; this.canBeDouble = canBeDouble; } public boolean canBeLong() { return canBeLong; } public boolean canBeDouble() { return canBeDouble; } } private static class Value { public final ValueType type; private final double doubleValue; private final long longValue; private final Object objectValue; private Value(long longValue) { this.type = ValueType.LONG; this.doubleValue = longValue; this.longValue = longValue; this.objectValue = String.valueOf(longValue); } private Value(double doubleValue) { this.type = ValueType.DOUBLE; this.doubleValue = doubleValue; this.longValue = 0; this.objectValue = String.valueOf(doubleValue); } private Value(String string) { ValueType t = ValueType.STRING; double d = Double.NaN; long l = 0; Object o = string; try { d = l = Long.parseLong(string); o = l; t = ValueType.LONG; } catch (NumberFormatException e) { try { o = d = Double.parseDouble(string); t = ValueType.DOUBLE; } catch (NumberFormatException e2) { // ok } } type = t; doubleValue = d; longValue = l; objectValue = o; } public Value(List<Value> values) { type = ValueType.LIST; doubleValue = Double.NaN; longValue = 0; objectValue = values; } public long getLong() { return longValue; } public double getDouble() { return doubleValue; } public List<Value> getList() { return (List<Value>) objectValue; } @Override public String toString() { if (type == ValueType.LIST) { StringBuilder sb = new StringBuilder(); for (Value v : (List<Value>) objectValue) { if (sb.length() != 0) sb.append(", "); // inner lists would require special treatment if (v.type == ValueType.LIST) { sb.append("[").append(v).append("]"); } else { sb.append(v); } } return sb.toString(); } else { return String.valueOf(objectValue); } } } private static interface OneArgFunctor { Value exec(Value value); } private static interface TwoArgFunctor { Value exec(Value first, Value second); } private enum Operator { SPACE(" ", 0, true, false, null, null), TAB("\t", 0, true, false, null, null), NEWLINE("\n", 0, true, false, null, null), CR("\r", 0, true, false, null, null), PLUS("+", 100, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return plus(first, second); } }), MINUS("-", 100, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return minus(first, second); } }), MULTIPLY("*", 200, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return multiply(first, second); } }), DIVIDE("/", 200, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return div(first, second); } }), MODULO("%", 200, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return modulo(first, second); } }), POWER("^", 300, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return power(first, second); } }), RANGE("..", 50, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return range(first, second); } }), COMMA(",", 10, false, false, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return concat(first, second); } }), OPENPAR("(", 0, false, false, null, null), CLOSEPAR(")", 0, false, false, null, null), OPENVAR("${", 0, false, false, null, null), CLOSEVAR("}", 0, false, false, null, null), MAX("max", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return max(value); } }, null), MIN("min", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return min(value); } }, null), FLOOR("floor", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return floor(value); } }, null), CEIL("ceil", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return ceil(value); } }, null), ABS("abs", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return abs(value); } }, null), LIST_GET(".get", 400, false, true, null, new TwoArgFunctor() { @Override public Value exec(Value first, Value second) { return listGet(first, second); } }), LIST_SIZE(".size", 500, false, true, new OneArgFunctor() { @Override public Value exec(Value first) { return listSize(first); } }, null), DOUBLE("double", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return toDouble(value); } }, null), // GCD accepts actually *list* of values GREATEST_COMMON_DIVISOR("gcd", 0, false, true, new OneArgFunctor() { @Override public Value exec(Value value) { return gcd(value); } }, null); private static Map<String, Operator> symbolMap = new HashMap<String, Operator>(); private String symbol; private int precedence; private boolean isWhite; private boolean isFunction; private OneArgFunctor functor1; private TwoArgFunctor functor2; static { for (Operator op : values()) { symbolMap.put(op.symbol, op); } } Operator(String symbol, int precedence, boolean isWhite, boolean isFunction, OneArgFunctor functor1, TwoArgFunctor functor2) { this.symbol = symbol; this.precedence = precedence; this.isWhite = isWhite; this.isFunction = isFunction; this.functor1 = functor1; this.functor2 = functor2; } /** * @return Symbols that don't belong to functions */ public static String[] symbols() { Operator[] values = values(); ArrayList<String> symbols = new ArrayList<>(values.length); for (int i = 0; i < values.length; ++i) { if (!values[i].isFunction()) { symbols.add(values[i].symbol); } } return symbols.toArray(new String[symbols.size()]); } public static Operator from(String symbol) { return symbolMap.get(symbol); } public int precedence() { return precedence; } public boolean isWhite() { return isWhite; } public boolean isFunction() { return isFunction; } public void exec(Stack<Value> operands) { if (functor1 != null) { operands.push(functor1.exec(operands.pop())); } else if (functor2 != null) { Value second = operands.pop(); Value first = operands.pop(); operands.push(functor2.exec(first, second)); } else { throw new IllegalStateException("This operator cannot be executed."); } } } }