From 874edea4ed1fcfca3a35c98d0706c320fc65c58b Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Thu, 3 Nov 2016 20:13:35 +0000 Subject: [PATCH] rework classes, add more interfaces; start implementing builtin functions using Value type git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1054 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/ColumnImpl.java | 9 + .../jackcess/util/BuiltinOperators.java | 437 +++++++++++++++++ .../jackcess/util/DefaultFunctions.java | 152 ++++++ .../jackcess/util/Expression.java | 68 +++ .../jackcess/util/ExpressionTokenizer.java | 78 +-- .../jackcess/util/Expressionator.java | 462 +++++++++++++----- .../jackcess/util/ExpressionatorTest.java | 4 +- 7 files changed, 1052 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/util/DefaultFunctions.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/util/Expression.java diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 998e80a..d61bab8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -1524,6 +1524,15 @@ public class ColumnImpl implements Column, Comparable { return false; } else if(obj instanceof Boolean) { return ((Boolean)obj).booleanValue(); + } else if(obj instanceof Number) { + // Access considers 0 as "false" + if(obj instanceof BigDecimal) { + return (((BigDecimal)obj).compareTo(BigDecimal.ZERO) != 0); + } + if(obj instanceof BigInteger) { + return (((BigInteger)obj).compareTo(BigInteger.ZERO) != 0); + } + return (((Number)obj).doubleValue() != 0.0); } return Boolean.parseBoolean(obj.toString()); } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java b/src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java new file mode 100644 index 0000000..cb7eb6c --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/util/BuiltinOperators.java @@ -0,0 +1,437 @@ +/* +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 new file mode 100644 index 0000000..3c9c292 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/util/DefaultFunctions.java @@ -0,0 +1,152 @@ +/* +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 new file mode 100644 index 0000000..f2bb462 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/util/Expression.java @@ -0,0 +1,68 @@ +/* +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 index b9a3cd4..192a62d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/ExpressionTokenizer.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/ExpressionTokenizer.java @@ -27,9 +27,9 @@ import java.util.List; import java.util.Map; import java.util.Set; -import com.healthmarketscience.jackcess.DatabaseBuilder; -import com.healthmarketscience.jackcess.impl.DatabaseImpl; import static com.healthmarketscience.jackcess.util.Expressionator.*; +import com.healthmarketscience.jackcess.util.Expression.ValueType; + /** * @@ -74,7 +74,8 @@ class ExpressionTokenizer * Tokenizes an expression string of the given type and (optionally) in the * context of the relevant database. */ - static List tokenize(Type exprType, String exprStr, DatabaseImpl db) { + static List tokenize(Type exprType, String exprStr, + ParseContext context) { if(exprStr != null) { exprStr = exprStr.trim(); @@ -99,10 +100,9 @@ class ExpressionTokenizer case IS_OP_FLAG: // special case '-' for negative number - Map.Entry numLit = maybeParseNumberLiteral(c, buf); + Token numLit = maybeParseNumberLiteral(c, buf); if(numLit != null) { - tokens.add(new Token(TokenType.LITERAL, numLit.getKey(), - numLit.getValue())); + tokens.add(numLit); continue; } @@ -152,12 +152,11 @@ class ExpressionTokenizer switch(c) { case QUOTED_STR_CHAR: - tokens.add(new Token(TokenType.LITERAL, parseQuotedString(buf))); + tokens.add(new Token(TokenType.LITERAL, null, parseQuotedString(buf), + ValueType.STRING)); break; case DATE_LIT_QUOTE_CHAR: - Map.Entry dateLit = parseDateLiteralString(buf, db); - tokens.add(new Token(TokenType.LITERAL, dateLit.getKey(), - dateLit.getValue())); + tokens.add(parseDateLiteralString(buf, context)); break; case OBJ_NAME_START_CHAR: tokens.add(new Token(TokenType.OBJ_NAME, parseObjNameString(buf))); @@ -176,10 +175,9 @@ class ExpressionTokenizer } else { if(isDigit(c)) { - Map.Entry numLit = maybeParseNumberLiteral(c, buf); + Token numLit = maybeParseNumberLiteral(c, buf); if(numLit != null) { - tokens.add(new Token(TokenType.LITERAL, numLit.getKey(), - numLit.getValue())); + tokens.add(numLit); continue; } } @@ -309,8 +307,8 @@ class ExpressionTokenizer return sb.toString(); } - private static Map.Entry parseDateLiteralString( - ExprBuf buf, DatabaseImpl db) + private static Token parseDateLiteralString( + ExprBuf buf, ParseContext context) { String dateStr = parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null); @@ -318,27 +316,30 @@ class ExpressionTokenizer boolean hasTime = (dateStr.indexOf(':') >= 0); SimpleDateFormat sdf = null; + ValueType valType = null; if(hasDate && hasTime) { - sdf = buf.getDateTimeFormat(db); + sdf = buf.getDateTimeFormat(context); + valType = ValueType.DATE_TIME; } else if(hasDate) { - sdf = buf.getDateFormat(db); + sdf = buf.getDateFormat(context); + valType = ValueType.DATE; } else if(hasTime) { - sdf = buf.getTimeFormat(db); + sdf = buf.getTimeFormat(context); + valType = ValueType.TIME; } else { throw new IllegalArgumentException("Invalid date time literal " + dateStr + " " + buf); } - // FIXME, do we need to know which "type" it was? try { - return newEntry(sdf.parse(dateStr), dateStr); + 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 Map.Entry maybeParseNumberLiteral(char firstChar, ExprBuf buf) { + private static Token maybeParseNumberLiteral(char firstChar, ExprBuf buf) { StringBuilder sb = buf.getScratchBuffer().append(firstChar); boolean hasDigit = isDigit(firstChar); @@ -374,7 +375,7 @@ class ExpressionTokenizer // what number type to use here? BigDecimal num = new BigDecimal(numStr); foundNum = true; - return newEntry(num, numStr); + return new Token(TokenType.LITERAL, num, numStr, ValueType.BIG_DEC); } catch(NumberFormatException ne) { throw new IllegalArgumentException( "Invalid number literal " + numStr + " " + buf, ne); @@ -458,32 +459,27 @@ class ExpressionTokenizer return _scratch; } - public SimpleDateFormat getDateFormat(DatabaseImpl db) { + public SimpleDateFormat getDateFormat(ParseContext context) { if(_dateFmt == null) { - _dateFmt = newFormat(DATE_FORMAT, db); + _dateFmt = context.createDateFormat(DATE_FORMAT); } return _dateFmt; } - public SimpleDateFormat getTimeFormat(DatabaseImpl db) { + public SimpleDateFormat getTimeFormat(ParseContext context) { if(_timeFmt == null) { - _timeFmt = newFormat(TIME_FORMAT, db); + _timeFmt = context.createDateFormat(TIME_FORMAT); } return _timeFmt; } - public SimpleDateFormat getDateTimeFormat(DatabaseImpl db) { + public SimpleDateFormat getDateTimeFormat(ParseContext context) { if(_dateTimeFmt == null) { - _dateTimeFmt = newFormat(DATE_TIME_FORMAT, db); + _dateTimeFmt = context.createDateFormat(DATE_TIME_FORMAT); } return _dateTimeFmt; } - private static SimpleDateFormat newFormat(String str, DatabaseImpl db) { - return ((db != null) ? db.createDateFormat(str) : - DatabaseBuilder.createDateFormat(str)); - } - @Override public String toString() { return "[char " + _pos + "] '" + _str + "'"; @@ -496,15 +492,21 @@ class ExpressionTokenizer 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; + _val = ((val != null) ? val : valStr); _valStr = valStr; + _valType = valType; } public TokenType getType() { @@ -519,14 +521,18 @@ class ExpressionTokenizer return _valStr; } + public ValueType getValueType() { + return _valType; + } + @Override public String toString() { if(_type == TokenType.SPACE) { return "' '"; } String str = "[" + _type + "] '" + _val + "'"; - if(_type == TokenType.LITERAL) { - str += " (" + _val.getClass() + ")"; + 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 index 6b5d3fd..286ab8b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java @@ -16,10 +16,10 @@ 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.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -30,10 +30,10 @@ import java.util.Map; import java.util.Set; import java.util.regex.Pattern; -import com.healthmarketscience.jackcess.Database; -import com.healthmarketscience.jackcess.impl.DatabaseImpl; +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.*; /** * @@ -54,6 +54,20 @@ public class Expressionator 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; } @@ -64,12 +78,13 @@ public class Expressionator private static final String CLOSE_PAREN = ")"; private static final String FUNC_PARAM_SEP = ","; - private static final Map WORD_TYPES = new HashMap(); + private static final Map WORD_TYPES = + new HashMap(); static { setWordType(WordType.OP, "+", "-", "*", "/", "\\", "^", "&", "mod"); setWordType(WordType.COMP, "<", "<=", ">", ">=", "=", "<>"); - setWordType(WordType.LOG_OP, "and", "or", "eqv", "xor"); + 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", @@ -81,7 +96,16 @@ public class Expressionator private interface OpType {} private enum UnaryOp implements OpType { - NEG("-", false), NOT("Not", true); + 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; @@ -99,11 +123,51 @@ public class Expressionator public String toString() { return _str; } + + public abstract Value eval(Value param1); } private enum BinaryOp implements OpType { - PLUS("+"), MINUS("-"), MULT("*"), DIV("/"), INT_DIV("\\"), EXP("^"), - CONCAT("&"), MOD("Mod"); + 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; @@ -115,10 +179,41 @@ public class Expressionator public String toString() { return _str; } + + public abstract Value eval(Value param1, Value param2); } private enum CompOp implements OpType { - LT("<"), LTE("<="), GT(">"), GTE(">="), EQ("="), NE("<>"); + 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; @@ -130,10 +225,36 @@ public class Expressionator public String toString() { return _str; } + + public abstract Value eval(Value param1, Value param2); } private enum LogOp implements OpType { - AND("And"), OR("Or"), EQV("Eqv"), XOR("Xor"); + 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; @@ -145,14 +266,53 @@ public class Expressionator 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"), IS_NULL("Is Null"), IS_NOT_NULL("Is Not Null"), LIKE("Like"), - BETWEEN("Between"), NOT_BETWEEN("Not Between"), IN("In"), - NOT_IN("Not In"); + 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; @@ -164,6 +324,8 @@ public class Expressionator public String toString() { return _str; } + + public abstract Value eval(Value param1, Object param2, Object param3); } private static final Map PRECENDENCE = @@ -182,6 +344,7 @@ public class Expressionator 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}); @@ -190,7 +353,7 @@ public class Expressionator private static final Expr THIS_COL_VALUE = new Expr() { - @Override protected Object eval(RowContext ctx) { + @Override protected Value eval(RowContext ctx) { return ctx.getThisColumnValue(); } @Override protected void toExprString(StringBuilder sb, boolean isDebug) { @@ -198,18 +361,25 @@ public class Expressionator } }; - private static final Expr NULL_VALUE = new EConstValue(null, "Null"); - private static final Expr TRUE_VALUE = new EConstValue(Boolean.TRUE, "True"); - private static final Expr FALSE_VALUE = new EConstValue(Boolean.FALSE, "False"); + 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, Database db) { + static String testTokenize(Type exprType, String exprStr, + ParseContext context) { + if(context == null) { + context = DEFAULT_PARSE_CONTEXT; + } List tokens = trimSpaces( - ExpressionTokenizer.tokenize(exprType, exprStr, (DatabaseImpl)db)); + ExpressionTokenizer.tokenize(exprType, exprStr, context)); if(tokens == null) { // FIXME, NULL_EXPR? @@ -219,7 +389,12 @@ public class Expressionator return tokens.toString(); } - public static Expr parse(Type exprType, String exprStr, Database db) { + 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 @@ -228,14 +403,14 @@ public class Expressionator // - record validation cannot refer to outside columns List tokens = trimSpaces( - ExpressionTokenizer.tokenize(exprType, exprStr, (DatabaseImpl)db)); + ExpressionTokenizer.tokenize(exprType, exprStr, context)); if(tokens == null) { // FIXME, NULL_EXPR? return null; } - return parseExpression(new TokBuf(exprType, tokens), false); + return parseExpression(new TokBuf(exprType, tokens, context), false); } private static List trimSpaces(List tokens) { @@ -274,7 +449,7 @@ public class Expressionator case LITERAL: - buf.setPendingExpr(new ELiteralValue(t.getValue())); + buf.setPendingExpr(new ELiteralValue(t.getValueType(), t.getValue())); break; case OP: @@ -456,8 +631,13 @@ public class Expressionator buf.next(); List params = findParenExprs(buf, true); - buf.setPendingExpr( - new EFunc(firstTok.getValueStr(), params)); + 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; @@ -602,7 +782,6 @@ public class Expressionator Expr expr = buf.takePendingExpr(); - // FIXME Expr specOpExpr = null; switch(specOp) { case IS_NULL: @@ -774,24 +953,27 @@ public class Expressionator 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) { - this(exprType, false, tokens, null, 0); + 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); + this(parent._exprType, parent._simpleExpr, tokens, parent, parentOff, + parent._context); } private TokBuf(Type exprType, boolean simpleExpr, List tokens, - TokBuf parent, int parentOff) { + 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(); @@ -897,6 +1079,10 @@ public class Expressionator return ExpressionTokenizer.newEntry(pos, toks); } + public Function getFunction(String funcName) { + return _context.getExpressionFunction(funcName); + } + @Override public String toString() { @@ -958,6 +1144,30 @@ public class Expressionator } } + 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()); @@ -1017,6 +1227,7 @@ public class Expressionator Pattern.UNICODE_CASE); } + private interface LeftAssocExpr { public OpType getOp(); public Expr getLeft(); @@ -1029,22 +1240,63 @@ public class Expressionator public void setRight(Expr right); } - public static abstract class Expr + 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() { - return eval(null); + 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) { - Object val = eval(ctx); + public Boolean evalCondition(RowContext ctx) { + Value val = eval(ctx); + + if(val.getType() == ValueType.NULL) { + return null; + } - if(val instanceof Boolean) { - return (Boolean)val; + 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()); } - // a single value as a conditional expression seems to act like an - // implicit "=" - return val.equals(ctx.getThisColumnValue()); + return (Boolean)val.get(); } @Override @@ -1126,31 +1378,23 @@ public class Expressionator return outerExpr; } - protected abstract Object eval(RowContext ctx); + protected abstract Value eval(RowContext ctx); protected abstract void toExprString(StringBuilder sb, boolean isDebug); } - public interface RowContext - { - public Object getThisColumnValue(); - - public Object getRowValue(String collectionName, String objName, - String colName); - } - private static final class EConstValue extends Expr { - private final Object _val; + private final Value _val; private final String _str; - private EConstValue(Object val, String str) { + private EConstValue(Value val, String str) { _val = val; _str = str; } @Override - protected Object eval(RowContext ctx) { + protected Value eval(RowContext ctx) { return _val; } @@ -1162,28 +1406,28 @@ public class Expressionator private static final class ELiteralValue extends Expr { - private final Object _value; + private final Value _val; - private ELiteralValue(Object value) { - _value = value; + private ELiteralValue(ValueType valType, Object value) { + _val = new BuiltinOperators.SimpleValue(valType, value); } @Override - public Object eval(RowContext ctx) { - return _value; + public Value eval(RowContext ctx) { + return _val; } @Override protected void toExprString(StringBuilder sb, boolean isDebug) { - // FIXME, stronger typing? - if(_value instanceof String) { - sb.append("\"").append(_value).append("\""); - } else if(_value instanceof Date) { - // FIXME Date,Time,DateTime formatting? - sb.append("#").append(_value).append("#"); + 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(_value); - } + sb.append(_val.get()); + } } } @@ -1201,7 +1445,7 @@ public class Expressionator } @Override - public Object eval(RowContext ctx) { + public Value eval(RowContext ctx) { return ctx.getRowValue(_collectionName, _objName, _fieldName); } @@ -1217,16 +1461,6 @@ public class Expressionator } } - private static abstract class EOp - { - - } - - private static abstract class ECond - { - - } - private static class EParen extends Expr { private final Expr _expr; @@ -1236,7 +1470,7 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { + protected Value eval(RowContext ctx) { return _expr.eval(ctx); } @@ -1250,24 +1484,22 @@ public class Expressionator private static class EFunc extends Expr { - private final String _name; + private final Function _func; private final List _params; - private EFunc(String name, List params) { - _name = name; + private EFunc(Function func, List params) { + _func = func; _params = params; } @Override - protected Object eval(RowContext ctx) { - // FIXME how do func results act for conditional values? (literals become = tests) - - return false; + protected Value eval(RowContext ctx) { + return _func.eval(exprListToValues(_params, ctx)); } @Override protected void toExprString(StringBuilder sb, boolean isDebug) { - sb.append(_name).append("("); + sb.append(_func.getName()).append("("); if(!_params.isEmpty()) { exprListToString(_params, ",", sb, isDebug); @@ -1320,16 +1552,13 @@ public class Expressionator private static class EBinaryOp extends EBaseBinaryOp { - private EBinaryOp(BinaryOp op, Expr left, Expr right) { super(op, left, right); } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + protected Value eval(RowContext ctx) { + return ((BinaryOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); } } @@ -1357,10 +1586,8 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + protected Value eval(RowContext ctx) { + return ((UnaryOp)_op).eval(_expr.eval(ctx)); } @Override @@ -1380,10 +1607,8 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + protected Value eval(RowContext ctx) { + return ((CompOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); } } @@ -1394,10 +1619,12 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + 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)); } } @@ -1432,10 +1659,8 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), null, null); } @Override @@ -1447,6 +1672,7 @@ public class Expressionator private static class ELikeOp extends ESpecOp { + // FIXME, compile Pattern on first use? private final Pattern _pattern; private final String _patternStr; @@ -1457,18 +1683,15 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + 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(" \"") - .append(_patternStr.replace("\"", "\"\"")) - .append("\""); + sb.append(" ").append(_op).append(" "); + literalStrToString(_patternStr, sb); if(isDebug) { sb.append("(").append(_pattern).append(")"); } @@ -1485,10 +1708,9 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), + exprListToDelayedValues(_exprs, ctx), null); } @Override @@ -1522,10 +1744,10 @@ public class Expressionator } @Override - protected Object eval(RowContext ctx) { - // FIXME - - return null; + protected Value eval(RowContext ctx) { + return _op.eval(_expr.eval(ctx), + new DelayedValue(_startRangeExpr, ctx), + new DelayedValue(_endRangeExpr, ctx)); } @Override diff --git a/src/test/java/com/healthmarketscience/jackcess/util/ExpressionatorTest.java b/src/test/java/com/healthmarketscience/jackcess/util/ExpressionatorTest.java index 039fb8a..8da921f 100644 --- a/src/test/java/com/healthmarketscience/jackcess/util/ExpressionatorTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/util/ExpressionatorTest.java @@ -40,7 +40,7 @@ public class ExpressionatorTest extends TestCase doTestSimpleBinOp("EBinaryOp", "+", "-", "*", "/", "\\", "^", "&", "Mod"); doTestSimpleBinOp("ECompOp", "<", "<=", ">", ">=", "=", "<>"); - doTestSimpleBinOp("ELogicalOp", "And", "Or", "Eqv", "Xor"); + doTestSimpleBinOp("ELogicalOp", "And", "Or", "Eqv", "Xor", "Imp"); for(String constStr : new String[]{"True", "False", "Null"}) { validateExpr(constStr, "{" + constStr + "}"); @@ -120,7 +120,7 @@ public class ExpressionatorTest extends TestCase private static void validateExpr(String exprStr, String debugStr, String cleanStr) { - Expressionator.Expr expr = Expressionator.parse( + Expression expr = Expressionator.parse( Expressionator.Type.FIELD_VALIDATOR, exprStr, null); assertEquals(debugStr, expr.toDebugString()); assertEquals(cleanStr, expr.toString()); -- 2.39.5