/** * Copyright 2015 ArcBees Inc. * * 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 * * 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 com.arcbees.gaestudio.server.util; import java.math.BigDecimal; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CharStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.TokenStream; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.FetchOptions; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.PropertyProjection; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.datastore.Query.FilterOperator; import com.google.appengine.api.datastore.Query.FilterPredicate; import com.google.appengine.api.datastore.Query.SortDirection; import com.google.appengine.api.users.User; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; public class GqlQuery { private Query query; private FetchOptions fetchOptions; private ParseResult parseResult; /** * @param queryStr * @param context */ public GqlQuery(String queryStr, Map<String, Object> context) { super(); Preconditions.checkNotNull(queryStr); build(queryStr, context); } /** * @param queryStr * @param params */ public GqlQuery(String queryStr, Object... params) { super(); Preconditions.checkNotNull(queryStr); // evaluation context Map<String, Object> context = Maps.newHashMap(); for (int i = 0; i < params.length; i++) { context.put(String.valueOf(i + 1), params[i]); } build(queryStr, context); } /** * @return */ public ParseResult getParseResult() { return parseResult; } public void build(ParseResult parseResult, Map<String, Object> context) { // from clause if (parseResult.from == null) { this.query = new Query(); } else { this.query = new Query(parseResult.from.kind); } // select clause if (parseResult.select.isKeyOnly()) { this.query.setKeysOnly(); } for (String projection : parseResult.select.projections) { query.addProjection(new PropertyProjection(projection, null)); } // where clause if (parseResult.where != null) { for (Condition c : parseResult.where.conditions) { this.query.setFilter(new FilterPredicate(c.propertyName, c.operator, c.e.evaluate(context))); } // set ancester if (parseResult.where.ancestor != null) { this.query.setAncestor(parseResult.where.ancestor.ancestorKey(context)); } } // order by if (parseResult.orderBy != null) { for (OrderByItem o : parseResult.orderBy.items) { this.query.addSort(o.propertyName, o.direction); } } // limit if (parseResult.limit != null) { this.fetchOptions = FetchOptions.Builder.withLimit(parseResult.limit.limit); } if (parseResult.offset != null) { if (this.fetchOptions == null) { this.fetchOptions = FetchOptions.Builder.withDefaults(); } this.fetchOptions.offset(parseResult.offset.offset); } } private void build(String queryStr, Map<String, Object> context) { parseResult = parse(queryStr); build(parseResult, context); } static ParseResult parse(String queryStr) { try { CharStream input = new ANTLRStringStream(queryStr); GQLLexer lexer = new GQLLexer(input); TokenStream tokens = new CommonTokenStream(lexer); GQLParser parser = new GQLParser(tokens); return parser.query().r; } catch (RecognitionException e) { throw new GqlQueryException("GQL syntax error"); } } public Query query() { return query; } public FetchOptions fetchOptions() { if (fetchOptions == null) { fetchOptions = FetchOptions.Builder.withDefaults(); } return fetchOptions; } public static class GqlQueryException extends RuntimeException { /** * */ private static final long serialVersionUID = 1L; public GqlQueryException() { super(); } public GqlQueryException(String message, Throwable cause) { super(message, cause); } public GqlQueryException(String message) { super(message); } public GqlQueryException(Throwable cause) { super(cause); } } public static class ParseResult { private Select select; private From from; private Where where; private OrderBy orderBy; private Limit limit; private Offset offset; public Select getSelect() { return select; } public ParseResult setSelect(Select select) { this.select = select; return this; } public From getFrom() { return from; } public ParseResult setFrom(From from) { this.from = from; return this; } public Where getWhere() { return where; } public ParseResult setWhere(Where where) { this.where = where; return this; } public OrderBy getOrderBy() { return orderBy; } public ParseResult setOrderBy(OrderBy orderBy) { this.orderBy = orderBy; return this; } public Limit getLimit() { return limit; } public ParseResult setLimit(Limit limit) { this.limit = limit; return this; } public Offset getOffset() { return offset; } public ParseResult setOffset(Offset offset) { this.offset = offset; return this; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((from == null) ? 0 : from.hashCode()); result = prime * result + ((limit == null) ? 0 : limit.hashCode()); result = prime * result + ((offset == null) ? 0 : offset.hashCode()); result = prime * result + ((orderBy == null) ? 0 : orderBy.hashCode()); result = prime * result + ((select == null) ? 0 : select.hashCode()); result = prime * result + ((where == null) ? 0 : where.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ParseResult other = (ParseResult) obj; if (from == null) { if (other.from != null) { return false; } } else if (!from.equals(other.from)) { return false; } if (limit == null) { if (other.limit != null) { return false; } } else if (!limit.equals(other.limit)) { return false; } if (offset == null) { if (other.offset != null) { return false; } } else if (!offset.equals(other.offset)) { return false; } if (orderBy == null) { if (other.orderBy != null) { return false; } } else if (!orderBy.equals(other.orderBy)) { return false; } if (select == null) { if (other.select != null) { return false; } } else if (!select.equals(other.select)) { return false; } if (where == null) { if (other.where != null) { return false; } } else if (!where.equals(other.where)) { return false; } return true; } @Override public String toString() { return "ParseResult [select=" + select + ", from=" + from + ", where=" + where + ", orderBy=" + orderBy + ", limit=" + limit + ", offset=" + offset + "]"; } } public static class Select { /** * select item, only * and __key__ is allowed (case sensitive). */ private final boolean keyOnly; private final ArrayList<String> projections; public Select(boolean keyOnly) { super(); this.keyOnly = keyOnly; this.projections = new ArrayList<>(); } public void addProjection(String field) { this.projections.add(field); } public ArrayList<String> getProjections() { return projections; } public boolean isKeyOnly() { return this.keyOnly; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (keyOnly ? 1231 : 1237); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Select other = (Select) obj; if (keyOnly != other.keyOnly) { return false; } if (!projections.equals(other.getProjections())) { return false; } return true; } @Override public String toString() { return "Select [keyOnly=" + keyOnly + "]"; } } public static class From { private final String kind; public From(String kind) { super(); this.kind = kind; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((kind == null) ? 0 : kind.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } From other = (From) obj; if (kind == null) { if (other.kind != null) { return false; } } else if (!kind.equals(other.kind)) { return false; } return true; } @Override public String toString() { return "From [kind=" + kind + "]"; } } public static class Where { private final List<Condition> conditions; private Ancestor ancestor; public Where() { conditions = Lists.newLinkedList(); } public Where withCondition(Condition condition) { this.conditions.add(condition); return this; } public Where withAncestor(Evaluator e) { ancestor = new Ancestor(e); return this; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((ancestor == null) ? 0 : ancestor.hashCode()); result = prime * result + ((conditions == null) ? 0 : conditions.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Where other = (Where) obj; if (ancestor == null) { if (other.ancestor != null) { return false; } } else if (!ancestor.equals(other.ancestor)) { return false; } if (conditions == null) { if (other.conditions != null) { return false; } } else if (!conditions.equals(other.conditions)) { return false; } return true; } @Override public String toString() { return "Where [conditions=" + conditions + ", ancestor=" + ancestor + "]"; } } public static class Ancestor { private final Evaluator e; public Ancestor(Evaluator e) { super(); this.e = e; } public Key ancestorKey(Map<String, Object> context) { Object val = e.evaluate(context); if (val instanceof Key) { return (Key) val; } else if (val instanceof Entity) { return ((Entity) val).getKey(); } else { throw new GqlQueryException("Invalid GQL query string. ANCESTOR IS must be followed by Key or Entity"); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((e == null) ? 0 : e.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Ancestor other = (Ancestor) obj; if (e == null) { if (other.e != null) { return false; } } else if (!e.equals(other.e)) { return false; } return true; } @Override public String toString() { return "Ancestor [e=" + e + "]"; } } public static class OrderBy { private final List<OrderByItem> items; public OrderBy() { items = Lists.newLinkedList(); } public OrderBy withItem(OrderByItem item) { this.items.add(item); return this; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((items == null) ? 0 : items.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } OrderBy other = (OrderBy) obj; if (items == null) { if (other.items != null) { return false; } } else if (!items.equals(other.items)) { return false; } return true; } @Override public String toString() { return "OrderBy [items=" + items + "]"; } } public static class Limit { private final Integer limit; public Limit(Integer limit) { super(); this.limit = limit; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((limit == null) ? 0 : limit.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Limit other = (Limit) obj; if (limit == null) { if (other.limit != null) { return false; } } else if (!limit.equals(other.limit)) { return false; } return true; } @Override public String toString() { return "Limit [limit=" + limit + "]"; } } public static class Offset { private final Integer offset; public Offset(Integer offset) { super(); this.offset = offset; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((offset == null) ? 0 : offset.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Offset other = (Offset) obj; if (offset == null) { if (other.offset != null) { return false; } } else if (!offset.equals(other.offset)) { return false; } return true; } @Override public String toString() { return "Offset [offset=" + offset + "]"; } } /** * where condition. * * @author Max Zhu (thebbsky@gmail.com) */ public static class Condition { private String propertyName; private FilterOperator operator; private Evaluator e; public Condition(String propertyName, FilterOperator operator, Evaluator e) { super(); Preconditions.checkNotNull(propertyName); Preconditions.checkNotNull(operator); Preconditions.checkNotNull(e); this.propertyName = propertyName; this.operator = operator; this.e = e; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((e == null) ? 0 : e.hashCode()); result = prime * result + ((operator == null) ? 0 : operator.hashCode()); result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Condition other = (Condition) obj; if (e == null) { if (other.e != null) { return false; } } else if (!e.equals(other.e)) { return false; } if (operator != other.operator) { return false; } if (propertyName == null) { if (other.propertyName != null) { return false; } } else if (!propertyName.equals(other.propertyName)) { return false; } return true; } @Override public String toString() { return "Condition [propertyName=" + propertyName + ", operator=" + operator + ", e=" + e + "]"; } } /** * @author Max Zhu (thebbsky@gmail.com) */ public static class OrderByItem { private String propertyName; private SortDirection direction; public OrderByItem(String propertyName) { super(); this.propertyName = propertyName; this.direction = SortDirection.ASCENDING; } public OrderByItem setDirection(boolean ascending) { this.direction = ascending ? SortDirection.ASCENDING : SortDirection.DESCENDING; return this; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((direction == null) ? 0 : direction.hashCode()); result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } OrderByItem other = (OrderByItem) obj; if (direction != other.direction) { return false; } if (propertyName == null) { if (other.propertyName != null) { return false; } } else if (!propertyName.equals(other.propertyName)) { return false; } return true; } @Override public String toString() { return "OrderByItem [propertyName=" + propertyName + ", direction=" + direction + "]"; } } public interface Evaluator { Object evaluate(Map<String, Object> context); } public static class NullEvaluator implements Evaluator { private static NullEvaluator singleton; private NullEvaluator() { } public static synchronized NullEvaluator get() { if (singleton == null) { singleton = new NullEvaluator(); } return singleton; } @Override public Object evaluate(Map<String, Object> context) { return null; } } public static class DecimalEvaluator implements Evaluator { private final BigDecimal payload; public DecimalEvaluator(String strNumber) { payload = new BigDecimal(strNumber); } @Override public Object evaluate(Map<String, Object> context) { if ((double) payload.longValue() == payload.doubleValue()) { return this.payload.longValue(); } else { return this.payload.doubleValue(); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (payload.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } DecimalEvaluator other = (DecimalEvaluator) obj; return payload.equals(other.payload); } @Override public String toString() { return "DecimalEvaluator [payload=" + payload + "]"; } } public static class StringEvaluator implements Evaluator { private final String payload; public StringEvaluator(String rawText) { super(); // remove single quote String withoutQuote = rawText.substring(1, rawText.length() - 1); // replace \' with ' this.payload = withoutQuote.replace("\\'", "'"); } @Override public Object evaluate(Map<String, Object> context) { return this.payload; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((payload == null) ? 0 : payload.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } StringEvaluator other = (StringEvaluator) obj; if (payload == null) { if (other.payload != null) { return false; } } else if (!payload.equals(other.payload)) { return false; } return true; } @Override public String toString() { return "StringEvaluator [payload=" + payload + "]"; } } public static class BooleanEvaluator implements Evaluator { private final Boolean payload; public BooleanEvaluator(String input) { this.payload = Boolean.valueOf(input); } @Override public Object evaluate(Map<String, Object> context) { return this.payload; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((payload == null) ? 0 : payload.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } BooleanEvaluator other = (BooleanEvaluator) obj; if (payload == null) { if (other.payload != null) { return false; } } else if (!payload.equals(other.payload)) { return false; } return true; } @Override public String toString() { return "BooleanEvaluator [payload=" + payload + "]"; } } public static class FunctionEvaluator implements Evaluator { public enum Type { DATETIME, DATE, TIME, KEY, USER, GEOPT } private static final String DEFAULT_AUTH_DOMAIN = "gmail.com"; private static final DateFormat timeFmter = new SimpleDateFormat("HH:mm:ss"); private static final DateFormat dateFmter = new SimpleDateFormat("yyyy-MM-dd"); private static final DateFormat datetimeFmter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private final Type type; private final List<Evaluator> ops; public FunctionEvaluator(String type, Evaluator... ops) { this.type = Type.valueOf(type.toUpperCase()); this.ops = Lists.newArrayList(ops); } public FunctionEvaluator(String type, List<Evaluator> ops) { this.type = Type.valueOf(type.toUpperCase()); this.ops = ops; } public FunctionEvaluator withParam(Evaluator e) { this.ops.add(e); return this; } @Override public Object evaluate(Map<String, Object> context) { switch (this.type) { case DATETIME: return datetime(context); case DATE: return date(context); case TIME: return time(context); case GEOPT: return geopt(context); case KEY: return key(context); case USER: return user(context); default: // not supposed to happen throw new GqlQueryException("Invalid function type " + this.type); } } private Object user(Map<String, Object> context) { if (this.ops.size() == 1) { Object val = this.ops.get(0).evaluate(context); if (val instanceof String) { return new User((String) val, DEFAULT_AUTH_DOMAIN); } else { throw new GqlQueryException("Invalid GQL query string. Function key: invalid input"); } } else { throw new GqlQueryException("Invalid GQL query string. Function key: wrong number of arguments"); } } private Object key(Map<String, Object> context) { if (this.ops.isEmpty()) { throw new GqlQueryException("Invalid GQL query string. Function key: wrong number of arguments"); } else if (this.ops.size() == 1) { // KEY('encoded key') String keyString = (String) this.ops.get(0).evaluate(context); return KeyFactory.stringToKey(keyString); } else if (this.ops.size() % 2 == 0) { // KEY('kind', 'name'/ID [, 'kind', 'name'/ID...]) try { Key key = null; Iterator<Evaluator> i = this.ops.iterator(); while (i.hasNext()) { String kind = (String) i.next().evaluate(context); Object nameId = i.next().evaluate(context); if (nameId instanceof String) { key = KeyFactory.createKey(key, kind, (String) nameId); } else if (nameId instanceof Long) { key = KeyFactory.createKey(key, kind, (Long) nameId); } } return key; } catch (ClassCastException e) { throw new GqlQueryException("Invalid GQL query string. Function key: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function key: wrong number of arguments"); } } private Object geopt(Map<String, Object> context) { return null; } private Object time(Map<String, Object> context) { if (this.ops.size() == 1) { // TIME('HH:MM:SS') Object r = this.ops.get(0).evaluate(context); if (r instanceof String) { try { return timeFmter.parse((String) r); } catch (ParseException e) { throw new GqlQueryException("Invalid GQL query string. Function time: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function time: invalid input"); } } else if (this.ops.size() == 3) { // TIME(hour, minute, second) try { int hour = ((Number) this.ops.get(0).evaluate(context)).intValue(); int minute = ((Number) this.ops.get(1).evaluate(context)).intValue(); int second = ((Number) this.ops.get(2).evaluate(context)).intValue(); Calendar c = Calendar.getInstance(); c.set(Calendar.HOUR, hour); c.set(Calendar.MINUTE, minute); c.set(Calendar.SECOND, second); return c.getTime(); } catch (ClassCastException e) { throw new GqlQueryException("Invalid GQL query string. Function time: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function time: wrong number of arguments"); } } private Object date(Map<String, Object> context) { if (this.ops.size() == 1) { // DATE('YYYY-MM-DD') Object r = this.ops.get(0).evaluate(context); if (r instanceof String) { try { return dateFmter.parse((String) r); } catch (ParseException e) { throw new GqlQueryException("Invalid GQL query string. Function date: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function date: invalid input"); } } else if (this.ops.size() == 3) { // DATE(year, month, day) try { int year = ((Number) this.ops.get(0).evaluate(context)).intValue(); int month = ((Number) this.ops.get(1).evaluate(context)).intValue(); int day = ((Number) this.ops.get(2).evaluate(context)).intValue(); return createDate(year, month, day, 0, 0, 0); } catch (ClassCastException e) { throw new GqlQueryException("Invalid GQL query string. Function date: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function date: wrong number of arguments"); } } private Object datetime(Map<String, Object> context) { if (this.ops.size() == 1) { // DATETIME('YYYY-MM-DD HH:MM:SS') Object r = this.ops.get(0).evaluate(context); if (r instanceof String) { try { return datetimeFmter.parse((String) r); } catch (ParseException e) { throw new GqlQueryException("Invalid GQL query string. Function datetime: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function datetime: invalid input"); } } else if (this.ops.size() == 6) { // DATETIME(year, month, day, hour, minute, second) try { int year = ((Number) this.ops.get(0).evaluate(context)).intValue(); int month = ((Number) this.ops.get(1).evaluate(context)).intValue(); int day = ((Number) this.ops.get(2).evaluate(context)).intValue(); int hour = ((Number) this.ops.get(3).evaluate(context)).intValue(); int minute = ((Number) this.ops.get(4).evaluate(context)).intValue(); int second = ((Number) this.ops.get(5).evaluate(context)).intValue(); return createDate(year, month, day, hour, minute, second); } catch (ClassCastException e) { throw new GqlQueryException("Invalid GQL query string. Function datetime: invalid input", e); } } else { throw new GqlQueryException("Invalid GQL query string. Function datetime: wrong number of arguments"); } } private Date createDate(int year, int month, int day, int hour, int minute, int second) { Calendar c = Calendar.getInstance(); c.clear(); c.set(Calendar.YEAR, year); c.set(Calendar.MONTH, month); c.set(Calendar.DATE, day); c.set(Calendar.HOUR, hour); c.set(Calendar.MINUTE, minute); c.set(Calendar.SECOND, second); return c.getTime(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((ops == null) ? 0 : ops.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } FunctionEvaluator other = (FunctionEvaluator) obj; if (ops == null) { if (other.ops != null) { return false; } } else if (!ops.equals(other.ops)) { return false; } return type == other.type; } @Override public String toString() { return "FunctionEvaluator [type=" + type + ", ops=" + ops + "]"; } } public static class ParamEvaluator implements Evaluator { private final String paramName; public ParamEvaluator(String paramName) { super(); this.paramName = paramName; } @Override public Object evaluate(Map<String, Object> context) { return context.get(paramName); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((paramName == null) ? 0 : paramName.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ParamEvaluator other = (ParamEvaluator) obj; if (paramName == null) { if (other.paramName != null) { return false; } } else if (!paramName.equals(other.paramName)) { return false; } return true; } @Override public String toString() { return "ParamEvaluator [paramName=" + paramName + "]"; } } public static class ListEvaluator implements Evaluator { private final List<Evaluator> evaluators; public ListEvaluator(Evaluator... evaluators) { super(); this.evaluators = Lists.newArrayList(evaluators); } public ListEvaluator(List<Evaluator> evaluators) { super(); this.evaluators = evaluators; } @Override public Object evaluate(Map<String, Object> context) { List<Object> result = new ArrayList<>(evaluators.size()); for (Evaluator e : evaluators) { result.add(e.evaluate(context)); } return result; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((evaluators == null) ? 0 : evaluators.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ListEvaluator other = (ListEvaluator) obj; if (evaluators == null) { if (other.evaluators != null) { return false; } } else if (!evaluators.equals(other.evaluators)) { return false; } return true; } @Override public String toString() { return "ListEvaluator [evaluators=" + evaluators + "]"; } } }