From fdeff8480b3b57f72a42659c30369b6d7a1a9299 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 18 Nov 2016 00:03:34 +0000 Subject: reorg of expression classes git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1058 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/expr/Expression.java | 30 + .../jackcess/expr/Function.java | 27 + .../jackcess/expr/RowContext.java | 29 + .../healthmarketscience/jackcess/expr/Value.java | 45 + .../jackcess/impl/expr/BuiltinOperators.java | 440 +++++ .../jackcess/impl/expr/DefaultFunctions.java | 155 ++ .../jackcess/impl/expr/ExpressionTokenizer.java | 541 ++++++ .../jackcess/impl/expr/Expressionator.java | 1765 ++++++++++++++++++++ .../jackcess/util/BuiltinOperators.java | 437 ----- .../jackcess/util/DefaultFunctions.java | 152 -- .../jackcess/util/Expression.java | 68 - .../jackcess/util/ExpressionTokenizer.java | 541 ------ .../jackcess/util/Expressionator.java | 1762 ------------------- 13 files changed, 3032 insertions(+), 2960 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/expr/Expression.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/expr/Function.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/expr/RowContext.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/expr/Value.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java delete mode 100644 src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java delete mode 100644 src/main/java/com/healthmarketscience/jackcess/util/DefaultFunctions.java delete mode 100644 src/main/java/com/healthmarketscience/jackcess/util/Expression.java delete mode 100644 src/main/java/com/healthmarketscience/jackcess/util/ExpressionTokenizer.java delete mode 100644 src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java (limited to 'src/main/java') diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Expression.java b/src/main/java/com/healthmarketscience/jackcess/expr/Expression.java new file mode 100644 index 0000000..c5accc5 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Expression.java @@ -0,0 +1,30 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.expr; + +/** + * + * @author James Ahlborn + */ +public interface Expression +{ + public Object evalDefault(); + + public Boolean evalCondition(RowContext ctx); + + public String toDebugString(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Function.java b/src/main/java/com/healthmarketscience/jackcess/expr/Function.java new file mode 100644 index 0000000..9b250b3 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Function.java @@ -0,0 +1,27 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.expr; + +/** + * + * @author James Ahlborn + */ +public interface Function +{ + public String getName(); + public Value eval(Value... params); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/RowContext.java b/src/main/java/com/healthmarketscience/jackcess/expr/RowContext.java new file mode 100644 index 0000000..cc60f4d --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/RowContext.java @@ -0,0 +1,29 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.expr; + +/** + * + * @author James Ahlborn + */ +public interface RowContext +{ + public Value getThisColumnValue(); + + public Value getRowValue(String collectionName, String objName, + String colName); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java new file mode 100644 index 0000000..5c1d13f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java @@ -0,0 +1,45 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.expr; + +/** + * + * @author James Ahlborn + */ +public interface Value +{ + public enum Type + { + NULL, BOOLEAN, STRING, DATE, TIME, DATE_TIME, LONG, DOUBLE, BIG_INT, BIG_DEC; + + public boolean isNumeric() { + return inRange(LONG, BIG_DEC); + } + + public boolean isTemporal() { + return inRange(DATE, DATE_TIME); + } + + private boolean inRange(Type start, Type end) { + return ((start.ordinal() <= ordinal()) && (ordinal() <= end.ordinal())); + } + } + + + public Type getType(); + public Object get(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java new file mode 100644 index 0000000..aa58b97 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java @@ -0,0 +1,440 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.impl.expr; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; +import java.util.Map; +import java.util.regex.Pattern; + +import com.healthmarketscience.jackcess.RuntimeIOException; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.expr.Expression; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.RowContext; + + +/** + * + * @author James Ahlborn + */ +public class BuiltinOperators +{ + + public static final Value NULL_VAL = + new SimpleValue(Value.Type.NULL, null); + public static final Value TRUE_VAL = + new SimpleValue(Value.Type.BOOLEAN, Boolean.TRUE); + public static final Value FALSE_VAL = + new SimpleValue(Value.Type.BOOLEAN, Boolean.FALSE); + + public static class SimpleValue implements Value + { + private final Value.Type _type; + private final Object _val; + + public SimpleValue(Value.Type type, Object val) { + _type = type; + _val = val; + } + + public Value.Type getType() { + return _type; + } + + public Object get() { + return _val; + } + } + + private BuiltinOperators() {} + + // FIXME, null propagation: + // http://www.utteraccess.com/wiki/index.php/Nulls_And_Their_Behavior + // https://theaccessbuddy.wordpress.com/2012/10/24/6-logical-operators-in-ms-access-that-you-must-know-operator-types-3-of-5/ + // - number ops + // - comparison ops + // - logical ops (some "special") + // - And - can be false if one arg is false + // - Or - can be true if one arg is true + // - between, not, like, in + // - *NOT* concal op '&' + // FIXME, Imp operator? + + public static Value negate(Value param1) { + // FIXME + return null; + } + + public static Value add(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value subtract(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value multiply(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value divide(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value intDivide(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value exp(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value concat(Value param1, Value param2) { + // note, this op converts null to empty string + + + // FIXME + return null; + } + + public static Value mod(Value param1, Value param2) { + // FIXME + return null; + } + + public static Value not(Value param1) { + if(paramIsNull(param1)) { + // null propagation + return NULL_VAL; + } + + return toValue(!nonNullValueToBoolean(param1)); + } + + public static Value lessThan(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullCompareTo(param1, param2) < 0); + } + + public static Value greaterThan(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullCompareTo(param1, param2) > 0); + } + + public static Value lessThanEq(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullCompareTo(param1, param2) <= 0); + } + + public static Value greaterThanEq(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullCompareTo(param1, param2) >= 0); + } + + public static Value equals(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullCompareTo(param1, param2) == 0); + } + + public static Value notEquals(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullCompareTo(param1, param2) != 0); + } + + public static Value and(Value param1, Value param2) { + + // "and" uses short-circuit logic + + if(paramIsNull(param1)) { + return NULL_VAL; + } + + boolean b1 = nonNullValueToBoolean(param1); + if(!b1) { + return FALSE_VAL; + } + + if(paramIsNull(param2)) { + return NULL_VAL; + } + + return toValue(nonNullValueToBoolean(param2)); + } + + public static Value or(Value param1, Value param2) { + + // "or" uses short-circuit logic + + if(paramIsNull(param1)) { + return NULL_VAL; + } + + boolean b1 = nonNullValueToBoolean(param1); + if(b1) { + return TRUE_VAL; + } + + if(paramIsNull(param2)) { + return NULL_VAL; + } + + return toValue(nonNullValueToBoolean(param2)); + } + + public static Value eqv(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + boolean b1 = nonNullValueToBoolean(param1); + boolean b2 = nonNullValueToBoolean(param2); + + return toValue(b1 == b2); + } + + public static Value xor(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + boolean b1 = nonNullValueToBoolean(param1); + boolean b2 = nonNullValueToBoolean(param2); + + return toValue(b1 ^ b2); + } + + public static Value imp(Value param1, Value param2) { + + // "imp" uses short-circuit logic + + if(paramIsNull(param1)) { + if(paramIsNull(param2) || !nonNullValueToBoolean(param2)) { + // null propagation + return NULL_VAL; + } + + return TRUE_VAL; + } + + boolean b1 = nonNullValueToBoolean(param1); + if(!b1) { + return TRUE_VAL; + } + + if(paramIsNull(param2)) { + // null propagation + return NULL_VAL; + } + + return toValue(nonNullValueToBoolean(param2)); + } + + public static Value isNull(Value param1) { + return toValue(param1.getType() == Value.Type.NULL); + } + + public static Value isNotNull(Value param1) { + return toValue(param1.getType() == Value.Type.NULL); + } + + public static Value like(Value param1, Pattern pattern) { + // FIXME + return null; + } + + public static Value between(Value param1, Value param2, Value param3) { + + // null propagate any field left to right. uses short circuit eval + if(anyParamIsNull(param1, param2, param3)) { + // null propagation + return NULL_VAL; + } + + // FIXME + return null; + } + + public static Value notBetween(Value param1, Value param2, Value param3) { + return not(between(param1, param2, param3)); + } + + public static Value in(Value param1, Value[] params) { + + // null propagate any param. uses short circuit eval of params + if(paramIsNull(param1)) { + // null propagation + return NULL_VAL; + } + + for(Value val : params) { + if(paramIsNull(val)) { + continue; + } + + // FIXME test + } + + // FIXME + return null; + } + + public static Value notIn(Value param1, Value[] params) { + return not(in(param1, params)); + } + + + private static boolean anyParamIsNull(Value param1, Value param2) { + return (paramIsNull(param1) || paramIsNull(param2)); + } + + private static boolean anyParamIsNull(Value param1, Value param2, + Value param3) { + return (paramIsNull(param1) || paramIsNull(param2) || paramIsNull(param3)); + } + + private static boolean paramIsNull(Value param1) { + return (param1.getType() == Value.Type.NULL); + } + + protected static CharSequence paramToString(Object param) { + try { + return ColumnImpl.toCharSequence(param); + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + + protected static boolean paramToBoolean(Object param) { + // FIXME, null is false...? + return ColumnImpl.toBooleanValue(param); + } + + protected static Number paramToNumber(Object param) { + // FIXME + return null; + } + + protected static boolean nonNullValueToBoolean(Value val) { + switch(val.getType()) { + case BOOLEAN: + return (Boolean)val.get(); + case STRING: + case DATE: + case TIME: + case DATE_TIME: + // strings and dates are always true + return true; + case LONG: + return (((Number)val.get()).longValue() != 0L); + case DOUBLE: + return (((Number)val.get()).doubleValue() != 0.0d); + case BIG_INT: + return (((BigInteger)val.get()).compareTo(BigInteger.ZERO) != 0L); + case BIG_DEC: + return (((BigDecimal)val.get()).compareTo(BigDecimal.ZERO) != 0L); + default: + throw new RuntimeException("Unexpected type " + val.getType()); + } + } + + protected static int nonNullCompareTo( + Value param1, Value param2) + { + // FIXME + return 0; + } + + public static Value toValue(boolean b) { + return (b ? TRUE_VAL : FALSE_VAL); + } + + public static Value toValue(Object obj) { + if(obj == null) { + return NULL_VAL; + } + + if(obj instanceof Value) { + return (Value)obj; + } + + if(obj instanceof Boolean) { + return (((Boolean)obj) ? TRUE_VAL : FALSE_VAL); + } + + if(obj instanceof Date) { + // any way to figure out whether it's a date/time/dateTime? + return new SimpleValue(Value.Type.DATE_TIME, obj); + } + + if(obj instanceof Number) { + if((obj instanceof Double) || (obj instanceof Float)) { + return new SimpleValue(Value.Type.DOUBLE, obj); + } + if(obj instanceof BigDecimal) { + return new SimpleValue(Value.Type.BIG_DEC, obj); + } + if(obj instanceof BigInteger) { + return new SimpleValue(Value.Type.BIG_INT, obj); + } + return new SimpleValue(Value.Type.LONG, obj); + } + + try { + return new SimpleValue(Value.Type.STRING, + ColumnImpl.toCharSequence(obj).toString()); + } catch(IOException e) { + throw new RuntimeIOException(e); + } + } + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java new file mode 100644 index 0000000..7031f0e --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java @@ -0,0 +1,155 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.impl.expr; + +import java.util.HashMap; +import java.util.Map; + + +import com.healthmarketscience.jackcess.expr.Expression; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.RowContext; + +/** + * + * @author James Ahlborn + */ +public class DefaultFunctions +{ + private static final Map FUNCS = + new HashMap(); + + private DefaultFunctions() {} + + public static Function getFunction(String name) { + return FUNCS.get(name.toLowerCase()); + } + + public static abstract class BaseFunction implements Function + { + private final String _name; + private final int _minParams; + private final int _maxParams; + + protected BaseFunction(String name, int minParams, int maxParams) + { + _name = name; + _minParams = minParams; + _maxParams = maxParams; + } + + public String getName() { + return _name; + } + + protected void validateNumParams(Value[] params) { + int num = params.length; + if((num < _minParams) || (num > _maxParams)) { + String range = ((_minParams == _maxParams) ? "" + _minParams : + _minParams + " to " + _maxParams); + throw new IllegalArgumentException( + this + ": invalid number of parameters " + + num + " passed, expected " + range); + } + } + + protected static CharSequence paramToString(Object param) + { + return BuiltinOperators.paramToString(param); + } + + protected static boolean paramToBoolean(Object param) + { + return BuiltinOperators.paramToBoolean(param); + } + + protected static Number paramToNumber(Object param) + { + return BuiltinOperators.paramToNumber(param); + } + + @Override + public String toString() { + return getName() + "()"; + } + } + + public static abstract class Func1 extends BaseFunction + { + protected Func1(String name) { + super(name, 1, 1); + } + + public final Value eval(Value... params) { + validateNumParams(params); + return eval1(params[0]); + } + + protected abstract Value eval1(Value param); + } + + public static abstract class Func2 extends BaseFunction + { + protected Func2(String name) { + super(name, 2, 2); + } + + public final Value eval(Value... params) { + validateNumParams(params); + return eval2(params[0], params[1]); + } + + protected abstract Value eval2(Value param1, Value param2); + } + + public static abstract class Func3 extends BaseFunction + { + protected Func3(String name) { + super(name, 3, 3); + } + + public final Value eval(Value... params) { + validateNumParams(params); + return eval3(params[0], params[1], params[2]); + } + + protected abstract Value eval3(Value param1, Value param2, Value param3); + } + + public static final Function IIF = new Func3("IIf") { + @Override + protected Value eval3(Value param1, Value param2, Value param3) { + // FIXME + // return (paramToBoolean(param1) ? param2 : param3); + return null; + } + }; + + // https://www.techonthenet.com/access/functions/ + // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83 + + private static void registerFunc(Function func) { + if(FUNCS.put(func.getName().toLowerCase(), func) != null) { + throw new IllegalStateException("Duplicate function " + func); + } + } + + static { + registerFunc(IIF); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java new file mode 100644 index 0000000..40418f4 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -0,0 +1,541 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.healthmarketscience.jackcess.impl.expr.Expressionator.*; +import com.healthmarketscience.jackcess.expr.Value; + + +/** + * + * @author James Ahlborn + */ +class ExpressionTokenizer +{ + private static final int EOF = -1; + private static final char QUOTED_STR_CHAR = '"'; + private static final char OBJ_NAME_START_CHAR = '['; + private static final char OBJ_NAME_END_CHAR = ']'; + private static final char DATE_LIT_QUOTE_CHAR = '#'; + private static final char EQUALS_CHAR = '='; + + private static final String DATE_FORMAT = "M/d/yyyy"; + private static final String TIME_FORMAT = "HH:mm:ss"; + private static final String DATE_TIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT; + + private static final byte IS_OP_FLAG = 0x01; + private static final byte IS_COMP_FLAG = 0x02; + private static final byte IS_DELIM_FLAG = 0x04; + private static final byte IS_SPACE_FLAG = 0x08; + private static final byte IS_QUOTE_FLAG = 0x10; + + enum TokenType { + OBJ_NAME, LITERAL, OP, DELIM, STRING, SPACE; + } + + private static final byte[] CHAR_FLAGS = new byte[128]; + private static final Set TWO_CHAR_COMP_OPS = new HashSet( + Arrays.asList("<=", ">=", "<>")); + + static { + setCharFlag(IS_OP_FLAG, '+', '-', '*', '/', '\\', '^', '&'); + setCharFlag(IS_COMP_FLAG, '<', '>', '='); + setCharFlag(IS_DELIM_FLAG, '.', '!', ',', '(', ')'); + setCharFlag(IS_SPACE_FLAG, ' ', '\n', '\r', '\t'); + setCharFlag(IS_QUOTE_FLAG, '"', '#', '[', ']'); + } + + /** + * Tokenizes an expression string of the given type and (optionally) in the + * context of the relevant database. + */ + static List tokenize(Type exprType, String exprStr, + ParseContext context) { + + if(exprStr != null) { + exprStr = exprStr.trim(); + } + + if((exprStr == null) || (exprStr.length() == 0)) { + return null; + } + + List tokens = new ArrayList(); + + ExprBuf buf = new ExprBuf(exprStr); + + while(buf.hasNext()) { + char c = buf.next(); + + byte charFlag = getCharFlag(c); + if(charFlag != 0) { + + // what could it be? + switch(charFlag) { + case IS_OP_FLAG: + + // special case '-' for negative number + Token numLit = maybeParseNumberLiteral(c, buf); + if(numLit != null) { + tokens.add(numLit); + continue; + } + + // all simple operator chars are single character operators + tokens.add(new Token(TokenType.OP, String.valueOf(c))); + break; + + case IS_COMP_FLAG: + + switch(exprType) { + case DEFAULT_VALUE: + + // special case + if((c == EQUALS_CHAR) && (buf.prevPos() == 0)) { + // a leading equals sign indicates how a default value should be + // evaluated + tokens.add(new Token(TokenType.OP, String.valueOf(c))); + continue; + } + // def values can't have cond at top level + throw new IllegalArgumentException( + exprType + " cannot have top-level conditional " + buf); + + case FIELD_VALIDATOR: + case RECORD_VALIDATOR: + + tokens.add(new Token(TokenType.OP, parseCompOp(c, buf))); + break; + } + + break; + + case IS_DELIM_FLAG: + + // all delimiter chars are single character symbols + tokens.add(new Token(TokenType.DELIM, String.valueOf(c))); + break; + + case IS_SPACE_FLAG: + + // normalize whitespace into single space + consumeWhitespace(buf); + tokens.add(new Token(TokenType.SPACE, " ")); + break; + + case IS_QUOTE_FLAG: + + switch(c) { + case QUOTED_STR_CHAR: + tokens.add(new Token(TokenType.LITERAL, null, parseQuotedString(buf), + Value.Type.STRING)); + break; + case DATE_LIT_QUOTE_CHAR: + tokens.add(parseDateLiteralString(buf, context)); + break; + case OBJ_NAME_START_CHAR: + tokens.add(new Token(TokenType.OBJ_NAME, parseObjNameString(buf))); + break; + default: + throw new IllegalArgumentException( + "Invalid leading quote character " + c + " " + buf); + } + + break; + + default: + throw new RuntimeException("unknown char flag " + charFlag); + } + + } else { + + if(isDigit(c)) { + Token numLit = maybeParseNumberLiteral(c, buf); + if(numLit != null) { + tokens.add(numLit); + continue; + } + } + + // standalone word of some sort + String str = parseBareString(c, buf, exprType); + tokens.add(new Token(TokenType.STRING, str)); + } + + } + + return tokens; + } + + private static byte getCharFlag(char c) { + return ((c < 128) ? CHAR_FLAGS[c] : 0); + } + + private static boolean isSpecialChar(char c) { + return (getCharFlag(c) != 0); + } + + private static String parseCompOp(char firstChar, ExprBuf buf) { + String opStr = String.valueOf(firstChar); + + int c = buf.peekNext(); + if((c != EOF) && hasFlag(getCharFlag((char)c), IS_COMP_FLAG)) { + + // is the combo a valid comparison operator? + String tmpStr = opStr + (char)c; + if(TWO_CHAR_COMP_OPS.contains(tmpStr)) { + opStr = tmpStr; + buf.next(); + } + } + + return opStr; + } + + private static void consumeWhitespace(ExprBuf buf) { + int c = EOF; + while(((c = buf.peekNext()) != EOF) && + hasFlag(getCharFlag((char)c), IS_SPACE_FLAG)) { + buf.next(); + } + } + + private static String parseBareString(char firstChar, ExprBuf buf, + Type exprType) { + StringBuilder sb = buf.getScratchBuffer().append(firstChar); + + byte stopFlags = (IS_OP_FLAG | IS_DELIM_FLAG | IS_SPACE_FLAG); + if(exprType == Type.FIELD_VALIDATOR) { + stopFlags |= IS_COMP_FLAG; + } + + while(buf.hasNext()) { + char c = buf.next(); + byte charFlag = getCharFlag(c); + if(hasFlag(charFlag, stopFlags)) { + buf.popPrev(); + break; + } + sb.append(c); + } + + return sb.toString(); + } + + private static String parseQuotedString(ExprBuf buf) { + StringBuilder sb = buf.getScratchBuffer(); + + boolean complete = false; + while(buf.hasNext()) { + char c = buf.next(); + if(c == QUOTED_STR_CHAR) { + int nc = buf.peekNext(); + if(nc == QUOTED_STR_CHAR) { + sb.append(QUOTED_STR_CHAR); + buf.next(); + } else { + complete = true; + break; + } + } + + sb.append(c); + } + + if(!complete) { + throw new IllegalArgumentException("Missing closing '" + QUOTED_STR_CHAR + + "' for quoted string " + buf); + } + + return sb.toString(); + } + + private static String parseObjNameString(ExprBuf buf) { + return parseStringUntil(buf, OBJ_NAME_END_CHAR, OBJ_NAME_START_CHAR); + } + + private static String parseStringUntil(ExprBuf buf, char endChar, + Character startChar) + { + StringBuilder sb = buf.getScratchBuffer(); + + boolean complete = false; + while(buf.hasNext()) { + char c = buf.next(); + if(c == endChar) { + complete = true; + break; + } else if((startChar != null) && + (startChar == c)) { + throw new IllegalArgumentException("Missing closing '" + endChar + + "' for quoted string " + buf); + } + + sb.append(c); + } + + if(!complete) { + throw new IllegalArgumentException("Missing closing '" + endChar + + "' for quoted string " + buf); + } + + return sb.toString(); + } + + private static Token parseDateLiteralString( + ExprBuf buf, ParseContext context) + { + String dateStr = parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null); + + boolean hasDate = (dateStr.indexOf('/') >= 0); + boolean hasTime = (dateStr.indexOf(':') >= 0); + + SimpleDateFormat sdf = null; + Value.Type valType = null; + if(hasDate && hasTime) { + sdf = buf.getDateTimeFormat(context); + valType = Value.Type.DATE_TIME; + } else if(hasDate) { + sdf = buf.getDateFormat(context); + valType = Value.Type.DATE; + } else if(hasTime) { + sdf = buf.getTimeFormat(context); + valType = Value.Type.TIME; + } else { + throw new IllegalArgumentException("Invalid date time literal " + dateStr + + " " + buf); + } + + try { + return new Token(TokenType.LITERAL, sdf.parse(dateStr), dateStr, valType); + } catch(ParseException pe) { + throw new IllegalArgumentException( + "Invalid date time literal " + dateStr + " " + buf, pe); + } + } + + private static Token maybeParseNumberLiteral(char firstChar, ExprBuf buf) { + StringBuilder sb = buf.getScratchBuffer().append(firstChar); + boolean hasDigit = isDigit(firstChar); + + int startPos = buf.curPos(); + boolean foundNum = false; + + try { + + int c = EOF; + while((c = buf.peekNext()) != EOF) { + if(isDigit(c)) { + hasDigit = true; + sb.append((char)c); + buf.next(); + } else if(c == '.') { + sb.append((char)c); + buf.next(); + } else if(isSpecialChar((char)c)) { + break; + } else { + // found a non-number, non-special string + return null; + } + } + + if(!hasDigit) { + // no digits, no number + return null; + } + + String numStr = sb.toString(); + try { + // what number type to use here? + BigDecimal num = new BigDecimal(numStr); + foundNum = true; + return new Token(TokenType.LITERAL, num, numStr, Value.Type.BIG_DEC); + } catch(NumberFormatException ne) { + throw new IllegalArgumentException( + "Invalid number literal " + numStr + " " + buf, ne); + } + + } finally { + if(!foundNum) { + buf.reset(startPos); + } + } + } + + private static boolean hasFlag(byte charFlag, byte flag) { + return ((charFlag & flag) != 0); + } + + private static void setCharFlag(byte flag, char... chars) { + for(char c : chars) { + CHAR_FLAGS[c] |= flag; + } + } + + private static boolean isDigit(int c) { + return ((c >= '0') && (c <= '9')); + } + + static Map.Entry newEntry(K a, V b) { + return new AbstractMap.SimpleImmutableEntry(a, b); + } + + private static final class ExprBuf + { + private final String _str; + private int _pos; + private SimpleDateFormat _dateFmt; + private SimpleDateFormat _timeFmt; + private SimpleDateFormat _dateTimeFmt; + private final StringBuilder _scratch = new StringBuilder(); + + private ExprBuf(String str) { + _str = str; + } + + private int len() { + return _str.length(); + } + + public int curPos() { + return _pos; + } + + public int prevPos() { + return _pos - 1; + } + + public boolean hasNext() { + return _pos < len(); + } + + public char next() { + return _str.charAt(_pos++); + } + + public void popPrev() { + --_pos; + } + + public int peekNext() { + if(!hasNext()) { + return EOF; + } + return _str.charAt(_pos); + } + + public void reset(int pos) { + _pos = pos; + } + + public StringBuilder getScratchBuffer() { + _scratch.setLength(0); + return _scratch; + } + + public SimpleDateFormat getDateFormat(ParseContext context) { + if(_dateFmt == null) { + _dateFmt = context.createDateFormat(DATE_FORMAT); + } + return _dateFmt; + } + + public SimpleDateFormat getTimeFormat(ParseContext context) { + if(_timeFmt == null) { + _timeFmt = context.createDateFormat(TIME_FORMAT); + } + return _timeFmt; + } + + public SimpleDateFormat getDateTimeFormat(ParseContext context) { + if(_dateTimeFmt == null) { + _dateTimeFmt = context.createDateFormat(DATE_TIME_FORMAT); + } + return _dateTimeFmt; + } + + @Override + public String toString() { + return "[char " + _pos + "] '" + _str + "'"; + } + } + + + static final class Token + { + private final TokenType _type; + private final Object _val; + private final String _valStr; + private final Value.Type _valType; + + private Token(TokenType type, String val) { + this(type, val, val); + } + + private Token(TokenType type, Object val, String valStr) { + this(type, val, valStr, null); + } + + private Token(TokenType type, Object val, String valStr, Value.Type valType) { + _type = type; + _val = ((val != null) ? val : valStr); + _valStr = valStr; + _valType = valType; + } + + public TokenType getType() { + return _type; + } + + public Object getValue() { + return _val; + } + + public String getValueStr() { + return _valStr; + } + + public Value.Type getValueType() { + return _valType; + } + + @Override + public String toString() { + if(_type == TokenType.SPACE) { + return "' '"; + } + String str = "[" + _type + "] '" + _val + "'"; + if(_valType != null) { + str += " (" + _valType + ")"; + } + return str; + } + } + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java new file mode 100644 index 0000000..625358e --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java @@ -0,0 +1,1765 @@ +/* +Copyright (c) 2016 James Ahlborn + +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.healthmarketscience.jackcess.impl.expr; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.expr.Expression; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.RowContext; +import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType; +import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token; + +/** + * + * @author James Ahlborn + */ +public class Expressionator +{ + + // Useful links: + // - syntax: https://support.office.com/en-us/article/Guide-to-expression-syntax-ebc770bc-8486-4adc-a9ec-7427cce39a90 + // - examples: https://support.office.com/en-us/article/Examples-of-expressions-d3901e11-c04e-4649-b40b-8b6ec5aed41f + // - validation rule usage: https://support.office.com/en-us/article/Restrict-data-input-by-using-a-validation-rule-6c0b2ce1-76fa-4be0-8ae9-038b52652320 + + // FIXME + // - need to short-circuit AND/OR + + public enum Type { + DEFAULT_VALUE, FIELD_VALIDATOR, RECORD_VALIDATOR; + } + + public interface ParseContext { + public SimpleDateFormat createDateFormat(String formatStr); + public Function getExpressionFunction(String name); + } + + public static final ParseContext DEFAULT_PARSE_CONTEXT = new ParseContext() { + public SimpleDateFormat createDateFormat(String formatStr) { + return DatabaseBuilder.createDateFormat(formatStr); + } + public Function getExpressionFunction(String name) { + return DefaultFunctions.getFunction(name); + } + }; + + private enum WordType { + OP, COMP, LOG_OP, CONST, SPEC_OP_PREFIX, DELIM; + } + + private static final String FUNC_START_DELIM = "("; + private static final String FUNC_END_DELIM = ")"; + private static final String OPEN_PAREN = "("; + private static final String CLOSE_PAREN = ")"; + private static final String FUNC_PARAM_SEP = ","; + + private static final Map WORD_TYPES = + new HashMap(); + + static { + setWordType(WordType.OP, "+", "-", "*", "/", "\\", "^", "&", "mod"); + setWordType(WordType.COMP, "<", "<=", ">", ">=", "=", "<>"); + setWordType(WordType.LOG_OP, "and", "or", "eqv", "xor", "imp"); + setWordType(WordType.CONST, "true", "false", "null"); + setWordType(WordType.SPEC_OP_PREFIX, "is", "like", "between", "in", "not"); + // "X is null", "X is not null", "X like P", "X between A and B", + // "X not between A and B", "X in (A, B, C...)", "X not in (A, B, C...)", + // "not X" + setWordType(WordType.DELIM, ".", "!", ",", "(", ")"); + } + + private interface OpType {} + + private enum UnaryOp implements OpType { + NEG("-", false) { + @Override public Value eval(Value param1) { + return BuiltinOperators.negate(param1); + } + }, + NOT("Not", true) { + @Override public Value eval(Value param1) { + return BuiltinOperators.not(param1); + } + }; + + private final String _str; + private final boolean _needSpace; + + private UnaryOp(String str, boolean needSpace) { + _str = str; + _needSpace = needSpace; + } + + public boolean needsSpace() { + return _needSpace; + } + + @Override + public String toString() { + return _str; + } + + public abstract Value eval(Value param1); + } + + private enum BinaryOp implements OpType { + PLUS("+") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.add(param1, param2); + } + }, + MINUS("-") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.subtract(param1, param2); + } + }, + MULT("*") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.multiply(param1, param2); + } + }, + DIV("/") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.divide(param1, param2); + } + }, + INT_DIV("\\") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.intDivide(param1, param2); + } + }, + EXP("^") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.exp(param1, param2); + } + }, + CONCAT("&") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.concat(param1, param2); + } + }, + MOD("Mod") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.mod(param1, param2); + } + }; + + private final String _str; + + private BinaryOp(String str) { + _str = str; + } + + @Override + public String toString() { + return _str; + } + + public abstract Value eval(Value param1, Value param2); + } + + private enum CompOp implements OpType { + LT("<") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.lessThan(param1, param2); + } + }, + LTE("<=") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.lessThanEq(param1, param2); + } + }, + GT(">") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.greaterThan(param1, param2); + } + }, + GTE(">=") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.greaterThanEq(param1, param2); + } + }, + EQ("=") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.equals(param1, param2); + } + }, + NE("<>") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.notEquals(param1, param2); + } + }; + + private final String _str; + + private CompOp(String str) { + _str = str; + } + + @Override + public String toString() { + return _str; + } + + public abstract Value eval(Value param1, Value param2); + } + + private enum LogOp implements OpType { + AND("And") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.and(param1, param2); + } + }, + OR("Or") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.or(param1, param2); + } + }, + EQV("Eqv") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.eqv(param1, param2); + } + }, + XOR("Xor") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.xor(param1, param2); + } + }, + IMP("Imp") { + @Override public Value eval(Value param1, Value param2) { + return BuiltinOperators.imp(param1, param2); + } + }; + + private final String _str; + + private LogOp(String str) { + _str = str; + } + + @Override + public String toString() { + return _str; + } + + public abstract Value eval(Value param1, Value param2); + } + + private enum SpecOp implements OpType { + // note, "NOT" is not actually used as a special operation, always + // replaced with UnaryOp.NOT + NOT("Not") { + @Override public Value eval(Value param1, Object param2, Object param3) { + throw new UnsupportedOperationException(); + } + }, + IS_NULL("Is Null") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.isNull(param1); + } + }, + IS_NOT_NULL("Is Not Null") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.isNotNull(param1); + } + }, + LIKE("Like") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.like(param1, (Pattern)param2); + } + }, + BETWEEN("Between") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.between(param1, (Value)param2, (Value)param3); + } + }, + NOT_BETWEEN("Not Between") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.notBetween(param1, (Value)param2, (Value)param3); + } + }, + IN("In") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.in(param1, (Value[])param2); + } + }, + NOT_IN("Not In") { + @Override public Value eval(Value param1, Object param2, Object param3) { + return BuiltinOperators.notIn(param1, (Value[])param2); + } + }; + + private final String _str; + + private SpecOp(String str) { + _str = str; + } + + @Override + public String toString() { + return _str; + } + + public abstract Value eval(Value param1, Object param2, Object param3); + } + + private static final Map PRECENDENCE = + buildPrecedenceMap( + new OpType[]{BinaryOp.EXP}, + new OpType[]{UnaryOp.NEG}, + new OpType[]{BinaryOp.MULT, BinaryOp.DIV}, + new OpType[]{BinaryOp.INT_DIV}, + new OpType[]{BinaryOp.MOD}, + new OpType[]{BinaryOp.PLUS, BinaryOp.MINUS}, + new OpType[]{BinaryOp.CONCAT}, + new OpType[]{CompOp.LT, CompOp.GT, CompOp.NE, CompOp.LTE, CompOp.GTE, + CompOp.EQ, SpecOp.LIKE, SpecOp.IS_NULL, SpecOp.IS_NOT_NULL}, + new OpType[]{UnaryOp.NOT}, + new OpType[]{LogOp.AND}, + new OpType[]{LogOp.OR}, + new OpType[]{LogOp.XOR}, + new OpType[]{LogOp.EQV}, + new OpType[]{LogOp.IMP}, + new OpType[]{SpecOp.IN, SpecOp.NOT_IN, SpecOp.BETWEEN, + SpecOp.NOT_BETWEEN}); + + private static final Set REGEX_SPEC_CHARS = new HashSet( + Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&')); + + + private static final Expr THIS_COL_VALUE = new Expr() { + @Override protected Value eval(RowContext ctx) { + return ctx.getThisColumnValue(); + } + @Override protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append(""); + } + }; + + private static final Expr NULL_VALUE = new EConstValue( + BuiltinOperators.NULL_VAL, "Null"); + private static final Expr TRUE_VALUE = new EConstValue( + BuiltinOperators.TRUE_VAL, "True"); + private static final Expr FALSE_VALUE = new EConstValue( + BuiltinOperators.FALSE_VAL, "False"); + + private Expressionator() + { + } + + static String testTokenize(Type exprType, String exprStr, + ParseContext context) { + + if(context == null) { + context = DEFAULT_PARSE_CONTEXT; + } + List tokens = trimSpaces( + ExpressionTokenizer.tokenize(exprType, exprStr, context)); + + if(tokens == null) { + // FIXME, NULL_EXPR? + return null; + } + + return tokens.toString(); + } + + public static Expression parse(Type exprType, String exprStr, + ParseContext context) { + + if(context == null) { + context = DEFAULT_PARSE_CONTEXT; + } + + // FIXME,restrictions: + // - default value only accepts simple exprs, otherwise becomes literal text + // - def val cannot refer to any columns + // - field validation cannot refer to other columns + // - record validation cannot refer to outside columns + + List tokens = trimSpaces( + ExpressionTokenizer.tokenize(exprType, exprStr, context)); + + if(tokens == null) { + // FIXME, NULL_EXPR? + return null; + } + + return parseExpression(new TokBuf(exprType, tokens, context), false); + } + + private static List trimSpaces(List tokens) { + if(tokens == null) { + return null; + } + + // for the most part, spaces are superfluous except for one situation(?). + // when they appear between a string literal and '(' they help distinguish + // a function call from another expression form + for(int i = 1; i < (tokens.size() - 1); ++i) { + Token t = tokens.get(i); + if(t.getType() == TokenType.SPACE) { + if((tokens.get(i - 1).getType() == TokenType.STRING) && + isDelim(tokens.get(i + 1), FUNC_START_DELIM)) { + // we want to keep this space + } else { + tokens.remove(i); + --i; + } + } + } + return tokens; + } + + private static Expr parseExpression(TokBuf buf, boolean singleExpr) + { + while(buf.hasNext()) { + Token t = buf.next(); + + switch(t.getType()) { + case OBJ_NAME: + + parseObjectRefExpression(t, buf); + break; + + case LITERAL: + + buf.setPendingExpr(new ELiteralValue(t.getValueType(), t.getValue())); + break; + + case OP: + + WordType wordType = getWordType(t); + if(wordType == null) { + // shouldn't happen + throw new RuntimeException("Invalid operator " + t); + } + + // this can only be an OP or a COMP (those are the only words that the + // tokenizer would define as TokenType.OP) + switch(wordType) { + case OP: + parseOperatorExpression(t, buf); + break; + + case COMP: + + parseCompOpExpression(t, buf); + break; + + default: + throw new RuntimeException("Unexpected OP word type " + wordType); + } + + break; + + case DELIM: + + parseDelimExpression(t, buf); + break; + + case STRING: + + // see if it's a special word? + wordType = getWordType(t); + if(wordType == null) { + + // is it a function call? + if(!maybeParseFuncCallExpression(t, buf)) { + + // is it an object name? + Token next = buf.peekNext(); + if((next != null) && isObjNameSep(next)) { + + parseObjectRefExpression(t, buf); + + } else { + + // FIXME maybe bare obj name, maybe string literal? + throw new UnsupportedOperationException("FIXME"); + } + } + + } else { + + // this could be anything but COMP or DELIM (all COMPs would be + // returned as TokenType.OP and all DELIMs would be TokenType.DELIM) + switch(wordType) { + case OP: + + parseOperatorExpression(t, buf); + break; + + case LOG_OP: + + parseLogicalOpExpression(t, buf); + break; + + case CONST: + + parseConstExpression(t, buf); + break; + + case SPEC_OP_PREFIX: + + parseSpecOpExpression(t, buf); + break; + + default: + throw new RuntimeException("Unexpected STRING word type " + + wordType); + } + } + + break; + + case SPACE: + // top-level space is irrelevant (and we strip them anyway) + break; + + default: + throw new RuntimeException("unknown token type " + t); + } + + if(singleExpr && buf.hasPendingExpr()) { + break; + } + } + + Expr expr = buf.takePendingExpr(); + if(expr == null) { + throw new IllegalArgumentException("No expression found? " + buf); + } + + return expr; + } + + private static void parseObjectRefExpression(Token firstTok, TokBuf buf) { + + // object references may be joined by '.' or '!'. access syntac docs claim + // object identifiers can be formatted like: + // "[Collection name]![Object name].[Property name]" + // However, in practice, they only ever seem to be (at most) two levels + // and only use '.'. + Deque objNames = new LinkedList(); + objNames.add(firstTok.getValueStr()); + + Token t = null; + boolean atSep = false; + while((t = buf.peekNext()) != null) { + if(!atSep) { + if(isObjNameSep(t)) { + buf.next(); + atSep = true; + continue; + } + } else { + if((t.getType() == TokenType.OBJ_NAME) || + (t.getType() == TokenType.STRING)) { + buf.next(); + // always insert at beginning of list so names are in reverse order + objNames.addFirst(t.getValueStr()); + atSep = false; + continue; + } + } + break; + } + + if(atSep || (objNames.size() > 3)) { + throw new IllegalArgumentException("Invalid object reference " + buf); + } + + // names are in reverse order + String fieldName = objNames.poll(); + String objName = objNames.poll(); + String collectionName = objNames.poll(); + + buf.setPendingExpr( + new EObjValue(collectionName, objName, fieldName)); + } + + private static void parseDelimExpression(Token firstTok, TokBuf buf) { + // the only "top-level" delim we expect to find is open paren, and + // there shouldn't be any pending expression + if(!isDelim(firstTok, OPEN_PAREN) || buf.hasPendingExpr()) { + throw new IllegalArgumentException("Unexpected delimiter " + + firstTok.getValue() + " " + buf); + } + + Expr subExpr = findParenExprs(buf, false).get(0); + buf.setPendingExpr(new EParen(subExpr)); + } + + private static boolean maybeParseFuncCallExpression( + Token firstTok, TokBuf buf) { + + int startPos = buf.curPos(); + boolean foundFunc = false; + + try { + Token t = buf.peekNext(); + if(!isDelim(t, FUNC_START_DELIM)) { + // not a function call + return false; + } + + buf.next(); + List params = findParenExprs(buf, true); + String funcName = firstTok.getValueStr(); + Function func = buf.getFunction(funcName); + if(func == null) { + throw new IllegalArgumentException("Could not find function '" + + funcName + "' " + buf); + } + buf.setPendingExpr(new EFunc(func, params)); + foundFunc = true; + return true; + + } finally { + if(!foundFunc) { + buf.reset(startPos); + } + } + } + + private static List findParenExprs( + TokBuf buf, boolean allowMulti) { + + if(allowMulti) { + // simple case, no nested expr + Token t = buf.peekNext(); + if(isDelim(t, CLOSE_PAREN)) { + buf.next(); + return Collections.emptyList(); + } + } + + // find closing ")", handle nested parens + List exprs = new ArrayList(3); + int level = 1; + int startPos = buf.curPos(); + while(buf.hasNext()) { + + Token t = buf.next(); + + if(isDelim(t, OPEN_PAREN)) { + + ++level; + + } else if(isDelim(t, CLOSE_PAREN)) { + + --level; + if(level == 0) { + TokBuf subBuf = buf.subBuf(startPos, buf.prevPos()); + exprs.add(parseExpression(subBuf, false)); + return exprs; + } + + } else if(allowMulti && (level == 1) && isDelim(t, FUNC_PARAM_SEP)) { + + TokBuf subBuf = buf.subBuf(startPos, buf.prevPos()); + exprs.add(parseExpression(subBuf, false)); + startPos = buf.curPos(); + } + } + + throw new IllegalArgumentException("Missing closing '" + CLOSE_PAREN + + " " + buf); + } + + private static void parseOperatorExpression(Token t, TokBuf buf) { + + // most ops are two argument except that '-' could be negation + if(buf.hasPendingExpr()) { + parseBinaryOpExpression(t, buf); + } else if(isOp(t, "-")) { + parseUnaryOpExpression(t, buf); + } else { + throw new IllegalArgumentException( + "Missing left expression for binary operator " + t.getValue() + + " " + buf); + } + } + + private static void parseBinaryOpExpression(Token firstTok, TokBuf buf) { + BinaryOp op = getOpType(firstTok, BinaryOp.class); + Expr leftExpr = buf.takePendingExpr(); + Expr rightExpr = parseExpression(buf, true); + + buf.setPendingExpr(new EBinaryOp(op, leftExpr, rightExpr)); + } + + private static void parseUnaryOpExpression(Token firstTok, TokBuf buf) { + UnaryOp op = getOpType(firstTok, UnaryOp.class); + Expr val = parseExpression(buf, true); + + buf.setPendingExpr(new EUnaryOp(op, val)); + } + + private static void parseCompOpExpression(Token firstTok, TokBuf buf) { + + if(!buf.hasPendingExpr()) { + if(buf.getExprType() == Type.FIELD_VALIDATOR) { + // comparison operators for field validators can implicitly use + // the current field value for the left value + buf.setPendingExpr(THIS_COL_VALUE); + } else { + throw new IllegalArgumentException( + "Missing left expression for comparison operator " + + firstTok.getValue() + " " + buf); + } + } + + CompOp op = getOpType(firstTok, CompOp.class); + Expr leftExpr = buf.takePendingExpr(); + Expr rightExpr = parseExpression(buf, true); + + buf.setPendingExpr(new ECompOp(op, leftExpr, rightExpr)); + } + + private static void parseLogicalOpExpression(Token firstTok, TokBuf buf) { + + if(!buf.hasPendingExpr()) { + throw new IllegalArgumentException( + "Missing left expression for logical operator " + + firstTok.getValue() + " " + buf); + } + + LogOp op = getOpType(firstTok, LogOp.class); + Expr leftExpr = buf.takePendingExpr(); + Expr rightExpr = parseExpression(buf, true); + + buf.setPendingExpr(new ELogicalOp(op, leftExpr, rightExpr)); + } + + private static void parseSpecOpExpression(Token firstTok, TokBuf buf) { + + SpecOp specOp = getSpecialOperator(firstTok, buf); + + if(specOp == SpecOp.NOT) { + // this is the unary prefix operator + parseUnaryOpExpression(firstTok, buf); + return; + } + + if(!buf.hasPendingExpr()) { + if(buf.getExprType() == Type.FIELD_VALIDATOR) { + // comparison operators for field validators can implicitly use + // the current field value for the left value + buf.setPendingExpr(THIS_COL_VALUE); + } else { + throw new IllegalArgumentException( + "Missing left expression for comparison operator " + + specOp + " " + buf); + } + } + + Expr expr = buf.takePendingExpr(); + + Expr specOpExpr = null; + switch(specOp) { + case IS_NULL: + case IS_NOT_NULL: + specOpExpr = new ENullOp(specOp, expr); + break; + + case LIKE: + Token t = buf.next(); + // FIXME, create LITERAL_STRING TokenType? + if(t.getType() != TokenType.LITERAL) { + throw new IllegalArgumentException("Missing Like pattern " + buf); + } + String patternStr = t.getValueStr(); + Pattern pattern = likePatternToRegex(patternStr, buf); + specOpExpr = new ELikeOp(specOp, expr, pattern, patternStr); + break; + + case BETWEEN: + case NOT_BETWEEN: + + // the "rest" of a between expression is of the form "X And Y". we are + // going to speculatively parse forward until we find the "And" + // operator. + Expr startRangeExpr = null; + while(true) { + + Expr tmpExpr = parseExpression(buf, true); + Token tmpT = buf.peekNext(); + + if(tmpT == null) { + // ran out of expression? + throw new IllegalArgumentException( + "Missing 'And' for 'Between' expression " + buf); + } + + if(isString(tmpT, "and")) { + buf.next(); + startRangeExpr = tmpExpr; + break; + } + + // put the pending expression back and try parsing some more + buf.restorePendingExpr(tmpExpr); + } + + Expr endRangeExpr = parseExpression(buf, true); + + specOpExpr = new EBetweenOp(specOp, expr, startRangeExpr, endRangeExpr); + break; + + case IN: + case NOT_IN: + + // there might be a space before open paren + t = buf.next(); + if(t.getType() == TokenType.SPACE) { + t = buf.next(); + } + if(!isDelim(t, OPEN_PAREN)) { + throw new IllegalArgumentException("Malformed In expression " + buf); + } + + List exprs = findParenExprs(buf, true); + specOpExpr = new EInOp(specOp, expr, exprs); + break; + + default: + throw new RuntimeException("Unexpected special op " + specOp); + } + + buf.setPendingExpr(specOpExpr); + } + + private static SpecOp getSpecialOperator(Token firstTok, TokBuf buf) { + String opStr = firstTok.getValueStr().toLowerCase(); + + if("is".equals(opStr)) { + Token t = buf.peekNext(); + if(isString(t, "null")) { + buf.next(); + return SpecOp.IS_NULL; + } else if(isString(t, "not")) { + buf.next(); + t = buf.peekNext(); + if(isString(t, "null")) { + return SpecOp.IS_NOT_NULL; + } + } + } else if("like".equals(opStr)) { + return SpecOp.LIKE; + } else if("between".equals(opStr)) { + return SpecOp.BETWEEN; + } else if("in".equals(opStr)) { + return SpecOp.IN; + } else if("not".equals(opStr)) { + Token t = buf.peekNext(); + if(isString(t, "between")) { + buf.next(); + return SpecOp.NOT_BETWEEN; + } else if(isString(t, "in")) { + buf.next(); + return SpecOp.NOT_IN; + } + return SpecOp.NOT; + } + + throw new IllegalArgumentException( + "Malformed special operator " + opStr + " " + buf); + } + + private static void parseConstExpression(Token firstTok, TokBuf buf) { + Expr constExpr = null; + if("true".equalsIgnoreCase(firstTok.getValueStr())) { + constExpr = TRUE_VALUE; + } else if("false".equalsIgnoreCase(firstTok.getValueStr())) { + constExpr = FALSE_VALUE; + } else if("null".equalsIgnoreCase(firstTok.getValueStr())) { + constExpr = NULL_VALUE; + } else { + throw new RuntimeException("Unexpected CONST word " + + firstTok.getValue()); + } + buf.setPendingExpr(constExpr); + } + + private static boolean isObjNameSep(Token t) { + return (isDelim(t, ".") || isDelim(t, "!")); + } + + private static boolean isOp(Token t, String opStr) { + return ((t != null) && (t.getType() == TokenType.OP) && + opStr.equalsIgnoreCase(t.getValueStr())); + } + + private static boolean isDelim(Token t, String opStr) { + return ((t != null) && (t.getType() == TokenType.DELIM) && + opStr.equalsIgnoreCase(t.getValueStr())); + } + + private static boolean isString(Token t, String opStr) { + return ((t != null) && (t.getType() == TokenType.STRING) && + opStr.equalsIgnoreCase(t.getValueStr())); + } + + private static WordType getWordType(Token t) { + return WORD_TYPES.get(t.getValueStr().toLowerCase()); + } + + private static void setWordType(WordType type, String... words) { + for(String w : words) { + WORD_TYPES.put(w, type); + } + } + + private static > T getOpType(Token t, Class opClazz) { + String str = t.getValueStr(); + for(T op : opClazz.getEnumConstants()) { + if(str.equalsIgnoreCase(op.toString())) { + return op; + } + } + throw new IllegalArgumentException("Unexpected op string " + t.getValueStr()); + } + + private static final class TokBuf + { + private final Type _exprType; + private final List _tokens; + private final TokBuf _parent; + private final int _parentOff; + private final ParseContext _context; + private int _pos; + private Expr _pendingExpr; + private final boolean _simpleExpr; + + private TokBuf(Type exprType, List tokens, ParseContext context) { + this(exprType, false, tokens, null, 0, context); + } + + private TokBuf(List tokens, TokBuf parent, int parentOff) { + this(parent._exprType, parent._simpleExpr, tokens, parent, parentOff, + parent._context); + } + + private TokBuf(Type exprType, boolean simpleExpr, List tokens, + TokBuf parent, int parentOff, ParseContext context) { + _exprType = exprType; + _tokens = tokens; + _parent = parent; + _parentOff = parentOff; + _context = context; + if(parent == null) { + // "top-level" expression, determine if it is a simple expression or not + simpleExpr = isSimpleExpression(); + } + _simpleExpr = simpleExpr; + } + + private boolean isSimpleExpression() { + if(_exprType != Type.DEFAULT_VALUE) { + return false; + } + + // a leading "=" indicates "full" expression handling for a DEFAULT_VALUE + Token t = peekNext(); + if(isOp(t, "=")) { + next(); + return false; + } + + // this is a "simple" DEFAULT_VALUE + return true; + } + + public Type getExprType() { + return _exprType; + } + + public boolean isSimpleExpr() { + return _simpleExpr; + } + + public boolean isTopLevel() { + return (_parent == null); + } + + public int curPos() { + return _pos; + } + + public int prevPos() { + return _pos - 1; + } + + public boolean hasNext() { + return (_pos < _tokens.size()); + } + + public Token peekNext() { + if(!hasNext()) { + return null; + } + return _tokens.get(_pos); + } + + public Token next() { + if(!hasNext()) { + throw new IllegalArgumentException( + "Unexpected end of expression " + this); + } + return _tokens.get(_pos++); + } + + public void reset(int pos) { + _pos = pos; + } + + public TokBuf subBuf(int start, int end) { + return new TokBuf(_tokens.subList(start, end), this, start); + } + + public void setPendingExpr(Expr expr) { + if(_pendingExpr != null) { + throw new IllegalArgumentException( + "Found multiple expressions with no operator " + this); + } + _pendingExpr = expr.resolveOrderOfOperations(); + } + + public void restorePendingExpr(Expr expr) { + // this is an expression which was previously set, so no need to re-resolve + _pendingExpr = expr; + } + + public Expr takePendingExpr() { + Expr expr = _pendingExpr; + _pendingExpr = null; + return expr; + } + + public boolean hasPendingExpr() { + return (_pendingExpr != null); + } + + private Map.Entry> getTopPos() { + int pos = _pos; + List toks = _tokens; + TokBuf cur = this; + while(cur._parent != null) { + pos += cur._parentOff; + cur = cur._parent; + toks = cur._tokens; + } + return ExpressionTokenizer.newEntry(pos, toks); + } + + public Function getFunction(String funcName) { + return _context.getExpressionFunction(funcName); + } + + @Override + public String toString() { + + Map.Entry> e = getTopPos(); + + // TODO actually format expression? + StringBuilder sb = new StringBuilder() + .append("[token ").append(e.getKey()).append("] ("); + + for(Iterator iter = e.getValue().iterator(); iter.hasNext(); ) { + Token t = iter.next(); + sb.append("'").append(t.getValueStr()).append("'"); + if(iter.hasNext()) { + sb.append(","); + } + } + + sb.append(")"); + + if(_pendingExpr != null) { + sb.append(" [pending '").append(_pendingExpr.toDebugString()) + .append("']"); + } + + return sb.toString(); + } + } + + private static boolean isHigherPrecendence(OpType op1, OpType op2) { + int prec1 = PRECENDENCE.get(op1); + int prec2 = PRECENDENCE.get(op2); + + // higher preceendence ops have lower numbers + return (prec1 < prec2); + } + + private static final Map buildPrecedenceMap( + OpType[]... opArrs) { + Map prec = new HashMap(); + + int level = 0; + for(OpType[] ops : opArrs) { + for(OpType op : ops) { + prec.put(op, level); + } + ++level; + } + + return prec; + } + + private static void exprListToString( + List exprs, String sep, StringBuilder sb, boolean isDebug) { + Iterator iter = exprs.iterator(); + iter.next().toString(sb, isDebug); + while(iter.hasNext()) { + sb.append(sep); + iter.next().toString(sb, isDebug); + } + } + + private static Value[] exprListToValues( + List exprs, RowContext ctx) { + Value[] paramVals = new Value[exprs.size()]; + for(int i = 0; i < exprs.size(); ++i) { + paramVals[i] = exprs.get(i).eval(ctx); + } + return paramVals; + } + + private static Value[] exprListToDelayedValues( + List exprs, RowContext ctx) { + Value[] paramVals = new Value[exprs.size()]; + for(int i = 0; i < exprs.size(); ++i) { + paramVals[i] = new DelayedValue(exprs.get(i), ctx); + } + return paramVals; + } + + private static void literalStrToString(String str, StringBuilder sb) { + sb.append("\"") + .append(str.replace("\"", "\"\"")) + .append("\""); + } + + private static Pattern likePatternToRegex(String pattern, Object location) { + + StringBuilder sb = new StringBuilder(pattern.length()); + + // Access LIKE pattern supports (note, matching is case-insensitive): + // - '*' -> 0 or more chars + // - '?' -> single character + // - '#' -> single digit + // - '[...]' -> character class, '[!...]' -> not in char class + + for(int i = 0; i < pattern.length(); ++i) { + char c = pattern.charAt(i); + + if(c == '*') { + sb.append(".*"); + } else if(c == '?') { + sb.append('.'); + } else if(c == '#') { + sb.append("\\d"); + } else if(c == '[') { + + // find closing brace + int startPos = i + 1; + int endPos = -1; + for(int j = startPos; j < pattern.length(); ++j) { + if(pattern.charAt(j) == ']') { + endPos = j; + break; + } + } + + if(endPos == -1) { + throw new IllegalArgumentException( + "Could not find closing bracket in pattern '" + pattern + "' " + + location); + } + + String charClass = pattern.substring(startPos, endPos); + + if((charClass.length() > 0) && (charClass.charAt(0) == '!')) { + // this is a negated char class + charClass = '^' + charClass.substring(1); + } + + sb.append('[').append(charClass).append(']'); + + } else if(REGEX_SPEC_CHARS.contains(c)) { + // this char is special in regexes, so escape it + sb.append('\\').append(c); + } else { + sb.append(c); + } + } + + return Pattern.compile(sb.toString(), + Pattern.CASE_INSENSITIVE | Pattern.DOTALL | + Pattern.UNICODE_CASE); + } + + + private interface LeftAssocExpr { + public OpType getOp(); + public Expr getLeft(); + public void setLeft(Expr left); + } + + private interface RightAssocExpr { + public OpType getOp(); + public Expr getRight(); + public void setRight(Expr right); + } + + private static final class DelayedValue implements Value + { + private Value _val; + private final Expr _expr; + private final RowContext _ctx; + + private DelayedValue(Expr expr, RowContext ctx) { + _expr = expr; + _ctx = ctx; + } + + private Value getDelegate() { + if(_val == null) { + _val = _expr.eval(_ctx); + } + return _val; + } + + public Value.Type getType() { + return getDelegate().getType(); + } + + public Object get() { + return getDelegate().get(); + } + } + + + private static abstract class Expr implements Expression + { + public Object evalDefault() { + Value val = eval(null); + + if(val.getType() == Value.Type.NULL) { + return null; + } + + // FIXME, booleans seem to go to -1 (true),0 (false) ...? + + return val.get(); + } + + public Boolean evalCondition(RowContext ctx) { + Value val = eval(ctx); + + if(val.getType() == Value.Type.NULL) { + return null; + } + + if(val.getType() != Value.Type.BOOLEAN) { + // a single value as a conditional expression seems to act like an + // implicit "=" + // FIXME, what about row validators? + val = BuiltinOperators.equals(val, ctx.getThisColumnValue()); + } + + return (Boolean)val.get(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb, false); + return sb.toString(); + } + + public String toDebugString() { + StringBuilder sb = new StringBuilder(); + toString(sb, true); + return sb.toString(); + } + + protected void toString(StringBuilder sb, boolean isDebug) { + if(isDebug) { + sb.append("<").append(getClass().getSimpleName()).append(">{"); + } + toExprString(sb, isDebug); + if(isDebug) { + sb.append("}"); + } + } + + protected Expr resolveOrderOfOperations() { + + if(!(this instanceof LeftAssocExpr)) { + // nothing we can do + return this; + } + + // in order to get the precedence right, we need to first associate this + // expression with the "rightmost" expression preceding it, then adjust + // this expression "down" (lower precedence) as the precedence of the + // operations dictates. since we parse from left to right, the initial + // "left" value isn't the immediate left expression, instead it's based + // on how the preceding operator precedence worked out. we need to + // adjust "this" expression to the closest preceding expression before + // we can correctly resolve precedence. + + Expr outerExpr = this; + final LeftAssocExpr thisExpr = (LeftAssocExpr)this; + final Expr thisLeft = thisExpr.getLeft(); + + // current: {{A op1 B} op2 {C}} + if(thisLeft instanceof RightAssocExpr) { + + RightAssocExpr leftOp = (RightAssocExpr)thisLeft; + + // target: {A op1 {B op2 {C}}} + + thisExpr.setLeft(leftOp.getRight()); + + // give the new version of this expression an opportunity to further + // swap (since the swapped expression may itself be a binary + // expression) + leftOp.setRight(resolveOrderOfOperations()); + outerExpr = thisLeft; + + // at this point, this expression has been pushed all the way to the + // rightmost preceding expression (we artifically gave "this" the + // highest precedence). now, we want to adjust precedence as + // necessary (shift it back down if the operator precedence is + // incorrect). note, we only need to check precedence against "this", + // as all other precedence has been resolved in previous parsing + // rounds. + if((leftOp.getRight() == this) && + !isHigherPrecendence(thisExpr.getOp(), leftOp.getOp())) { + + // doh, "this" is lower (or the same) precedence, restore the + // original order of things + leftOp.setRight(thisExpr.getLeft()); + thisExpr.setLeft(thisLeft); + outerExpr = this; + } + } + + return outerExpr; + } + + protected abstract Value eval(RowContext ctx); + + protected abstract void toExprString(StringBuilder sb, boolean isDebug); + } + + private static final class EConstValue extends Expr + { + private final Value _val; + private final String _str; + + private EConstValue(Value val, String str) { + _val = val; + _str = str; + } + + @Override + protected Value eval(RowContext ctx) { + return _val; + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append(_str); + } + } + + private static final class ELiteralValue extends Expr + { + private final Value _val; + + private ELiteralValue(Value.Type valType, Object value) { + _val = new BuiltinOperators.SimpleValue(valType, value); + } + + @Override + public Value eval(RowContext ctx) { + return _val; + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + if(_val.getType() == Value.Type.STRING) { + literalStrToString((String)_val.get(), sb); + } else if(_val.getType().isTemporal()) { + // // FIXME Date,Time,DateTime formatting? + // sb.append("#").append(_value).append("#"); + throw new UnsupportedOperationException(); + } else { + sb.append(_val.get()); + } + } + } + + private static final class EObjValue extends Expr + { + private final String _collectionName; + private final String _objName; + private final String _fieldName; + + + private EObjValue(String collectionName, String objName, String fieldName) { + _collectionName = collectionName; + _objName = objName; + _fieldName = fieldName; + } + + @Override + public Value eval(RowContext ctx) { + return ctx.getRowValue(_collectionName, _objName, _fieldName); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + if(_collectionName != null) { + sb.append("[").append(_collectionName).append("]."); + } + if(_objName != null) { + sb.append("[").append(_objName).append("]."); + } + sb.append("[").append(_fieldName).append("]"); + } + } + + private static class EParen extends Expr + { + private final Expr _expr; + + private EParen(Expr expr) { + _expr = expr; + } + + @Override + protected Value eval(RowContext ctx) { + return _expr.eval(ctx); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append("("); + _expr.toString(sb, isDebug); + sb.append(")"); + } + } + + private static class EFunc extends Expr + { + private final Function _func; + private final List _params; + + private EFunc(Function func, List params) { + _func = func; + _params = params; + } + + @Override + protected Value eval(RowContext ctx) { + return _func.eval(exprListToValues(_params, ctx)); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append(_func.getName()).append("("); + + if(!_params.isEmpty()) { + exprListToString(_params, ",", sb, isDebug); + } + + sb.append(")"); + } + } + + private static abstract class EBaseBinaryOp extends Expr + implements LeftAssocExpr, RightAssocExpr + { + protected final OpType _op; + protected Expr _left; + protected Expr _right; + + private EBaseBinaryOp(OpType op, Expr left, Expr right) { + _op = op; + _left = left; + _right = right; + } + + public OpType getOp() { + return _op; + } + + public Expr getLeft() { + return _left; + } + + public void setLeft(Expr left) { + _left = left; + } + + public Expr getRight() { + return _right; + } + + public void setRight(Expr right) { + _right = right; + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + _left.toString(sb, isDebug); + sb.append(" ").append(_op).append(" "); + _right.toString(sb, isDebug); + } + } + + private static class EBinaryOp extends EBaseBinaryOp + { + private EBinaryOp(BinaryOp op, Expr left, Expr right) { + super(op, left, right); + } + + @Override + protected Value eval(RowContext ctx) { + return ((BinaryOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); + } + } + + private static class EUnaryOp extends Expr + implements RightAssocExpr + { + private final OpType _op; + private Expr _expr; + + private EUnaryOp(UnaryOp op, Expr expr) { + _op = op; + _expr = expr; + } + + public OpType getOp() { + return _op; + } + + public Expr getRight() { + return _expr; + } + + public void setRight(Expr right) { + _expr = right; + } + + @Override + protected Value eval(RowContext ctx) { + return ((UnaryOp)_op).eval(_expr.eval(ctx)); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append(_op); + if(isDebug || ((UnaryOp)_op).needsSpace()) { + sb.append(" "); + } + _expr.toString(sb, isDebug); + } + } + + private static class ECompOp extends EBaseBinaryOp + { + private ECompOp(CompOp op, Expr left, Expr right) { + super(op, left, right); + } + + @Override + protected Value eval(RowContext ctx) { + return ((CompOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); + } + } + + private static class ELogicalOp extends EBaseBinaryOp + { + private ELogicalOp(LogOp op, Expr left, Expr right) { + super(op, left, right); + } + + @Override + protected Value eval(final RowContext ctx) { + + // logical operations do short circuit evaluation, so we need to delay + // computing results until necessary + return ((LogOp)_op).eval(new DelayedValue(_left, ctx), + new DelayedValue(_right, ctx)); + } + } + + private static abstract class ESpecOp extends Expr + implements LeftAssocExpr + { + protected final SpecOp _op; + protected Expr _expr; + + private ESpecOp(SpecOp op, Expr expr) { + _op = op; + _expr = expr; + } + + public OpType getOp() { + return _op; + } + + public Expr getLeft() { + return _expr; + } + + public void setLeft(Expr left) { + _expr = left; + } + } + + private static class ENullOp extends ESpecOp + { + private ENullOp(SpecOp op, Expr expr) { + super(op, expr); + } + + @Override + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), null, null); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + _expr.toString(sb, isDebug); + sb.append(" ").append(_op); + } + } + + private static class ELikeOp extends ESpecOp + { + // FIXME, compile Pattern on first use? + private final Pattern _pattern; + private final String _patternStr; + + private ELikeOp(SpecOp op, Expr expr, Pattern pattern, String patternStr) { + super(op, expr); + _pattern = pattern; + _patternStr = patternStr; + } + + @Override + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), _pattern, null); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + _expr.toString(sb, isDebug); + sb.append(" ").append(_op).append(" "); + literalStrToString(_patternStr, sb); + if(isDebug) { + sb.append("(").append(_pattern).append(")"); + } + } + } + + private static class EInOp extends ESpecOp + { + private final List _exprs; + + private EInOp(SpecOp op, Expr expr, List exprs) { + super(op, expr); + _exprs = exprs; + } + + @Override + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), + exprListToDelayedValues(_exprs, ctx), null); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + _expr.toString(sb, isDebug); + sb.append(" ").append(_op).append(" ("); + exprListToString(_exprs, ",", sb, isDebug); + sb.append(")"); + } + } + + private static class EBetweenOp extends ESpecOp + implements RightAssocExpr + { + private final Expr _startRangeExpr; + private Expr _endRangeExpr; + + private EBetweenOp(SpecOp op, Expr expr, Expr startRangeExpr, + Expr endRangeExpr) { + super(op, expr); + _startRangeExpr = startRangeExpr; + _endRangeExpr = endRangeExpr; + } + + public Expr getRight() { + return _endRangeExpr; + } + + public void setRight(Expr right) { + _endRangeExpr = right; + } + + @Override + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), + new DelayedValue(_startRangeExpr, ctx), + new DelayedValue(_endRangeExpr, ctx)); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + _expr.toString(sb, isDebug); + sb.append(" ").append(_op).append(" "); + _startRangeExpr.toString(sb, isDebug); + sb.append(" And "); + _endRangeExpr.toString(sb, isDebug); + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java b/src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java deleted file mode 100644 index cb7eb6c..0000000 --- a/src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java +++ /dev/null @@ -1,437 +0,0 @@ -/* -Copyright (c) 2016 James Ahlborn - -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.healthmarketscience.jackcess.util; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Date; -import java.util.Map; -import java.util.regex.Pattern; - -import com.healthmarketscience.jackcess.RuntimeIOException; -import com.healthmarketscience.jackcess.impl.ColumnImpl; -import com.healthmarketscience.jackcess.util.Expression.*; - - -/** - * - * @author James Ahlborn - */ -public class BuiltinOperators -{ - - public static final Value NULL_VAL = - new SimpleValue(ValueType.NULL, null); - public static final Value TRUE_VAL = - new SimpleValue(ValueType.BOOLEAN, Boolean.TRUE); - public static final Value FALSE_VAL = - new SimpleValue(ValueType.BOOLEAN, Boolean.FALSE); - - public static class SimpleValue implements Value - { - private final ValueType _type; - private final Object _val; - - public SimpleValue(ValueType type, Object val) { - _type = type; - _val = val; - } - - public ValueType getType() { - return _type; - } - - public Object get() { - return _val; - } - } - - private BuiltinOperators() {} - - // FIXME, null propagation: - // http://www.utteraccess.com/wiki/index.php/Nulls_And_Their_Behavior - // https://theaccessbuddy.wordpress.com/2012/10/24/6-logical-operators-in-ms-access-that-you-must-know-operator-types-3-of-5/ - // - number ops - // - comparison ops - // - logical ops (some "special") - // - And - can be false if one arg is false - // - Or - can be true if one arg is true - // - between, not, like, in - // - *NOT* concal op '&' - // FIXME, Imp operator? - - public static Value negate(Value param1) { - // FIXME - return null; - } - - public static Value add(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value subtract(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value multiply(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value divide(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value intDivide(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value exp(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value concat(Value param1, Value param2) { - // note, this op converts null to empty string - - - // FIXME - return null; - } - - public static Value mod(Value param1, Value param2) { - // FIXME - return null; - } - - public static Value not(Value param1) { - if(paramIsNull(param1)) { - // null propagation - return NULL_VAL; - } - - return toValue(!nonNullValueToBoolean(param1)); - } - - public static Value lessThan(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullCompareTo(param1, param2) < 0); - } - - public static Value greaterThan(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullCompareTo(param1, param2) > 0); - } - - public static Value lessThanEq(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullCompareTo(param1, param2) <= 0); - } - - public static Value greaterThanEq(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullCompareTo(param1, param2) >= 0); - } - - public static Value equals(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullCompareTo(param1, param2) == 0); - } - - public static Value notEquals(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullCompareTo(param1, param2) != 0); - } - - public static Value and(Value param1, Value param2) { - - // "and" uses short-circuit logic - - if(paramIsNull(param1)) { - return NULL_VAL; - } - - boolean b1 = nonNullValueToBoolean(param1); - if(!b1) { - return FALSE_VAL; - } - - if(paramIsNull(param2)) { - return NULL_VAL; - } - - return toValue(nonNullValueToBoolean(param2)); - } - - public static Value or(Value param1, Value param2) { - - // "or" uses short-circuit logic - - if(paramIsNull(param1)) { - return NULL_VAL; - } - - boolean b1 = nonNullValueToBoolean(param1); - if(b1) { - return TRUE_VAL; - } - - if(paramIsNull(param2)) { - return NULL_VAL; - } - - return toValue(nonNullValueToBoolean(param2)); - } - - public static Value eqv(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - boolean b1 = nonNullValueToBoolean(param1); - boolean b2 = nonNullValueToBoolean(param2); - - return toValue(b1 == b2); - } - - public static Value xor(Value param1, Value param2) { - if(anyParamIsNull(param1, param2)) { - // null propagation - return NULL_VAL; - } - - boolean b1 = nonNullValueToBoolean(param1); - boolean b2 = nonNullValueToBoolean(param2); - - return toValue(b1 ^ b2); - } - - public static Value imp(Value param1, Value param2) { - - // "imp" uses short-circuit logic - - if(paramIsNull(param1)) { - if(paramIsNull(param2) || !nonNullValueToBoolean(param2)) { - // null propagation - return NULL_VAL; - } - - return TRUE_VAL; - } - - boolean b1 = nonNullValueToBoolean(param1); - if(!b1) { - return TRUE_VAL; - } - - if(paramIsNull(param2)) { - // null propagation - return NULL_VAL; - } - - return toValue(nonNullValueToBoolean(param2)); - } - - public static Value isNull(Value param1) { - return toValue(param1.getType() == ValueType.NULL); - } - - public static Value isNotNull(Value param1) { - return toValue(param1.getType() == ValueType.NULL); - } - - public static Value like(Value param1, Pattern pattern) { - // FIXME - return null; - } - - public static Value between(Value param1, Value param2, Value param3) { - - // null propagate any field left to right. uses short circuit eval - if(anyParamIsNull(param1, param2, param3)) { - // null propagation - return NULL_VAL; - } - - // FIXME - return null; - } - - public static Value notBetween(Value param1, Value param2, Value param3) { - return not(between(param1, param2, param3)); - } - - public static Value in(Value param1, Value[] params) { - - // null propagate any param. uses short circuit eval of params - if(paramIsNull(param1)) { - // null propagation - return NULL_VAL; - } - - for(Value val : params) { - if(paramIsNull(val)) { - continue; - } - - // FIXME test - } - - // FIXME - return null; - } - - public static Value notIn(Value param1, Value[] params) { - return not(in(param1, params)); - } - - - private static boolean anyParamIsNull(Value param1, Value param2) { - return (paramIsNull(param1) || paramIsNull(param2)); - } - - private static boolean anyParamIsNull(Value param1, Value param2, - Value param3) { - return (paramIsNull(param1) || paramIsNull(param2) || paramIsNull(param3)); - } - - private static boolean paramIsNull(Value param1) { - return (param1.getType() == ValueType.NULL); - } - - protected static CharSequence paramToString(Object param) { - try { - return ColumnImpl.toCharSequence(param); - } catch(IOException e) { - throw new RuntimeIOException(e); - } - } - - protected static boolean paramToBoolean(Object param) { - // FIXME, null is false...? - return ColumnImpl.toBooleanValue(param); - } - - protected static Number paramToNumber(Object param) { - // FIXME - return null; - } - - protected static boolean nonNullValueToBoolean(Value val) { - switch(val.getType()) { - case BOOLEAN: - return (Boolean)val.get(); - case STRING: - case DATE: - case TIME: - case DATE_TIME: - // strings and dates are always true - return true; - case LONG: - return (((Number)val.get()).longValue() != 0L); - case DOUBLE: - return (((Number)val.get()).doubleValue() != 0.0d); - case BIG_INT: - return (((BigInteger)val.get()).compareTo(BigInteger.ZERO) != 0L); - case BIG_DEC: - return (((BigDecimal)val.get()).compareTo(BigDecimal.ZERO) != 0L); - default: - throw new RuntimeException("Unexpected type " + val.getType()); - } - } - - protected static int nonNullCompareTo( - Value param1, Value param2) - { - // FIXME - return 0; - } - - public static Value toValue(boolean b) { - return (b ? TRUE_VAL : FALSE_VAL); - } - - public static Value toValue(Object obj) { - if(obj == null) { - return NULL_VAL; - } - - if(obj instanceof Value) { - return (Value)obj; - } - - if(obj instanceof Boolean) { - return (((Boolean)obj) ? TRUE_VAL : FALSE_VAL); - } - - if(obj instanceof Date) { - // any way to figure out whether it's a date/time/dateTime? - return new SimpleValue(ValueType.DATE_TIME, obj); - } - - if(obj instanceof Number) { - if((obj instanceof Double) || (obj instanceof Float)) { - return new SimpleValue(ValueType.DOUBLE, obj); - } - if(obj instanceof BigDecimal) { - return new SimpleValue(ValueType.BIG_DEC, obj); - } - if(obj instanceof BigInteger) { - return new SimpleValue(ValueType.BIG_INT, obj); - } - return new SimpleValue(ValueType.LONG, obj); - } - - try { - return new SimpleValue(ValueType.STRING, - ColumnImpl.toCharSequence(obj).toString()); - } catch(IOException e) { - throw new RuntimeIOException(e); - } - } - -} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/DefaultFunctions.java b/src/main/java/com/healthmarketscience/jackcess/util/DefaultFunctions.java deleted file mode 100644 index 3c9c292..0000000 --- a/src/main/java/com/healthmarketscience/jackcess/util/DefaultFunctions.java +++ /dev/null @@ -1,152 +0,0 @@ -/* -Copyright (c) 2016 James Ahlborn - -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.healthmarketscience.jackcess.util; - -import java.util.HashMap; -import java.util.Map; - - -import com.healthmarketscience.jackcess.util.Expression.*; - -/** - * - * @author James Ahlborn - */ -public class DefaultFunctions -{ - private static final Map FUNCS = - new HashMap(); - - private DefaultFunctions() {} - - public static Function getFunction(String name) { - return FUNCS.get(name.toLowerCase()); - } - - public static abstract class BaseFunction implements Function - { - private final String _name; - private final int _minParams; - private final int _maxParams; - - protected BaseFunction(String name, int minParams, int maxParams) - { - _name = name; - _minParams = minParams; - _maxParams = maxParams; - } - - public String getName() { - return _name; - } - - protected void validateNumParams(Value[] params) { - int num = params.length; - if((num < _minParams) || (num > _maxParams)) { - String range = ((_minParams == _maxParams) ? "" + _minParams : - _minParams + " to " + _maxParams); - throw new IllegalArgumentException( - this + ": invalid number of parameters " + - num + " passed, expected " + range); - } - } - - protected static CharSequence paramToString(Object param) - { - return BuiltinOperators.paramToString(param); - } - - protected static boolean paramToBoolean(Object param) - { - return BuiltinOperators.paramToBoolean(param); - } - - protected static Number paramToNumber(Object param) - { - return BuiltinOperators.paramToNumber(param); - } - - @Override - public String toString() { - return getName() + "()"; - } - } - - public static abstract class Func1 extends BaseFunction - { - protected Func1(String name) { - super(name, 1, 1); - } - - public final Value eval(Value... params) { - validateNumParams(params); - return eval1(params[0]); - } - - protected abstract Value eval1(Value param); - } - - public static abstract class Func2 extends BaseFunction - { - protected Func2(String name) { - super(name, 2, 2); - } - - public final Value eval(Value... params) { - validateNumParams(params); - return eval2(params[0], params[1]); - } - - protected abstract Value eval2(Value param1, Value param2); - } - - public static abstract class Func3 extends BaseFunction - { - protected Func3(String name) { - super(name, 3, 3); - } - - public final Value eval(Value... params) { - validateNumParams(params); - return eval3(params[0], params[1], params[2]); - } - - protected abstract Value eval3(Value param1, Value param2, Value param3); - } - - public static final Function IIF = new Func3("IIf") { - @Override - protected Value eval3(Value param1, Value param2, Value param3) { - // FIXME - // return (paramToBoolean(param1) ? param2 : param3); - return null; - } - }; - - // https://www.techonthenet.com/access/functions/ - // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83 - - private static void registerFunc(Function func) { - if(FUNCS.put(func.getName().toLowerCase(), func) != null) { - throw new IllegalStateException("Duplicate function " + func); - } - } - - static { - registerFunc(IIF); - } -} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/Expression.java b/src/main/java/com/healthmarketscience/jackcess/util/Expression.java deleted file mode 100644 index f2bb462..0000000 --- a/src/main/java/com/healthmarketscience/jackcess/util/Expression.java +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright (c) 2016 James Ahlborn - -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.healthmarketscience.jackcess.util; - -/** - * - * @author James Ahlborn - */ -public interface Expression -{ - public interface RowContext - { - public Value getThisColumnValue(); - - public Value getRowValue(String collectionName, String objName, - String colName); - } - - public enum ValueType - { - NULL, BOOLEAN, STRING, DATE, TIME, DATE_TIME, LONG, DOUBLE, BIG_INT, BIG_DEC; - - public boolean isNumeric() { - return inRange(LONG, BIG_DEC); - } - - public boolean isTemporal() { - return inRange(DATE, DATE_TIME); - } - - private boolean inRange(ValueType start, ValueType end) { - return ((start.ordinal() <= ordinal()) && (ordinal() <= end.ordinal())); - } - } - - public interface Value - { - public ValueType getType(); - public Object get(); - } - - public interface Function - { - public String getName(); - public Value eval(Value... params); - } - - - public Object evalDefault(); - - public Boolean evalCondition(RowContext ctx); - - public String toDebugString(); -} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/ExpressionTokenizer.java b/src/main/java/com/healthmarketscience/jackcess/util/ExpressionTokenizer.java deleted file mode 100644 index 192a62d..0000000 --- a/src/main/java/com/healthmarketscience/jackcess/util/ExpressionTokenizer.java +++ /dev/null @@ -1,541 +0,0 @@ -/* -Copyright (c) 2016 James Ahlborn - -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.healthmarketscience.jackcess.util; - -import java.math.BigDecimal; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static com.healthmarketscience.jackcess.util.Expressionator.*; -import com.healthmarketscience.jackcess.util.Expression.ValueType; - - -/** - * - * @author James Ahlborn - */ -class ExpressionTokenizer -{ - private static final int EOF = -1; - private static final char QUOTED_STR_CHAR = '"'; - private static final char OBJ_NAME_START_CHAR = '['; - private static final char OBJ_NAME_END_CHAR = ']'; - private static final char DATE_LIT_QUOTE_CHAR = '#'; - private static final char EQUALS_CHAR = '='; - - private static final String DATE_FORMAT = "M/d/yyyy"; - private static final String TIME_FORMAT = "HH:mm:ss"; - private static final String DATE_TIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT; - - private static final byte IS_OP_FLAG = 0x01; - private static final byte IS_COMP_FLAG = 0x02; - private static final byte IS_DELIM_FLAG = 0x04; - private static final byte IS_SPACE_FLAG = 0x08; - private static final byte IS_QUOTE_FLAG = 0x10; - - enum TokenType { - OBJ_NAME, LITERAL, OP, DELIM, STRING, SPACE; - } - - private static final byte[] CHAR_FLAGS = new byte[128]; - private static final Set TWO_CHAR_COMP_OPS = new HashSet( - Arrays.asList("<=", ">=", "<>")); - - static { - setCharFlag(IS_OP_FLAG, '+', '-', '*', '/', '\\', '^', '&'); - setCharFlag(IS_COMP_FLAG, '<', '>', '='); - setCharFlag(IS_DELIM_FLAG, '.', '!', ',', '(', ')'); - setCharFlag(IS_SPACE_FLAG, ' ', '\n', '\r', '\t'); - setCharFlag(IS_QUOTE_FLAG, '"', '#', '[', ']'); - } - - /** - * Tokenizes an expression string of the given type and (optionally) in the - * context of the relevant database. - */ - static List tokenize(Type exprType, String exprStr, - ParseContext context) { - - if(exprStr != null) { - exprStr = exprStr.trim(); - } - - if((exprStr == null) || (exprStr.length() == 0)) { - return null; - } - - List tokens = new ArrayList(); - - ExprBuf buf = new ExprBuf(exprStr); - - while(buf.hasNext()) { - char c = buf.next(); - - byte charFlag = getCharFlag(c); - if(charFlag != 0) { - - // what could it be? - switch(charFlag) { - case IS_OP_FLAG: - - // special case '-' for negative number - Token numLit = maybeParseNumberLiteral(c, buf); - if(numLit != null) { - tokens.add(numLit); - continue; - } - - // all simple operator chars are single character operators - tokens.add(new Token(TokenType.OP, String.valueOf(c))); - break; - - case IS_COMP_FLAG: - - switch(exprType) { - case DEFAULT_VALUE: - - // special case - if((c == EQUALS_CHAR) && (buf.prevPos() == 0)) { - // a leading equals sign indicates how a default value should be - // evaluated - tokens.add(new Token(TokenType.OP, String.valueOf(c))); - continue; - } - // def values can't have cond at top level - throw new IllegalArgumentException( - exprType + " cannot have top-level conditional " + buf); - - case FIELD_VALIDATOR: - case RECORD_VALIDATOR: - - tokens.add(new Token(TokenType.OP, parseCompOp(c, buf))); - break; - } - - break; - - case IS_DELIM_FLAG: - - // all delimiter chars are single character symbols - tokens.add(new Token(TokenType.DELIM, String.valueOf(c))); - break; - - case IS_SPACE_FLAG: - - // normalize whitespace into single space - consumeWhitespace(buf); - tokens.add(new Token(TokenType.SPACE, " ")); - break; - - case IS_QUOTE_FLAG: - - switch(c) { - case QUOTED_STR_CHAR: - tokens.add(new Token(TokenType.LITERAL, null, parseQuotedString(buf), - ValueType.STRING)); - break; - case DATE_LIT_QUOTE_CHAR: - tokens.add(parseDateLiteralString(buf, context)); - break; - case OBJ_NAME_START_CHAR: - tokens.add(new Token(TokenType.OBJ_NAME, parseObjNameString(buf))); - break; - default: - throw new IllegalArgumentException( - "Invalid leading quote character " + c + " " + buf); - } - - break; - - default: - throw new RuntimeException("unknown char flag " + charFlag); - } - - } else { - - if(isDigit(c)) { - Token numLit = maybeParseNumberLiteral(c, buf); - if(numLit != null) { - tokens.add(numLit); - continue; - } - } - - // standalone word of some sort - String str = parseBareString(c, buf, exprType); - tokens.add(new Token(TokenType.STRING, str)); - } - - } - - return tokens; - } - - private static byte getCharFlag(char c) { - return ((c < 128) ? CHAR_FLAGS[c] : 0); - } - - private static boolean isSpecialChar(char c) { - return (getCharFlag(c) != 0); - } - - private static String parseCompOp(char firstChar, ExprBuf buf) { - String opStr = String.valueOf(firstChar); - - int c = buf.peekNext(); - if((c != EOF) && hasFlag(getCharFlag((char)c), IS_COMP_FLAG)) { - - // is the combo a valid comparison operator? - String tmpStr = opStr + (char)c; - if(TWO_CHAR_COMP_OPS.contains(tmpStr)) { - opStr = tmpStr; - buf.next(); - } - } - - return opStr; - } - - private static void consumeWhitespace(ExprBuf buf) { - int c = EOF; - while(((c = buf.peekNext()) != EOF) && - hasFlag(getCharFlag((char)c), IS_SPACE_FLAG)) { - buf.next(); - } - } - - private static String parseBareString(char firstChar, ExprBuf buf, - Type exprType) { - StringBuilder sb = buf.getScratchBuffer().append(firstChar); - - byte stopFlags = (IS_OP_FLAG | IS_DELIM_FLAG | IS_SPACE_FLAG); - if(exprType == Type.FIELD_VALIDATOR) { - stopFlags |= IS_COMP_FLAG; - } - - while(buf.hasNext()) { - char c = buf.next(); - byte charFlag = getCharFlag(c); - if(hasFlag(charFlag, stopFlags)) { - buf.popPrev(); - break; - } - sb.append(c); - } - - return sb.toString(); - } - - private static String parseQuotedString(ExprBuf buf) { - StringBuilder sb = buf.getScratchBuffer(); - - boolean complete = false; - while(buf.hasNext()) { - char c = buf.next(); - if(c == QUOTED_STR_CHAR) { - int nc = buf.peekNext(); - if(nc == QUOTED_STR_CHAR) { - sb.append(QUOTED_STR_CHAR); - buf.next(); - } else { - complete = true; - break; - } - } - - sb.append(c); - } - - if(!complete) { - throw new IllegalArgumentException("Missing closing '" + QUOTED_STR_CHAR + - "' for quoted string " + buf); - } - - return sb.toString(); - } - - private static String parseObjNameString(ExprBuf buf) { - return parseStringUntil(buf, OBJ_NAME_END_CHAR, OBJ_NAME_START_CHAR); - } - - private static String parseStringUntil(ExprBuf buf, char endChar, - Character startChar) - { - StringBuilder sb = buf.getScratchBuffer(); - - boolean complete = false; - while(buf.hasNext()) { - char c = buf.next(); - if(c == endChar) { - complete = true; - break; - } else if((startChar != null) && - (startChar == c)) { - throw new IllegalArgumentException("Missing closing '" + endChar + - "' for quoted string " + buf); - } - - sb.append(c); - } - - if(!complete) { - throw new IllegalArgumentException("Missing closing '" + endChar + - "' for quoted string " + buf); - } - - return sb.toString(); - } - - private static Token parseDateLiteralString( - ExprBuf buf, ParseContext context) - { - String dateStr = parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null); - - boolean hasDate = (dateStr.indexOf('/') >= 0); - boolean hasTime = (dateStr.indexOf(':') >= 0); - - SimpleDateFormat sdf = null; - ValueType valType = null; - if(hasDate && hasTime) { - sdf = buf.getDateTimeFormat(context); - valType = ValueType.DATE_TIME; - } else if(hasDate) { - sdf = buf.getDateFormat(context); - valType = ValueType.DATE; - } else if(hasTime) { - sdf = buf.getTimeFormat(context); - valType = ValueType.TIME; - } else { - throw new IllegalArgumentException("Invalid date time literal " + dateStr + - " " + buf); - } - - try { - return new Token(TokenType.LITERAL, sdf.parse(dateStr), dateStr, valType); - } catch(ParseException pe) { - throw new IllegalArgumentException( - "Invalid date time literal " + dateStr + " " + buf, pe); - } - } - - private static Token maybeParseNumberLiteral(char firstChar, ExprBuf buf) { - StringBuilder sb = buf.getScratchBuffer().append(firstChar); - boolean hasDigit = isDigit(firstChar); - - int startPos = buf.curPos(); - boolean foundNum = false; - - try { - - int c = EOF; - while((c = buf.peekNext()) != EOF) { - if(isDigit(c)) { - hasDigit = true; - sb.append((char)c); - buf.next(); - } else if(c == '.') { - sb.append((char)c); - buf.next(); - } else if(isSpecialChar((char)c)) { - break; - } else { - // found a non-number, non-special string - return null; - } - } - - if(!hasDigit) { - // no digits, no number - return null; - } - - String numStr = sb.toString(); - try { - // what number type to use here? - BigDecimal num = new BigDecimal(numStr); - foundNum = true; - return new Token(TokenType.LITERAL, num, numStr, ValueType.BIG_DEC); - } catch(NumberFormatException ne) { - throw new IllegalArgumentException( - "Invalid number literal " + numStr + " " + buf, ne); - } - - } finally { - if(!foundNum) { - buf.reset(startPos); - } - } - } - - private static boolean hasFlag(byte charFlag, byte flag) { - return ((charFlag & flag) != 0); - } - - private static void setCharFlag(byte flag, char... chars) { - for(char c : chars) { - CHAR_FLAGS[c] |= flag; - } - } - - private static boolean isDigit(int c) { - return ((c >= '0') && (c <= '9')); - } - - static Map.Entry newEntry(K a, V b) { - return new AbstractMap.SimpleImmutableEntry(a, b); - } - - private static final class ExprBuf - { - private final String _str; - private int _pos; - private SimpleDateFormat _dateFmt; - private SimpleDateFormat _timeFmt; - private SimpleDateFormat _dateTimeFmt; - private final StringBuilder _scratch = new StringBuilder(); - - private ExprBuf(String str) { - _str = str; - } - - private int len() { - return _str.length(); - } - - public int curPos() { - return _pos; - } - - public int prevPos() { - return _pos - 1; - } - - public boolean hasNext() { - return _pos < len(); - } - - public char next() { - return _str.charAt(_pos++); - } - - public void popPrev() { - --_pos; - } - - public int peekNext() { - if(!hasNext()) { - return EOF; - } - return _str.charAt(_pos); - } - - public void reset(int pos) { - _pos = pos; - } - - public StringBuilder getScratchBuffer() { - _scratch.setLength(0); - return _scratch; - } - - public SimpleDateFormat getDateFormat(ParseContext context) { - if(_dateFmt == null) { - _dateFmt = context.createDateFormat(DATE_FORMAT); - } - return _dateFmt; - } - - public SimpleDateFormat getTimeFormat(ParseContext context) { - if(_timeFmt == null) { - _timeFmt = context.createDateFormat(TIME_FORMAT); - } - return _timeFmt; - } - - public SimpleDateFormat getDateTimeFormat(ParseContext context) { - if(_dateTimeFmt == null) { - _dateTimeFmt = context.createDateFormat(DATE_TIME_FORMAT); - } - return _dateTimeFmt; - } - - @Override - public String toString() { - return "[char " + _pos + "] '" + _str + "'"; - } - } - - - static final class Token - { - private final TokenType _type; - private final Object _val; - private final String _valStr; - private final ValueType _valType; - - private Token(TokenType type, String val) { - this(type, val, val); - } - - private Token(TokenType type, Object val, String valStr) { - this(type, val, valStr, null); - } - - private Token(TokenType type, Object val, String valStr, ValueType valType) { - _type = type; - _val = ((val != null) ? val : valStr); - _valStr = valStr; - _valType = valType; - } - - public TokenType getType() { - return _type; - } - - public Object getValue() { - return _val; - } - - public String getValueStr() { - return _valStr; - } - - public ValueType getValueType() { - return _valType; - } - - @Override - public String toString() { - if(_type == TokenType.SPACE) { - return "' '"; - } - String str = "[" + _type + "] '" + _val + "'"; - if(_valType != null) { - str += " (" + _valType + ")"; - } - return str; - } - } - -} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java b/src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java deleted file mode 100644 index 286ab8b..0000000 --- a/src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java +++ /dev/null @@ -1,1762 +0,0 @@ -/* -Copyright (c) 2016 James Ahlborn - -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.healthmarketscience.jackcess.util; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -import com.healthmarketscience.jackcess.DatabaseBuilder; -import static com.healthmarketscience.jackcess.util.ExpressionTokenizer.Token; -import static com.healthmarketscience.jackcess.util.ExpressionTokenizer.TokenType; -import com.healthmarketscience.jackcess.util.Expression.*; - -/** - * - * @author James Ahlborn - */ -public class Expressionator -{ - - // Useful links: - // - syntax: https://support.office.com/en-us/article/Guide-to-expression-syntax-ebc770bc-8486-4adc-a9ec-7427cce39a90 - // - examples: https://support.office.com/en-us/article/Examples-of-expressions-d3901e11-c04e-4649-b40b-8b6ec5aed41f - // - validation rule usage: https://support.office.com/en-us/article/Restrict-data-input-by-using-a-validation-rule-6c0b2ce1-76fa-4be0-8ae9-038b52652320 - - // FIXME - // - need to short-circuit AND/OR - - public enum Type { - DEFAULT_VALUE, FIELD_VALIDATOR, RECORD_VALIDATOR; - } - - public interface ParseContext { - public SimpleDateFormat createDateFormat(String formatStr); - public Function getExpressionFunction(String name); - } - - public static final ParseContext DEFAULT_PARSE_CONTEXT = new ParseContext() { - public SimpleDateFormat createDateFormat(String formatStr) { - return DatabaseBuilder.createDateFormat(formatStr); - } - public Function getExpressionFunction(String name) { - return DefaultFunctions.getFunction(name); - } - }; - - private enum WordType { - OP, COMP, LOG_OP, CONST, SPEC_OP_PREFIX, DELIM; - } - - private static final String FUNC_START_DELIM = "("; - private static final String FUNC_END_DELIM = ")"; - private static final String OPEN_PAREN = "("; - private static final String CLOSE_PAREN = ")"; - private static final String FUNC_PARAM_SEP = ","; - - private static final Map WORD_TYPES = - new HashMap(); - - static { - setWordType(WordType.OP, "+", "-", "*", "/", "\\", "^", "&", "mod"); - setWordType(WordType.COMP, "<", "<=", ">", ">=", "=", "<>"); - setWordType(WordType.LOG_OP, "and", "or", "eqv", "xor", "imp"); - setWordType(WordType.CONST, "true", "false", "null"); - setWordType(WordType.SPEC_OP_PREFIX, "is", "like", "between", "in", "not"); - // "X is null", "X is not null", "X like P", "X between A and B", - // "X not between A and B", "X in (A, B, C...)", "X not in (A, B, C...)", - // "not X" - setWordType(WordType.DELIM, ".", "!", ",", "(", ")"); - } - - private interface OpType {} - - private enum UnaryOp implements OpType { - NEG("-", false) { - @Override public Value eval(Value param1) { - return BuiltinOperators.negate(param1); - } - }, - NOT("Not", true) { - @Override public Value eval(Value param1) { - return BuiltinOperators.not(param1); - } - }; - - private final String _str; - private final boolean _needSpace; - - private UnaryOp(String str, boolean needSpace) { - _str = str; - _needSpace = needSpace; - } - - public boolean needsSpace() { - return _needSpace; - } - - @Override - public String toString() { - return _str; - } - - public abstract Value eval(Value param1); - } - - private enum BinaryOp implements OpType { - PLUS("+") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.add(param1, param2); - } - }, - MINUS("-") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.subtract(param1, param2); - } - }, - MULT("*") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.multiply(param1, param2); - } - }, - DIV("/") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.divide(param1, param2); - } - }, - INT_DIV("\\") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.intDivide(param1, param2); - } - }, - EXP("^") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.exp(param1, param2); - } - }, - CONCAT("&") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.concat(param1, param2); - } - }, - MOD("Mod") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.mod(param1, param2); - } - }; - - private final String _str; - - private BinaryOp(String str) { - _str = str; - } - - @Override - public String toString() { - return _str; - } - - public abstract Value eval(Value param1, Value param2); - } - - private enum CompOp implements OpType { - LT("<") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.lessThan(param1, param2); - } - }, - LTE("<=") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.lessThanEq(param1, param2); - } - }, - GT(">") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.greaterThan(param1, param2); - } - }, - GTE(">=") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.greaterThanEq(param1, param2); - } - }, - EQ("=") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.equals(param1, param2); - } - }, - NE("<>") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.notEquals(param1, param2); - } - }; - - private final String _str; - - private CompOp(String str) { - _str = str; - } - - @Override - public String toString() { - return _str; - } - - public abstract Value eval(Value param1, Value param2); - } - - private enum LogOp implements OpType { - AND("And") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.and(param1, param2); - } - }, - OR("Or") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.or(param1, param2); - } - }, - EQV("Eqv") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.eqv(param1, param2); - } - }, - XOR("Xor") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.xor(param1, param2); - } - }, - IMP("Imp") { - @Override public Value eval(Value param1, Value param2) { - return BuiltinOperators.imp(param1, param2); - } - }; - - private final String _str; - - private LogOp(String str) { - _str = str; - } - - @Override - public String toString() { - return _str; - } - - public abstract Value eval(Value param1, Value param2); - } - - private enum SpecOp implements OpType { - // note, "NOT" is not actually used as a special operation, always - // replaced with UnaryOp.NOT - NOT("Not") { - @Override public Value eval(Value param1, Object param2, Object param3) { - throw new UnsupportedOperationException(); - } - }, - IS_NULL("Is Null") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.isNull(param1); - } - }, - IS_NOT_NULL("Is Not Null") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.isNotNull(param1); - } - }, - LIKE("Like") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.like(param1, (Pattern)param2); - } - }, - BETWEEN("Between") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.between(param1, (Value)param2, (Value)param3); - } - }, - NOT_BETWEEN("Not Between") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.notBetween(param1, (Value)param2, (Value)param3); - } - }, - IN("In") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.in(param1, (Value[])param2); - } - }, - NOT_IN("Not In") { - @Override public Value eval(Value param1, Object param2, Object param3) { - return BuiltinOperators.notIn(param1, (Value[])param2); - } - }; - - private final String _str; - - private SpecOp(String str) { - _str = str; - } - - @Override - public String toString() { - return _str; - } - - public abstract Value eval(Value param1, Object param2, Object param3); - } - - private static final Map PRECENDENCE = - buildPrecedenceMap( - new OpType[]{BinaryOp.EXP}, - new OpType[]{UnaryOp.NEG}, - new OpType[]{BinaryOp.MULT, BinaryOp.DIV}, - new OpType[]{BinaryOp.INT_DIV}, - new OpType[]{BinaryOp.MOD}, - new OpType[]{BinaryOp.PLUS, BinaryOp.MINUS}, - new OpType[]{BinaryOp.CONCAT}, - new OpType[]{CompOp.LT, CompOp.GT, CompOp.NE, CompOp.LTE, CompOp.GTE, - CompOp.EQ, SpecOp.LIKE, SpecOp.IS_NULL, SpecOp.IS_NOT_NULL}, - new OpType[]{UnaryOp.NOT}, - new OpType[]{LogOp.AND}, - new OpType[]{LogOp.OR}, - new OpType[]{LogOp.XOR}, - new OpType[]{LogOp.EQV}, - new OpType[]{LogOp.IMP}, - new OpType[]{SpecOp.IN, SpecOp.NOT_IN, SpecOp.BETWEEN, - SpecOp.NOT_BETWEEN}); - - private static final Set REGEX_SPEC_CHARS = new HashSet( - Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&')); - - - private static final Expr THIS_COL_VALUE = new Expr() { - @Override protected Value eval(RowContext ctx) { - return ctx.getThisColumnValue(); - } - @Override protected void toExprString(StringBuilder sb, boolean isDebug) { - sb.append(""); - } - }; - - private static final Expr NULL_VALUE = new EConstValue( - BuiltinOperators.NULL_VAL, "Null"); - private static final Expr TRUE_VALUE = new EConstValue( - BuiltinOperators.TRUE_VAL, "True"); - private static final Expr FALSE_VALUE = new EConstValue( - BuiltinOperators.FALSE_VAL, "False"); - - private Expressionator() - { - } - - static String testTokenize(Type exprType, String exprStr, - ParseContext context) { - - if(context == null) { - context = DEFAULT_PARSE_CONTEXT; - } - List tokens = trimSpaces( - ExpressionTokenizer.tokenize(exprType, exprStr, context)); - - if(tokens == null) { - // FIXME, NULL_EXPR? - return null; - } - - return tokens.toString(); - } - - public static Expression parse(Type exprType, String exprStr, - ParseContext context) { - - if(context == null) { - context = DEFAULT_PARSE_CONTEXT; - } - - // FIXME,restrictions: - // - default value only accepts simple exprs, otherwise becomes literal text - // - def val cannot refer to any columns - // - field validation cannot refer to other columns - // - record validation cannot refer to outside columns - - List tokens = trimSpaces( - ExpressionTokenizer.tokenize(exprType, exprStr, context)); - - if(tokens == null) { - // FIXME, NULL_EXPR? - return null; - } - - return parseExpression(new TokBuf(exprType, tokens, context), false); - } - - private static List trimSpaces(List tokens) { - if(tokens == null) { - return null; - } - - // for the most part, spaces are superfluous except for one situation(?). - // when they appear between a string literal and '(' they help distinguish - // a function call from another expression form - for(int i = 1; i < (tokens.size() - 1); ++i) { - Token t = tokens.get(i); - if(t.getType() == TokenType.SPACE) { - if((tokens.get(i - 1).getType() == TokenType.STRING) && - isDelim(tokens.get(i + 1), FUNC_START_DELIM)) { - // we want to keep this space - } else { - tokens.remove(i); - --i; - } - } - } - return tokens; - } - - private static Expr parseExpression(TokBuf buf, boolean singleExpr) - { - while(buf.hasNext()) { - Token t = buf.next(); - - switch(t.getType()) { - case OBJ_NAME: - - parseObjectRefExpression(t, buf); - break; - - case LITERAL: - - buf.setPendingExpr(new ELiteralValue(t.getValueType(), t.getValue())); - break; - - case OP: - - WordType wordType = getWordType(t); - if(wordType == null) { - // shouldn't happen - throw new RuntimeException("Invalid operator " + t); - } - - // this can only be an OP or a COMP (those are the only words that the - // tokenizer would define as TokenType.OP) - switch(wordType) { - case OP: - parseOperatorExpression(t, buf); - break; - - case COMP: - - parseCompOpExpression(t, buf); - break; - - default: - throw new RuntimeException("Unexpected OP word type " + wordType); - } - - break; - - case DELIM: - - parseDelimExpression(t, buf); - break; - - case STRING: - - // see if it's a special word? - wordType = getWordType(t); - if(wordType == null) { - - // is it a function call? - if(!maybeParseFuncCallExpression(t, buf)) { - - // is it an object name? - Token next = buf.peekNext(); - if((next != null) && isObjNameSep(next)) { - - parseObjectRefExpression(t, buf); - - } else { - - // FIXME maybe bare obj name, maybe string literal? - throw new UnsupportedOperationException("FIXME"); - } - } - - } else { - - // this could be anything but COMP or DELIM (all COMPs would be - // returned as TokenType.OP and all DELIMs would be TokenType.DELIM) - switch(wordType) { - case OP: - - parseOperatorExpression(t, buf); - break; - - case LOG_OP: - - parseLogicalOpExpression(t, buf); - break; - - case CONST: - - parseConstExpression(t, buf); - break; - - case SPEC_OP_PREFIX: - - parseSpecOpExpression(t, buf); - break; - - default: - throw new RuntimeException("Unexpected STRING word type " - + wordType); - } - } - - break; - - case SPACE: - // top-level space is irrelevant (and we strip them anyway) - break; - - default: - throw new RuntimeException("unknown token type " + t); - } - - if(singleExpr && buf.hasPendingExpr()) { - break; - } - } - - Expr expr = buf.takePendingExpr(); - if(expr == null) { - throw new IllegalArgumentException("No expression found? " + buf); - } - - return expr; - } - - private static void parseObjectRefExpression(Token firstTok, TokBuf buf) { - - // object references may be joined by '.' or '!'. access syntac docs claim - // object identifiers can be formatted like: - // "[Collection name]![Object name].[Property name]" - // However, in practice, they only ever seem to be (at most) two levels - // and only use '.'. - Deque objNames = new LinkedList(); - objNames.add(firstTok.getValueStr()); - - Token t = null; - boolean atSep = false; - while((t = buf.peekNext()) != null) { - if(!atSep) { - if(isObjNameSep(t)) { - buf.next(); - atSep = true; - continue; - } - } else { - if((t.getType() == TokenType.OBJ_NAME) || - (t.getType() == TokenType.STRING)) { - buf.next(); - // always insert at beginning of list so names are in reverse order - objNames.addFirst(t.getValueStr()); - atSep = false; - continue; - } - } - break; - } - - if(atSep || (objNames.size() > 3)) { - throw new IllegalArgumentException("Invalid object reference " + buf); - } - - // names are in reverse order - String fieldName = objNames.poll(); - String objName = objNames.poll(); - String collectionName = objNames.poll(); - - buf.setPendingExpr( - new EObjValue(collectionName, objName, fieldName)); - } - - private static void parseDelimExpression(Token firstTok, TokBuf buf) { - // the only "top-level" delim we expect to find is open paren, and - // there shouldn't be any pending expression - if(!isDelim(firstTok, OPEN_PAREN) || buf.hasPendingExpr()) { - throw new IllegalArgumentException("Unexpected delimiter " + - firstTok.getValue() + " " + buf); - } - - Expr subExpr = findParenExprs(buf, false).get(0); - buf.setPendingExpr(new EParen(subExpr)); - } - - private static boolean maybeParseFuncCallExpression( - Token firstTok, TokBuf buf) { - - int startPos = buf.curPos(); - boolean foundFunc = false; - - try { - Token t = buf.peekNext(); - if(!isDelim(t, FUNC_START_DELIM)) { - // not a function call - return false; - } - - buf.next(); - List params = findParenExprs(buf, true); - String funcName = firstTok.getValueStr(); - Function func = buf.getFunction(funcName); - if(func == null) { - throw new IllegalArgumentException("Could not find function '" + - funcName + "' " + buf); - } - buf.setPendingExpr(new EFunc(func, params)); - foundFunc = true; - return true; - - } finally { - if(!foundFunc) { - buf.reset(startPos); - } - } - } - - private static List findParenExprs( - TokBuf buf, boolean allowMulti) { - - if(allowMulti) { - // simple case, no nested expr - Token t = buf.peekNext(); - if(isDelim(t, CLOSE_PAREN)) { - buf.next(); - return Collections.emptyList(); - } - } - - // find closing ")", handle nested parens - List exprs = new ArrayList(3); - int level = 1; - int startPos = buf.curPos(); - while(buf.hasNext()) { - - Token t = buf.next(); - - if(isDelim(t, OPEN_PAREN)) { - - ++level; - - } else if(isDelim(t, CLOSE_PAREN)) { - - --level; - if(level == 0) { - TokBuf subBuf = buf.subBuf(startPos, buf.prevPos()); - exprs.add(parseExpression(subBuf, false)); - return exprs; - } - - } else if(allowMulti && (level == 1) && isDelim(t, FUNC_PARAM_SEP)) { - - TokBuf subBuf = buf.subBuf(startPos, buf.prevPos()); - exprs.add(parseExpression(subBuf, false)); - startPos = buf.curPos(); - } - } - - throw new IllegalArgumentException("Missing closing '" + CLOSE_PAREN - + " " + buf); - } - - private static void parseOperatorExpression(Token t, TokBuf buf) { - - // most ops are two argument except that '-' could be negation - if(buf.hasPendingExpr()) { - parseBinaryOpExpression(t, buf); - } else if(isOp(t, "-")) { - parseUnaryOpExpression(t, buf); - } else { - throw new IllegalArgumentException( - "Missing left expression for binary operator " + t.getValue() + - " " + buf); - } - } - - private static void parseBinaryOpExpression(Token firstTok, TokBuf buf) { - BinaryOp op = getOpType(firstTok, BinaryOp.class); - Expr leftExpr = buf.takePendingExpr(); - Expr rightExpr = parseExpression(buf, true); - - buf.setPendingExpr(new EBinaryOp(op, leftExpr, rightExpr)); - } - - private static void parseUnaryOpExpression(Token firstTok, TokBuf buf) { - UnaryOp op = getOpType(firstTok, UnaryOp.class); - Expr val = parseExpression(buf, true); - - buf.setPendingExpr(new EUnaryOp(op, val)); - } - - private static void parseCompOpExpression(Token firstTok, TokBuf buf) { - - if(!buf.hasPendingExpr()) { - if(buf.getExprType() == Type.FIELD_VALIDATOR) { - // comparison operators for field validators can implicitly use - // the current field value for the left value - buf.setPendingExpr(THIS_COL_VALUE); - } else { - throw new IllegalArgumentException( - "Missing left expression for comparison operator " + - firstTok.getValue() + " " + buf); - } - } - - CompOp op = getOpType(firstTok, CompOp.class); - Expr leftExpr = buf.takePendingExpr(); - Expr rightExpr = parseExpression(buf, true); - - buf.setPendingExpr(new ECompOp(op, leftExpr, rightExpr)); - } - - private static void parseLogicalOpExpression(Token firstTok, TokBuf buf) { - - if(!buf.hasPendingExpr()) { - throw new IllegalArgumentException( - "Missing left expression for logical operator " + - firstTok.getValue() + " " + buf); - } - - LogOp op = getOpType(firstTok, LogOp.class); - Expr leftExpr = buf.takePendingExpr(); - Expr rightExpr = parseExpression(buf, true); - - buf.setPendingExpr(new ELogicalOp(op, leftExpr, rightExpr)); - } - - private static void parseSpecOpExpression(Token firstTok, TokBuf buf) { - - SpecOp specOp = getSpecialOperator(firstTok, buf); - - if(specOp == SpecOp.NOT) { - // this is the unary prefix operator - parseUnaryOpExpression(firstTok, buf); - return; - } - - if(!buf.hasPendingExpr()) { - if(buf.getExprType() == Type.FIELD_VALIDATOR) { - // comparison operators for field validators can implicitly use - // the current field value for the left value - buf.setPendingExpr(THIS_COL_VALUE); - } else { - throw new IllegalArgumentException( - "Missing left expression for comparison operator " + - specOp + " " + buf); - } - } - - Expr expr = buf.takePendingExpr(); - - Expr specOpExpr = null; - switch(specOp) { - case IS_NULL: - case IS_NOT_NULL: - specOpExpr = new ENullOp(specOp, expr); - break; - - case LIKE: - Token t = buf.next(); - // FIXME, create LITERAL_STRING TokenType? - if(t.getType() != TokenType.LITERAL) { - throw new IllegalArgumentException("Missing Like pattern " + buf); - } - String patternStr = t.getValueStr(); - Pattern pattern = likePatternToRegex(patternStr, buf); - specOpExpr = new ELikeOp(specOp, expr, pattern, patternStr); - break; - - case BETWEEN: - case NOT_BETWEEN: - - // the "rest" of a between expression is of the form "X And Y". we are - // going to speculatively parse forward until we find the "And" - // operator. - Expr startRangeExpr = null; - while(true) { - - Expr tmpExpr = parseExpression(buf, true); - Token tmpT = buf.peekNext(); - - if(tmpT == null) { - // ran out of expression? - throw new IllegalArgumentException( - "Missing 'And' for 'Between' expression " + buf); - } - - if(isString(tmpT, "and")) { - buf.next(); - startRangeExpr = tmpExpr; - break; - } - - // put the pending expression back and try parsing some more - buf.restorePendingExpr(tmpExpr); - } - - Expr endRangeExpr = parseExpression(buf, true); - - specOpExpr = new EBetweenOp(specOp, expr, startRangeExpr, endRangeExpr); - break; - - case IN: - case NOT_IN: - - // there might be a space before open paren - t = buf.next(); - if(t.getType() == TokenType.SPACE) { - t = buf.next(); - } - if(!isDelim(t, OPEN_PAREN)) { - throw new IllegalArgumentException("Malformed In expression " + buf); - } - - List exprs = findParenExprs(buf, true); - specOpExpr = new EInOp(specOp, expr, exprs); - break; - - default: - throw new RuntimeException("Unexpected special op " + specOp); - } - - buf.setPendingExpr(specOpExpr); - } - - private static SpecOp getSpecialOperator(Token firstTok, TokBuf buf) { - String opStr = firstTok.getValueStr().toLowerCase(); - - if("is".equals(opStr)) { - Token t = buf.peekNext(); - if(isString(t, "null")) { - buf.next(); - return SpecOp.IS_NULL; - } else if(isString(t, "not")) { - buf.next(); - t = buf.peekNext(); - if(isString(t, "null")) { - return SpecOp.IS_NOT_NULL; - } - } - } else if("like".equals(opStr)) { - return SpecOp.LIKE; - } else if("between".equals(opStr)) { - return SpecOp.BETWEEN; - } else if("in".equals(opStr)) { - return SpecOp.IN; - } else if("not".equals(opStr)) { - Token t = buf.peekNext(); - if(isString(t, "between")) { - buf.next(); - return SpecOp.NOT_BETWEEN; - } else if(isString(t, "in")) { - buf.next(); - return SpecOp.NOT_IN; - } - return SpecOp.NOT; - } - - throw new IllegalArgumentException( - "Malformed special operator " + opStr + " " + buf); - } - - private static void parseConstExpression(Token firstTok, TokBuf buf) { - Expr constExpr = null; - if("true".equalsIgnoreCase(firstTok.getValueStr())) { - constExpr = TRUE_VALUE; - } else if("false".equalsIgnoreCase(firstTok.getValueStr())) { - constExpr = FALSE_VALUE; - } else if("null".equalsIgnoreCase(firstTok.getValueStr())) { - constExpr = NULL_VALUE; - } else { - throw new RuntimeException("Unexpected CONST word " - + firstTok.getValue()); - } - buf.setPendingExpr(constExpr); - } - - private static boolean isObjNameSep(Token t) { - return (isDelim(t, ".") || isDelim(t, "!")); - } - - private static boolean isOp(Token t, String opStr) { - return ((t != null) && (t.getType() == TokenType.OP) && - opStr.equalsIgnoreCase(t.getValueStr())); - } - - private static boolean isDelim(Token t, String opStr) { - return ((t != null) && (t.getType() == TokenType.DELIM) && - opStr.equalsIgnoreCase(t.getValueStr())); - } - - private static boolean isString(Token t, String opStr) { - return ((t != null) && (t.getType() == TokenType.STRING) && - opStr.equalsIgnoreCase(t.getValueStr())); - } - - private static WordType getWordType(Token t) { - return WORD_TYPES.get(t.getValueStr().toLowerCase()); - } - - private static void setWordType(WordType type, String... words) { - for(String w : words) { - WORD_TYPES.put(w, type); - } - } - - private static > T getOpType(Token t, Class opClazz) { - String str = t.getValueStr(); - for(T op : opClazz.getEnumConstants()) { - if(str.equalsIgnoreCase(op.toString())) { - return op; - } - } - throw new IllegalArgumentException("Unexpected op string " + t.getValueStr()); - } - - private static final class TokBuf - { - private final Type _exprType; - private final List _tokens; - private final TokBuf _parent; - private final int _parentOff; - private final ParseContext _context; - private int _pos; - private Expr _pendingExpr; - private final boolean _simpleExpr; - - private TokBuf(Type exprType, List tokens, ParseContext context) { - this(exprType, false, tokens, null, 0, context); - } - - private TokBuf(List tokens, TokBuf parent, int parentOff) { - this(parent._exprType, parent._simpleExpr, tokens, parent, parentOff, - parent._context); - } - - private TokBuf(Type exprType, boolean simpleExpr, List tokens, - TokBuf parent, int parentOff, ParseContext context) { - _exprType = exprType; - _tokens = tokens; - _parent = parent; - _parentOff = parentOff; - _context = context; - if(parent == null) { - // "top-level" expression, determine if it is a simple expression or not - simpleExpr = isSimpleExpression(); - } - _simpleExpr = simpleExpr; - } - - private boolean isSimpleExpression() { - if(_exprType != Type.DEFAULT_VALUE) { - return false; - } - - // a leading "=" indicates "full" expression handling for a DEFAULT_VALUE - Token t = peekNext(); - if(isOp(t, "=")) { - next(); - return false; - } - - // this is a "simple" DEFAULT_VALUE - return true; - } - - public Type getExprType() { - return _exprType; - } - - public boolean isSimpleExpr() { - return _simpleExpr; - } - - public boolean isTopLevel() { - return (_parent == null); - } - - public int curPos() { - return _pos; - } - - public int prevPos() { - return _pos - 1; - } - - public boolean hasNext() { - return (_pos < _tokens.size()); - } - - public Token peekNext() { - if(!hasNext()) { - return null; - } - return _tokens.get(_pos); - } - - public Token next() { - if(!hasNext()) { - throw new IllegalArgumentException( - "Unexpected end of expression " + this); - } - return _tokens.get(_pos++); - } - - public void reset(int pos) { - _pos = pos; - } - - public TokBuf subBuf(int start, int end) { - return new TokBuf(_tokens.subList(start, end), this, start); - } - - public void setPendingExpr(Expr expr) { - if(_pendingExpr != null) { - throw new IllegalArgumentException( - "Found multiple expressions with no operator " + this); - } - _pendingExpr = expr.resolveOrderOfOperations(); - } - - public void restorePendingExpr(Expr expr) { - // this is an expression which was previously set, so no need to re-resolve - _pendingExpr = expr; - } - - public Expr takePendingExpr() { - Expr expr = _pendingExpr; - _pendingExpr = null; - return expr; - } - - public boolean hasPendingExpr() { - return (_pendingExpr != null); - } - - private Map.Entry> getTopPos() { - int pos = _pos; - List toks = _tokens; - TokBuf cur = this; - while(cur._parent != null) { - pos += cur._parentOff; - cur = cur._parent; - toks = cur._tokens; - } - return ExpressionTokenizer.newEntry(pos, toks); - } - - public Function getFunction(String funcName) { - return _context.getExpressionFunction(funcName); - } - - @Override - public String toString() { - - Map.Entry> e = getTopPos(); - - // TODO actually format expression? - StringBuilder sb = new StringBuilder() - .append("[token ").append(e.getKey()).append("] ("); - - for(Iterator iter = e.getValue().iterator(); iter.hasNext(); ) { - Token t = iter.next(); - sb.append("'").append(t.getValueStr()).append("'"); - if(iter.hasNext()) { - sb.append(","); - } - } - - sb.append(")"); - - if(_pendingExpr != null) { - sb.append(" [pending '").append(_pendingExpr.toDebugString()) - .append("']"); - } - - return sb.toString(); - } - } - - private static boolean isHigherPrecendence(OpType op1, OpType op2) { - int prec1 = PRECENDENCE.get(op1); - int prec2 = PRECENDENCE.get(op2); - - // higher preceendence ops have lower numbers - return (prec1 < prec2); - } - - private static final Map buildPrecedenceMap( - OpType[]... opArrs) { - Map prec = new HashMap(); - - int level = 0; - for(OpType[] ops : opArrs) { - for(OpType op : ops) { - prec.put(op, level); - } - ++level; - } - - return prec; - } - - private static void exprListToString( - List exprs, String sep, StringBuilder sb, boolean isDebug) { - Iterator iter = exprs.iterator(); - iter.next().toString(sb, isDebug); - while(iter.hasNext()) { - sb.append(sep); - iter.next().toString(sb, isDebug); - } - } - - private static Value[] exprListToValues( - List exprs, RowContext ctx) { - Value[] paramVals = new Value[exprs.size()]; - for(int i = 0; i < exprs.size(); ++i) { - paramVals[i] = exprs.get(i).eval(ctx); - } - return paramVals; - } - - private static Value[] exprListToDelayedValues( - List exprs, RowContext ctx) { - Value[] paramVals = new Value[exprs.size()]; - for(int i = 0; i < exprs.size(); ++i) { - paramVals[i] = new DelayedValue(exprs.get(i), ctx); - } - return paramVals; - } - - private static void literalStrToString(String str, StringBuilder sb) { - sb.append("\"") - .append(str.replace("\"", "\"\"")) - .append("\""); - } - - private static Pattern likePatternToRegex(String pattern, Object location) { - - StringBuilder sb = new StringBuilder(pattern.length()); - - // Access LIKE pattern supports (note, matching is case-insensitive): - // - '*' -> 0 or more chars - // - '?' -> single character - // - '#' -> single digit - // - '[...]' -> character class, '[!...]' -> not in char class - - for(int i = 0; i < pattern.length(); ++i) { - char c = pattern.charAt(i); - - if(c == '*') { - sb.append(".*"); - } else if(c == '?') { - sb.append('.'); - } else if(c == '#') { - sb.append("\\d"); - } else if(c == '[') { - - // find closing brace - int startPos = i + 1; - int endPos = -1; - for(int j = startPos; j < pattern.length(); ++j) { - if(pattern.charAt(j) == ']') { - endPos = j; - break; - } - } - - if(endPos == -1) { - throw new IllegalArgumentException( - "Could not find closing bracket in pattern '" + pattern + "' " + - location); - } - - String charClass = pattern.substring(startPos, endPos); - - if((charClass.length() > 0) && (charClass.charAt(0) == '!')) { - // this is a negated char class - charClass = '^' + charClass.substring(1); - } - - sb.append('[').append(charClass).append(']'); - - } else if(REGEX_SPEC_CHARS.contains(c)) { - // this char is special in regexes, so escape it - sb.append('\\').append(c); - } else { - sb.append(c); - } - } - - return Pattern.compile(sb.toString(), - Pattern.CASE_INSENSITIVE | Pattern.DOTALL | - Pattern.UNICODE_CASE); - } - - - private interface LeftAssocExpr { - public OpType getOp(); - public Expr getLeft(); - public void setLeft(Expr left); - } - - private interface RightAssocExpr { - public OpType getOp(); - public Expr getRight(); - public void setRight(Expr right); - } - - private static final class DelayedValue implements Value - { - private Value _val; - private final Expr _expr; - private final RowContext _ctx; - - private DelayedValue(Expr expr, RowContext ctx) { - _expr = expr; - _ctx = ctx; - } - - private Value getDelegate() { - if(_val == null) { - _val = _expr.eval(_ctx); - } - return _val; - } - - public ValueType getType() { - return getDelegate().getType(); - } - - public Object get() { - return getDelegate().get(); - } - } - - - private static abstract class Expr implements Expression - { - public Object evalDefault() { - Value val = eval(null); - - if(val.getType() == ValueType.NULL) { - return null; - } - - // FIXME, booleans seem to go to -1 (true),0 (false) ...? - - return val.get(); - } - - public Boolean evalCondition(RowContext ctx) { - Value val = eval(ctx); - - if(val.getType() == ValueType.NULL) { - return null; - } - - if(val.getType() != ValueType.BOOLEAN) { - // a single value as a conditional expression seems to act like an - // implicit "=" - // FIXME, what about row validators? - val = BuiltinOperators.equals(val, ctx.getThisColumnValue()); - } - - return (Boolean)val.get(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - toString(sb, false); - return sb.toString(); - } - - public String toDebugString() { - StringBuilder sb = new StringBuilder(); - toString(sb, true); - return sb.toString(); - } - - protected void toString(StringBuilder sb, boolean isDebug) { - if(isDebug) { - sb.append("<").append(getClass().getSimpleName()).append(">{"); - } - toExprString(sb, isDebug); - if(isDebug) { - sb.append("}"); - } - } - - protected Expr resolveOrderOfOperations() { - - if(!(this instanceof LeftAssocExpr)) { - // nothing we can do - return this; - } - - // in order to get the precedence right, we need to first associate this - // expression with the "rightmost" expression preceding it, then adjust - // this expression "down" (lower precedence) as the precedence of the - // operations dictates. since we parse from left to right, the initial - // "left" value isn't the immediate left expression, instead it's based - // on how the preceding operator precedence worked out. we need to - // adjust "this" expression to the closest preceding expression before - // we can correctly resolve precedence. - - Expr outerExpr = this; - final LeftAssocExpr thisExpr = (LeftAssocExpr)this; - final Expr thisLeft = thisExpr.getLeft(); - - // current: {{A op1 B} op2 {C}} - if(thisLeft instanceof RightAssocExpr) { - - RightAssocExpr leftOp = (RightAssocExpr)thisLeft; - - // target: {A op1 {B op2 {C}}} - - thisExpr.setLeft(leftOp.getRight()); - - // give the new version of this expression an opportunity to further - // swap (since the swapped expression may itself be a binary - // expression) - leftOp.setRight(resolveOrderOfOperations()); - outerExpr = thisLeft; - - // at this point, this expression has been pushed all the way to the - // rightmost preceding expression (we artifically gave "this" the - // highest precedence). now, we want to adjust precedence as - // necessary (shift it back down if the operator precedence is - // incorrect). note, we only need to check precedence against "this", - // as all other precedence has been resolved in previous parsing - // rounds. - if((leftOp.getRight() == this) && - !isHigherPrecendence(thisExpr.getOp(), leftOp.getOp())) { - - // doh, "this" is lower (or the same) precedence, restore the - // original order of things - leftOp.setRight(thisExpr.getLeft()); - thisExpr.setLeft(thisLeft); - outerExpr = this; - } - } - - return outerExpr; - } - - protected abstract Value eval(RowContext ctx); - - protected abstract void toExprString(StringBuilder sb, boolean isDebug); - } - - private static final class EConstValue extends Expr - { - private final Value _val; - private final String _str; - - private EConstValue(Value val, String str) { - _val = val; - _str = str; - } - - @Override - protected Value eval(RowContext ctx) { - return _val; - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - sb.append(_str); - } - } - - private static final class ELiteralValue extends Expr - { - private final Value _val; - - private ELiteralValue(ValueType valType, Object value) { - _val = new BuiltinOperators.SimpleValue(valType, value); - } - - @Override - public Value eval(RowContext ctx) { - return _val; - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - if(_val.getType() == ValueType.STRING) { - literalStrToString((String)_val.get(), sb); - } else if(_val.getType().isTemporal()) { - // // FIXME Date,Time,DateTime formatting? - // sb.append("#").append(_value).append("#"); - throw new UnsupportedOperationException(); - } else { - sb.append(_val.get()); - } - } - } - - private static final class EObjValue extends Expr - { - private final String _collectionName; - private final String _objName; - private final String _fieldName; - - - private EObjValue(String collectionName, String objName, String fieldName) { - _collectionName = collectionName; - _objName = objName; - _fieldName = fieldName; - } - - @Override - public Value eval(RowContext ctx) { - return ctx.getRowValue(_collectionName, _objName, _fieldName); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - if(_collectionName != null) { - sb.append("[").append(_collectionName).append("]."); - } - if(_objName != null) { - sb.append("[").append(_objName).append("]."); - } - sb.append("[").append(_fieldName).append("]"); - } - } - - private static class EParen extends Expr - { - private final Expr _expr; - - private EParen(Expr expr) { - _expr = expr; - } - - @Override - protected Value eval(RowContext ctx) { - return _expr.eval(ctx); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - sb.append("("); - _expr.toString(sb, isDebug); - sb.append(")"); - } - } - - private static class EFunc extends Expr - { - private final Function _func; - private final List _params; - - private EFunc(Function func, List params) { - _func = func; - _params = params; - } - - @Override - protected Value eval(RowContext ctx) { - return _func.eval(exprListToValues(_params, ctx)); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - sb.append(_func.getName()).append("("); - - if(!_params.isEmpty()) { - exprListToString(_params, ",", sb, isDebug); - } - - sb.append(")"); - } - } - - private static abstract class EBaseBinaryOp extends Expr - implements LeftAssocExpr, RightAssocExpr - { - protected final OpType _op; - protected Expr _left; - protected Expr _right; - - private EBaseBinaryOp(OpType op, Expr left, Expr right) { - _op = op; - _left = left; - _right = right; - } - - public OpType getOp() { - return _op; - } - - public Expr getLeft() { - return _left; - } - - public void setLeft(Expr left) { - _left = left; - } - - public Expr getRight() { - return _right; - } - - public void setRight(Expr right) { - _right = right; - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - _left.toString(sb, isDebug); - sb.append(" ").append(_op).append(" "); - _right.toString(sb, isDebug); - } - } - - private static class EBinaryOp extends EBaseBinaryOp - { - private EBinaryOp(BinaryOp op, Expr left, Expr right) { - super(op, left, right); - } - - @Override - protected Value eval(RowContext ctx) { - return ((BinaryOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); - } - } - - private static class EUnaryOp extends Expr - implements RightAssocExpr - { - private final OpType _op; - private Expr _expr; - - private EUnaryOp(UnaryOp op, Expr expr) { - _op = op; - _expr = expr; - } - - public OpType getOp() { - return _op; - } - - public Expr getRight() { - return _expr; - } - - public void setRight(Expr right) { - _expr = right; - } - - @Override - protected Value eval(RowContext ctx) { - return ((UnaryOp)_op).eval(_expr.eval(ctx)); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - sb.append(_op); - if(isDebug || ((UnaryOp)_op).needsSpace()) { - sb.append(" "); - } - _expr.toString(sb, isDebug); - } - } - - private static class ECompOp extends EBaseBinaryOp - { - private ECompOp(CompOp op, Expr left, Expr right) { - super(op, left, right); - } - - @Override - protected Value eval(RowContext ctx) { - return ((CompOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); - } - } - - private static class ELogicalOp extends EBaseBinaryOp - { - private ELogicalOp(LogOp op, Expr left, Expr right) { - super(op, left, right); - } - - @Override - protected Value eval(final RowContext ctx) { - - // logical operations do short circuit evaluation, so we need to delay - // computing results until necessary - return ((LogOp)_op).eval(new DelayedValue(_left, ctx), - new DelayedValue(_right, ctx)); - } - } - - private static abstract class ESpecOp extends Expr - implements LeftAssocExpr - { - protected final SpecOp _op; - protected Expr _expr; - - private ESpecOp(SpecOp op, Expr expr) { - _op = op; - _expr = expr; - } - - public OpType getOp() { - return _op; - } - - public Expr getLeft() { - return _expr; - } - - public void setLeft(Expr left) { - _expr = left; - } - } - - private static class ENullOp extends ESpecOp - { - private ENullOp(SpecOp op, Expr expr) { - super(op, expr); - } - - @Override - protected Value eval(RowContext ctx) { - return _op.eval(_expr.eval(ctx), null, null); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - _expr.toString(sb, isDebug); - sb.append(" ").append(_op); - } - } - - private static class ELikeOp extends ESpecOp - { - // FIXME, compile Pattern on first use? - private final Pattern _pattern; - private final String _patternStr; - - private ELikeOp(SpecOp op, Expr expr, Pattern pattern, String patternStr) { - super(op, expr); - _pattern = pattern; - _patternStr = patternStr; - } - - @Override - protected Value eval(RowContext ctx) { - return _op.eval(_expr.eval(ctx), _pattern, null); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - _expr.toString(sb, isDebug); - sb.append(" ").append(_op).append(" "); - literalStrToString(_patternStr, sb); - if(isDebug) { - sb.append("(").append(_pattern).append(")"); - } - } - } - - private static class EInOp extends ESpecOp - { - private final List _exprs; - - private EInOp(SpecOp op, Expr expr, List exprs) { - super(op, expr); - _exprs = exprs; - } - - @Override - protected Value eval(RowContext ctx) { - return _op.eval(_expr.eval(ctx), - exprListToDelayedValues(_exprs, ctx), null); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - _expr.toString(sb, isDebug); - sb.append(" ").append(_op).append(" ("); - exprListToString(_exprs, ",", sb, isDebug); - sb.append(")"); - } - } - - private static class EBetweenOp extends ESpecOp - implements RightAssocExpr - { - private final Expr _startRangeExpr; - private Expr _endRangeExpr; - - private EBetweenOp(SpecOp op, Expr expr, Expr startRangeExpr, - Expr endRangeExpr) { - super(op, expr); - _startRangeExpr = startRangeExpr; - _endRangeExpr = endRangeExpr; - } - - public Expr getRight() { - return _endRangeExpr; - } - - public void setRight(Expr right) { - _endRangeExpr = right; - } - - @Override - protected Value eval(RowContext ctx) { - return _op.eval(_expr.eval(ctx), - new DelayedValue(_startRangeExpr, ctx), - new DelayedValue(_endRangeExpr, ctx)); - } - - @Override - protected void toExprString(StringBuilder sb, boolean isDebug) { - _expr.toString(sb, isDebug); - sb.append(" ").append(_op).append(" "); - _startRangeExpr.toString(sb, isDebug); - sb.append(" And "); - _endRangeExpr.toString(sb, isDebug); - } - } -} -- cgit v1.2.3