aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java37
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Expression.java30
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Function.java28
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java92
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Value.java82
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java112
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java13
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java83
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java80
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java61
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java68
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java61
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java754
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java37
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java36
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java144
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java61
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java673
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java1961
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java61
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java87
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java37
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java6
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java377
24 files changed, 4961 insertions, 20 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java b/src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java
new file mode 100644
index 0000000..a4e21d1
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java
@@ -0,0 +1,37 @@
+/*
+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;
+
+import java.text.SimpleDateFormat;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public interface EvalContext
+{
+ public Value.Type getResultType();
+
+ public TemporalConfig getTemporalConfig();
+
+ public SimpleDateFormat createDateFormat(String formatStr);
+
+ public Value getThisColumnValue();
+
+ public Value getRowValue(String collectionName, String objName,
+ String colName);
+}
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..768909c
--- /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 eval(EvalContext ctx);
+
+ public String toDebugString();
+
+ public boolean isConstant();
+}
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..0d94dde
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/expr/Function.java
@@ -0,0 +1,28 @@
+/*
+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(EvalContext ctx, Value... params);
+ public boolean isPure();
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java
new file mode 100644
index 0000000..1c5bd74
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java
@@ -0,0 +1,92 @@
+/*
+Copyright (c) 2017 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 class TemporalConfig
+{
+ public static final String US_DATE_FORMAT = "M/d/yyyy";
+ public static final String US_TIME_FORMAT_12 = "hh:mm:ss a";
+ public static final String US_TIME_FORMAT_24 = "HH:mm:ss";
+
+ public static final TemporalConfig US_TEMPORAL_CONFIG = new TemporalConfig(
+ US_DATE_FORMAT, US_TIME_FORMAT_12, US_TIME_FORMAT_24, '/', ':');
+
+ private final String _dateFormat;
+ private final String _timeFormat12;
+ private final String _timeFormat24;
+ private final char _dateSeparator;
+ private final char _timeSeparator;
+ private final String _dateTimeFormat12;
+ private final String _dateTimeFormat24;
+
+ public TemporalConfig(String dateFormat, String timeFormat12,
+ String timeFormat24, char dateSeparator,
+ char timeSeparator)
+ {
+ _dateFormat = dateFormat;
+ _timeFormat12 = timeFormat12;
+ _timeFormat24 = timeFormat24;
+ _dateSeparator = dateSeparator;
+ _timeSeparator = timeSeparator;
+ _dateTimeFormat12 = _dateFormat + " " + _timeFormat12;
+ _dateTimeFormat24 = _dateFormat + " " + _timeFormat24;
+ }
+
+ public String getDateFormat() {
+ return _dateFormat;
+ }
+
+ public String getTimeFormat12() {
+ return _timeFormat12;
+ }
+
+ public String getTimeFormat24() {
+ return _timeFormat24;
+ }
+
+ public String getDateTimeFormat12() {
+ return _dateTimeFormat12;
+ }
+
+ public String getDateTimeFormat24() {
+ return _dateTimeFormat24;
+ }
+
+ public String getDefaultDateFormat() {
+ return getDateFormat();
+ }
+
+ public String getDefaultTimeFormat() {
+ return getTimeFormat12();
+ }
+
+ public String getDefaultDateTimeFormat() {
+ return getDateTimeFormat12();
+ }
+
+ public char getDateSeparator() {
+ return _dateSeparator;
+ }
+
+ public char getTimeSeparator() {
+ return _timeSeparator;
+ }
+}
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..ad0e587
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java
@@ -0,0 +1,82 @@
+/*
+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;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public interface Value
+{
+ public enum Type
+ {
+ NULL, STRING, DATE, TIME, DATE_TIME, LONG, DOUBLE, BIG_DEC;
+
+ public boolean isNumeric() {
+ return inRange(LONG, BIG_DEC);
+ }
+
+ public boolean isIntegral() {
+ // note when BOOLEAN is converted to number, it is integral
+ return (this == LONG);
+ }
+
+ public boolean isTemporal() {
+ return inRange(DATE, DATE_TIME);
+ }
+
+ public Type getPreferredFPType() {
+ return((ordinal() <= DOUBLE.ordinal()) ? DOUBLE : BIG_DEC);
+ }
+
+ public Type getPreferredNumericType() {
+ if(isNumeric()) {
+ return this;
+ }
+ if(isTemporal()) {
+ return ((this == DATE) ? LONG : DOUBLE);
+ }
+ return null;
+ }
+
+ private boolean inRange(Type start, Type end) {
+ return ((start.ordinal() <= ordinal()) && (ordinal() <= end.ordinal()));
+ }
+ }
+
+
+ public Type getType();
+
+ public Object get();
+
+ public boolean isNull();
+
+ public boolean getAsBoolean();
+
+ public String getAsString();
+
+ public Date getAsDateTime(EvalContext ctx);
+
+ public Long getAsLong();
+
+ public Double getAsDouble();
+
+ public BigDecimal getAsBigDecimal();
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
index 998e80a..203ad82 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
@@ -796,8 +796,26 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
*/
public long fromDateDouble(double value)
{
+ return fromDateDouble(value, getCalendar());
+ }
+
+ /**
+ * Returns a java long time value converted from an access date double.
+ * @usage _advanced_method_
+ */
+ public static long fromDateDouble(double value, DatabaseImpl db)
+ {
+ return fromDateDouble(value, db.getCalendar());
+ }
+
+ /**
+ * Returns a java long time value converted from an access date double.
+ * @usage _advanced_method_
+ */
+ public static long fromDateDouble(double value, Calendar c)
+ {
long localTime = fromLocalDateDouble(value);
- return localTime - getFromLocalTimeZoneOffset(localTime);
+ return localTime - getFromLocalTimeZoneOffset(localTime, c);
}
static long fromLocalDateDouble(double value)
@@ -843,10 +861,30 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
*/
public double toDateDouble(Object value)
{
+ return toDateDouble(value, getCalendar());
+ }
+
+ /**
+ * Returns an access date double converted from a java Date/Calendar/Number
+ * time value.
+ * @usage _advanced_method_
+ */
+ public static double toDateDouble(Object value, DatabaseImpl db)
+ {
+ return toDateDouble(value, db.getCalendar());
+ }
+
+ /**
+ * Returns an access date double converted from a java Date/Calendar/Number
+ * time value.
+ * @usage _advanced_method_
+ */
+ public static double toDateDouble(Object value, Calendar c)
+ {
// seems access stores dates in the local timezone. guess you just
// hope you read it in the same timezone in which it was written!
long time = toDateLong(value);
- time += getToLocalTimeZoneOffset(time);
+ time += getToLocalTimeZoneOffset(time, c);
return toLocalDateDouble(time);
}
@@ -881,9 +919,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* Gets the timezone offset from UTC to local time for the given time
* (including DST).
*/
- private long getToLocalTimeZoneOffset(long time)
+ private static long getToLocalTimeZoneOffset(long time, Calendar c)
{
- Calendar c = getCalendar();
c.setTimeInMillis(time);
return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
}
@@ -892,11 +929,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* Gets the timezone offset from local time to UTC for the given time
* (including DST).
*/
- private long getFromLocalTimeZoneOffset(long time)
+ private static long getFromLocalTimeZoneOffset(long time, Calendar c)
{
// getting from local time back to UTC is a little wonky (and not
// guaranteed to get you back to where you started)
- Calendar c = getCalendar();
c.setTimeInMillis(time);
// apply the zone offset first to get us closer to the original time
c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET));
@@ -1414,7 +1450,17 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* <code>null</code> is returned as 0 and Numbers are converted
* using their double representation.
*/
- static BigDecimal toBigDecimal(Object value)
+ BigDecimal toBigDecimal(Object value)
+ {
+ return toBigDecimal(value, getDatabase());
+ }
+
+ /**
+ * @return an appropriate BigDecimal representation of the given object.
+ * <code>null</code> is returned as 0 and Numbers are converted
+ * using their double representation.
+ */
+ static BigDecimal toBigDecimal(Object value, DatabaseImpl db)
{
if(value == null) {
return BigDecimal.ZERO;
@@ -1424,6 +1470,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return new BigDecimal((BigInteger)value);
} else if(value instanceof Number) {
return new BigDecimal(((Number)value).doubleValue());
+ } else if(value instanceof Boolean) {
+ // access seems to like -1 for true and 0 for false
+ return ((Boolean)value) ? BigDecimal.valueOf(-1) : BigDecimal.ZERO;
+ } else if(value instanceof Date) {
+ return new BigDecimal(toDateDouble(value, db));
}
return new BigDecimal(value.toString());
}
@@ -1433,12 +1484,27 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* <code>null</code> is returned as 0 and Strings are parsed as
* Doubles.
*/
- private static Number toNumber(Object value)
+ private Number toNumber(Object value)
+ {
+ return toNumber(value, getDatabase());
+ }
+
+ /**
+ * @return an appropriate Number representation of the given object.
+ * <code>null</code> is returned as 0 and Strings are parsed as
+ * Doubles.
+ */
+ private static Number toNumber(Object value, DatabaseImpl db)
{
if(value == null) {
return BigDecimal.ZERO;
} else if(value instanceof Number) {
return (Number)value;
+ } else if(value instanceof Boolean) {
+ // access seems to like -1 for true and 0 for false
+ return ((Boolean)value) ? -1 : 0;
+ } else if(value instanceof Date) {
+ return toDateDouble(value, db);
}
return Double.valueOf(value.toString());
}
@@ -1524,6 +1590,15 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
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.0d);
}
return Boolean.parseBoolean(obj.toString());
}
@@ -1540,11 +1615,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
}
/**
- * Treat booleans as integers (C-style).
+ * Treat booleans as integers (access-style).
*/
protected static Object booleanToInteger(Object obj) {
if (obj instanceof Boolean) {
- obj = ((Boolean) obj) ? 1 : 0;
+ obj = ((Boolean) obj) ? -1 : 0;
}
return obj;
}
@@ -1761,7 +1836,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
* Converts the given value to the "internal" representation for the given
* data type.
*/
- public static Object toInternalValue(DataType dataType, Object value)
+ public static Object toInternalValue(DataType dataType, Object value,
+ DatabaseImpl db)
throws IOException
{
if(value == null) {
@@ -1772,21 +1848,21 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
case BOOLEAN:
return ((value instanceof Boolean) ? value : toBooleanValue(value));
case BYTE:
- return ((value instanceof Byte) ? value : toNumber(value).byteValue());
+ return ((value instanceof Byte) ? value : toNumber(value, db).byteValue());
case INT:
return ((value instanceof Short) ? value :
- toNumber(value).shortValue());
+ toNumber(value, db).shortValue());
case LONG:
return ((value instanceof Integer) ? value :
- toNumber(value).intValue());
+ toNumber(value, db).intValue());
case MONEY:
- return toBigDecimal(value);
+ return toBigDecimal(value, db);
case FLOAT:
return ((value instanceof Float) ? value :
- toNumber(value).floatValue());
+ toNumber(value, db).floatValue());
case DOUBLE:
return ((value instanceof Double) ? value :
- toNumber(value).doubleValue());
+ toNumber(value, db).doubleValue());
case SHORT_DATE_TIME:
return ((value instanceof DateExt) ? value :
new Date(toDateLong(value)));
@@ -1796,7 +1872,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return ((value instanceof String) ? value :
toCharSequence(value).toString());
case NUMERIC:
- return toBigDecimal(value);
+ return toBigDecimal(value, db);
case COMPLEX_TYPE:
// leave alone for now?
return value;
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
index ac253fb..319879b 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
@@ -28,6 +28,7 @@ import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -722,6 +723,18 @@ public class DatabaseImpl implements Database
}
/**
+ * Returns a SimpleDateFormat for the given format string which is
+ * configured with a compatible Calendar instance (see
+ * {@link DatabaseBuilder#toCompatibleCalendar}) and this database's
+ * {@link TimeZone}.
+ */
+ public SimpleDateFormat createDateFormat(String formatStr) {
+ SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
+ sdf.setTimeZone(getTimeZone());
+ return sdf;
+ }
+
+ /**
* @returns the current handler for reading/writing properties, creating if
* necessary
*/
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java
new file mode 100644
index 0000000..4bf03af
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java
@@ -0,0 +1,83 @@
+/*
+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.DateFormat;
+import java.util.Date;
+
+import com.healthmarketscience.jackcess.impl.ColumnImpl;
+import com.healthmarketscience.jackcess.expr.EvalContext;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public abstract class BaseDateValue extends BaseValue
+{
+ private final Date _val;
+ private final DateFormat _fmt;
+
+ public BaseDateValue(Date val, DateFormat fmt)
+ {
+ _val = val;
+ _fmt = fmt;
+ }
+
+ public Object get() {
+ return _val;
+ }
+
+ protected DateFormat getFormat() {
+ return _fmt;
+ }
+
+ protected Double getNumber() {
+ return ColumnImpl.toDateDouble(_val, _fmt.getCalendar());
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ // ms access seems to treat dates/times as "true"
+ return true;
+ }
+
+ @Override
+ public String getAsString() {
+ return _fmt.format(_val);
+ }
+
+ @Override
+ public Date getAsDateTime(EvalContext ctx) {
+ return _val;
+ }
+
+ @Override
+ public Long getAsLong() {
+ return getNumber().longValue();
+ }
+
+ @Override
+ public Double getAsDouble() {
+ return getNumber();
+ }
+
+ @Override
+ public BigDecimal getAsBigDecimal() {
+ return BigDecimal.valueOf(getNumber());
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java
new file mode 100644
index 0000000..c34a914
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java
@@ -0,0 +1,80 @@
+/*
+Copyright (c) 2017 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.util.Date;
+
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.Value;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public abstract class BaseDelayedValue implements Value
+{
+ private Value _val;
+
+ protected BaseDelayedValue() {
+ }
+
+ private Value getDelegate() {
+ if(_val == null) {
+ _val = eval();
+ }
+ return _val;
+ }
+
+ public boolean isNull() {
+ return(getType() == Type.NULL);
+ }
+
+ public Value.Type getType() {
+ return getDelegate().getType();
+ }
+
+ public Object get() {
+ return getDelegate().get();
+ }
+
+ public boolean getAsBoolean() {
+ return getDelegate().getAsBoolean();
+ }
+
+ public String getAsString() {
+ return getDelegate().getAsString();
+ }
+
+ public Date getAsDateTime(EvalContext ctx) {
+ return getDelegate().getAsDateTime(ctx);
+ }
+
+ public Long getAsLong() {
+ return getDelegate().getAsLong();
+ }
+
+ public Double getAsDouble() {
+ return getDelegate().getAsDouble();
+ }
+
+ public BigDecimal getAsBigDecimal() {
+ return getDelegate().getAsBigDecimal();
+ }
+
+ protected abstract Value eval();
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java
new file mode 100644
index 0000000..585c71e
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java
@@ -0,0 +1,61 @@
+/*
+Copyright (c) 2017 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.Date;
+
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.impl.ColumnImpl;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public abstract class BaseNumericValue extends BaseValue
+{
+
+ protected BaseNumericValue()
+ {
+ }
+
+ @Override
+ public String getAsString() {
+ return getNumber().toString();
+ }
+
+ @Override
+ public Long getAsLong() {
+ return getNumber().longValue();
+ }
+
+ @Override
+ public Double getAsDouble() {
+ return getNumber().doubleValue();
+ }
+
+ @Override
+ public Date getAsDateTime(EvalContext ctx) {
+ double d = getNumber().doubleValue();
+
+ SimpleDateFormat sdf = ctx.createDateFormat(
+ ctx.getTemporalConfig().getDefaultDateTimeFormat());
+ return new Date(ColumnImpl.fromDateDouble(d, sdf.getCalendar()));
+ }
+
+ protected abstract Number getNumber();
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java
new file mode 100644
index 0000000..e30c303
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.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.impl.expr;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.expr.EvalContext;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public abstract class BaseValue implements Value
+{
+ public boolean isNull() {
+ return(getType() == Type.NULL);
+ }
+
+ public boolean getAsBoolean() {
+ throw invalidConversion(Value.Type.LONG);
+ }
+
+ public String getAsString() {
+ throw invalidConversion(Value.Type.STRING);
+ }
+
+ public Date getAsDateTime(EvalContext ctx) {
+ throw invalidConversion(Value.Type.DATE_TIME);
+ }
+
+ public Long getAsLong() {
+ throw invalidConversion(Value.Type.LONG);
+ }
+
+ public Double getAsDouble() {
+ throw invalidConversion(Value.Type.DOUBLE);
+ }
+
+ public BigDecimal getAsBigDecimal() {
+ throw invalidConversion(Value.Type.BIG_DEC);
+ }
+
+ private UnsupportedOperationException invalidConversion(Value.Type newType) {
+ return new UnsupportedOperationException(
+ getType() + " value cannot be converted to " + newType);
+ }
+
+ @Override
+ public String toString() {
+ return "Value[" + getType() + "] '" + get() + "'";
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java
new file mode 100644
index 0000000..3b64c51
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java
@@ -0,0 +1,61 @@
+/*
+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;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class BigDecimalValue extends BaseNumericValue
+{
+ private final BigDecimal _val;
+
+ public BigDecimalValue(BigDecimal val)
+ {
+ _val = val;
+ }
+
+ public Type getType() {
+ return Type.BIG_DEC;
+ }
+
+ public Object get() {
+ return _val;
+ }
+
+ @Override
+ protected Number getNumber() {
+ return _val;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return (_val.compareTo(BigDecimal.ZERO) != 0L);
+ }
+
+ @Override
+ public String getAsString() {
+ return _val.toPlainString();
+ }
+
+ @Override
+ public BigDecimal getAsBigDecimal() {
+ return _val;
+ }
+}
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..5a3d7b5
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
@@ -0,0 +1,754 @@
+/*
+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.math.BigInteger;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.impl.ColumnImpl;
+
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class BuiltinOperators
+{
+ private static final String DIV_BY_ZERO = "/ by zero";
+
+ public static final Value NULL_VAL = new BaseValue() {
+ @Override public boolean isNull() {
+ return true;
+ }
+ public Type getType() {
+ return Type.NULL;
+ }
+ public Object get() {
+ return null;
+ }
+ };
+ // access seems to like -1 for true and 0 for false (boolean values are
+ // basically an illusion)
+ public static final Value TRUE_VAL = new LongValue(-1L);
+ public static final Value FALSE_VAL = new LongValue(0L);
+ public static final Value EMPTY_STR_VAL = new StringValue("");
+
+ private enum CoercionType {
+ SIMPLE(true, true), GENERAL(false, true), COMPARE(false, false);
+
+ final boolean _preferTemporal;
+ final boolean _allowCoerceStringToNum;
+
+ private CoercionType(boolean preferTemporal,
+ boolean allowCoerceStringToNum) {
+ _preferTemporal = preferTemporal;
+ _allowCoerceStringToNum = allowCoerceStringToNum;
+ }
+ }
+
+ private BuiltinOperators() {}
+
+ // null propagation rules:
+ // 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 '&'
+
+ public static Value negate(EvalContext ctx, Value param1) {
+ if(param1.isNull()) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = param1.getType();
+
+ switch(mathType) {
+ // case STRING: break; unsupported
+ case DATE:
+ case TIME:
+ case DATE_TIME:
+ // dates/times get converted to date doubles for arithmetic
+ double result = -param1.getAsDouble();
+ return toDateValue(ctx, mathType, result, param1, null);
+ case LONG:
+ return toValue(-param1.getAsLong());
+ case DOUBLE:
+ return toValue(-param1.getAsDouble());
+ case BIG_DEC:
+ return toValue(param1.getAsBigDecimal().negate());
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ public static Value add(EvalContext ctx, Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.SIMPLE);
+
+ switch(mathType) {
+ case STRING:
+ // string '+' is a null-propagation (handled above) concat
+ return nonNullConcat(param1, param2);
+ case DATE:
+ case TIME:
+ case DATE_TIME:
+ // dates/times get converted to date doubles for arithmetic
+ double result = param1.getAsDouble() + param2.getAsDouble();
+ return toDateValue(ctx, mathType, result, param1, param2);
+ case LONG:
+ return toValue(param1.getAsLong() + param2.getAsLong());
+ case DOUBLE:
+ return toValue(param1.getAsDouble() + param2.getAsDouble());
+ case BIG_DEC:
+ return toValue(param1.getAsBigDecimal().add(param2.getAsBigDecimal()));
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ public static Value subtract(EvalContext ctx, Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.SIMPLE);
+
+ switch(mathType) {
+ // case STRING: break; unsupported
+ case DATE:
+ case TIME:
+ case DATE_TIME:
+ // dates/times get converted to date doubles for arithmetic
+ double result = param1.getAsDouble() - param2.getAsDouble();
+ return toDateValue(ctx, mathType, result, param1, param2);
+ case LONG:
+ return toValue(param1.getAsLong() - param2.getAsLong());
+ case DOUBLE:
+ return toValue(param1.getAsDouble() - param2.getAsDouble());
+ case BIG_DEC:
+ return toValue(param1.getAsBigDecimal().subtract(param2.getAsBigDecimal()));
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ public static Value multiply(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
+
+ switch(mathType) {
+ // case STRING: break; unsupported
+ // case DATE: break; promoted to double
+ // case TIME: break; promoted to double
+ // case DATE_TIME: break; promoted to double
+ case LONG:
+ return toValue(param1.getAsLong() * param2.getAsLong());
+ case DOUBLE:
+ return toValue(param1.getAsDouble() * param2.getAsDouble());
+ case BIG_DEC:
+ return toValue(param1.getAsBigDecimal().multiply(param2.getAsBigDecimal()));
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ public static Value divide(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
+
+ switch(mathType) {
+ // case STRING: break; unsupported
+ // case DATE: break; promoted to double
+ // case TIME: break; promoted to double
+ // case DATE_TIME: break; promoted to double
+ case LONG:
+ long lp1 = param1.getAsLong();
+ long lp2 = param2.getAsLong();
+ if((lp1 % lp2) == 0) {
+ return toValue(lp1 / lp2);
+ }
+ return toValue((double)lp1 / (double)lp2);
+ case DOUBLE:
+ double d2 = param2.getAsDouble();
+ if(d2 == 0.0d) {
+ throw new ArithmeticException(DIV_BY_ZERO);
+ }
+ return toValue(param1.getAsDouble() / d2);
+ case BIG_DEC:
+ return toValue(param1.getAsBigDecimal().divide(param2.getAsBigDecimal()));
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ @SuppressWarnings("fallthrough")
+ public static Value intDivide(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
+
+ boolean wasDouble = false;
+ switch(mathType) {
+ // case STRING: break; unsupported
+ // case DATE: break; promoted to double
+ // case TIME: break; promoted to double
+ // case DATE_TIME: break; promoted to double
+ case LONG:
+ return toValue(param1.getAsLong() / param2.getAsLong());
+ case DOUBLE:
+ wasDouble = true;
+ // fallthrough
+ case BIG_DEC:
+ BigInteger result = getAsBigInteger(param1).divide(
+ getAsBigInteger(param2));
+ return (wasDouble ? toValue(result.longValue()) : toValue(result));
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ public static Value exp(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
+
+ // jdk only supports general pow() as doubles, let's go with that
+ double result = Math.pow(param1.getAsDouble(), param2.getAsDouble());
+
+ // attempt to convert integral types back to integrals if possible
+ if((mathType == Value.Type.LONG) && isIntegral(result)) {
+ return toValue((long)result);
+ }
+
+ return toValue(result);
+ }
+
+ @SuppressWarnings("fallthrough")
+ public static Value mod(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
+
+ boolean wasDouble = false;
+ switch(mathType) {
+ // case STRING: break; unsupported
+ // case DATE: break; promoted to double
+ // case TIME: break; promoted to double
+ // case DATE_TIME: break; promoted to double
+ case LONG:
+ return toValue(param1.getAsLong() % param2.getAsLong());
+ case DOUBLE:
+ wasDouble = true;
+ // fallthrough
+ case BIG_DEC:
+ BigInteger bi1 = getAsBigInteger(param1);
+ BigInteger bi2 = getAsBigInteger(param2).abs();
+ if(bi2.signum() == 0) {
+ throw new ArithmeticException(DIV_BY_ZERO);
+ }
+ BigInteger result = bi1.mod(bi2);
+ // BigInteger.mod differs from % when using negative values, need to
+ // make them consistent
+ if((bi1.signum() == -1) && (result.signum() == 1)) {
+ result = result.subtract(bi2);
+ }
+ return (wasDouble ? toValue(result.longValue()) : toValue(result));
+ default:
+ throw new RuntimeException("Unexpected type " + mathType);
+ }
+ }
+
+ public static Value concat(Value param1, Value param2) {
+
+ // note, this op converts null to empty string
+ if(param1.isNull()) {
+ param1 = EMPTY_STR_VAL;
+ }
+
+ if(param2.isNull()) {
+ param2 = EMPTY_STR_VAL;
+ }
+
+ return nonNullConcat(param1, param2);
+ }
+
+ private static Value nonNullConcat(Value param1, Value param2) {
+ return toValue(param1.getAsString().concat(param2.getAsString()));
+ }
+
+ public static Value not(Value param1) {
+ if(param1.isNull()) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ return toValue(!param1.getAsBoolean());
+ }
+
+ 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(param1.isNull()) {
+ return NULL_VAL;
+ }
+
+ boolean b1 = param1.getAsBoolean();
+ if(!b1) {
+ return FALSE_VAL;
+ }
+
+ if(param2.isNull()) {
+ return NULL_VAL;
+ }
+
+ return toValue(param2.getAsBoolean());
+ }
+
+ public static Value or(Value param1, Value param2) {
+
+ // "or" uses short-circuit logic
+
+ if(param1.isNull()) {
+ return NULL_VAL;
+ }
+
+ boolean b1 = param1.getAsBoolean();
+ if(b1) {
+ return TRUE_VAL;
+ }
+
+ if(param2.isNull()) {
+ return NULL_VAL;
+ }
+
+ return toValue(param2.getAsBoolean());
+ }
+
+ public static Value eqv(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ boolean b1 = param1.getAsBoolean();
+ boolean b2 = param2.getAsBoolean();
+
+ return toValue(b1 == b2);
+ }
+
+ public static Value xor(Value param1, Value param2) {
+ if(anyParamIsNull(param1, param2)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ boolean b1 = param1.getAsBoolean();
+ boolean b2 = param2.getAsBoolean();
+
+ return toValue(b1 ^ b2);
+ }
+
+ public static Value imp(Value param1, Value param2) {
+
+ // "imp" uses short-circuit logic
+
+ if(param1.isNull()) {
+ if(param2.isNull() || !param2.getAsBoolean()) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ return TRUE_VAL;
+ }
+
+ boolean b1 = param1.getAsBoolean();
+ if(!b1) {
+ return TRUE_VAL;
+ }
+
+ if(param2.isNull()) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ return toValue(param2.getAsBoolean());
+ }
+
+ public static Value isNull(Value param1) {
+ return toValue(param1.isNull());
+ }
+
+ public static Value isNotNull(Value param1) {
+ return toValue(!param1.isNull());
+ }
+
+ public static Value like(Value param1, Pattern pattern) {
+ if(param1.isNull()) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ return toValue(pattern.matcher(param1.getAsString()).matches());
+ }
+
+ public static Value between(Value param1, Value param2, Value param3) {
+ // null propagate any param. uses short circuit eval of params
+ if(anyParamIsNull(param1, param2, param3)) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ // the between values can be in either order!?!
+ Value min = param2;
+ Value max = param3;
+ Value gt = greaterThan(min, max);
+ if(gt.getAsBoolean()) {
+ min = param3;
+ max = param2;
+ }
+
+ return and(greaterThanEq(param1, min), lessThanEq(param1, max));
+ }
+
+ 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(param1.isNull()) {
+ // null propagation
+ return NULL_VAL;
+ }
+
+ for(Value val : params) {
+ if(val.isNull()) {
+ continue;
+ }
+
+ Value eq = equals(param1, val);
+ if(eq.getAsBoolean()) {
+ return TRUE_VAL;
+ }
+ }
+
+ return FALSE_VAL;
+ }
+
+ public static Value notIn(Value param1, Value[] params) {
+ return not(in(param1, params));
+ }
+
+
+ private static boolean anyParamIsNull(Value param1, Value param2) {
+ return (param1.isNull() || param2.isNull());
+ }
+
+ private static boolean anyParamIsNull(Value param1, Value param2,
+ Value param3) {
+ return (param1.isNull() || param2.isNull() || param3.isNull());
+ }
+
+ protected static int nonNullCompareTo(
+ Value param1, Value param2)
+ {
+ // note that comparison does not do string to num coercion
+ Value.Type compareType = getMathTypePrecedence(param1, param2,
+ CoercionType.COMPARE);
+
+ switch(compareType) {
+ case STRING:
+ // string comparison is only valid if _both_ params are strings
+ if(param1.getType() != param2.getType()) {
+ throw new RuntimeException("Unexpected type " + compareType);
+ }
+ return param1.getAsString().compareToIgnoreCase(param2.getAsString());
+ // case DATE: break; promoted to double
+ // case TIME: break; promoted to double
+ // case DATE_TIME: break; promoted to double
+ case LONG:
+ return param1.getAsLong().compareTo(param2.getAsLong());
+ case DOUBLE:
+ return param1.getAsDouble().compareTo(param2.getAsDouble());
+ case BIG_DEC:
+ return param1.getAsBigDecimal().compareTo(param2.getAsBigDecimal());
+ default:
+ throw new RuntimeException("Unexpected type " + compareType);
+ }
+ }
+
+ public static Value toValue(boolean b) {
+ return (b ? TRUE_VAL : FALSE_VAL);
+ }
+
+ public static Value toValue(String s) {
+ return new StringValue(s);
+ }
+
+ public static Value toValue(Long s) {
+ return new LongValue(s);
+ }
+
+ public static Value toValue(Double s) {
+ return new DoubleValue(s);
+ }
+
+ public static Value toValue(BigInteger s) {
+ return toValue(new BigDecimal(s));
+ }
+
+ public static Value toValue(BigDecimal s) {
+ return new BigDecimalValue(s);
+ }
+
+ private static Value toDateValue(EvalContext ctx, Value.Type type, double v,
+ Value param1, Value param2)
+ {
+ DateFormat fmt = null;
+ if((param1 instanceof BaseDateValue) && (param1.getType() == type)) {
+ fmt = ((BaseDateValue)param1).getFormat();
+ } else if((param2 instanceof BaseDateValue) && (param2.getType() == type)) {
+ fmt = ((BaseDateValue)param2).getFormat();
+ } else {
+ String fmtStr = null;
+ switch(type) {
+ case DATE:
+ fmtStr = ctx.getTemporalConfig().getDefaultDateFormat();
+ break;
+ case TIME:
+ fmtStr = ctx.getTemporalConfig().getDefaultTimeFormat();
+ break;
+ case DATE_TIME:
+ fmtStr = ctx.getTemporalConfig().getDefaultDateTimeFormat();
+ break;
+ default:
+ throw new RuntimeException("Unexpected type " + type);
+ }
+ fmt = ctx.createDateFormat(fmtStr);
+ }
+
+ Date d = new Date(ColumnImpl.fromDateDouble(v, fmt.getCalendar()));
+
+ switch(type) {
+ case DATE:
+ return new DateValue(d, fmt);
+ case TIME:
+ return new TimeValue(d, fmt);
+ case DATE_TIME:
+ return new DateTimeValue(d, fmt);
+ default:
+ throw new RuntimeException("Unexpected type " + type);
+ }
+ }
+
+ private static Value.Type getMathTypePrecedence(
+ Value param1, Value param2, CoercionType cType)
+ {
+ Value.Type t1 = param1.getType();
+ Value.Type t2 = param2.getType();
+
+ // note: for general math, date/time become double
+
+ if(t1 == t2) {
+
+ if(!cType._preferTemporal && t1.isTemporal()) {
+ return t1.getPreferredNumericType();
+ }
+
+ return t1;
+ }
+
+ if((t1 == Value.Type.STRING) || (t2 == Value.Type.STRING)) {
+
+ if(cType._allowCoerceStringToNum) {
+ // see if this is mixed string/numeric and the string can be coerced
+ // to a number
+ Value.Type numericType = coerceStringToNumeric(param1, param2, cType);
+ if(numericType != null) {
+ // string can be coerced to number
+ return numericType;
+ }
+ }
+
+ // string always wins
+ return Value.Type.STRING;
+ }
+
+ // for "simple" math, keep as date/times
+ if(cType._preferTemporal &&
+ (t1.isTemporal() || t2.isTemporal())) {
+ return (t1.isTemporal() ?
+ (t2.isTemporal() ?
+ // for mixed temporal types, always go to date/time
+ Value.Type.DATE_TIME : t1) :
+ t2);
+ }
+
+ t1 = t1.getPreferredNumericType();
+ t2 = t2.getPreferredNumericType();
+
+ // if both types are integral, choose "largest"
+ if(t1.isIntegral() && t2.isIntegral()) {
+ return max(t1, t2);
+ }
+
+ // choose largest relevant floating-point type
+ return max(t1.getPreferredFPType(), t2.getPreferredFPType());
+ }
+
+ private static Value.Type coerceStringToNumeric(
+ Value param1, Value param2, CoercionType cType) {
+ Value.Type t1 = param1.getType();
+ Value.Type t2 = param2.getType();
+
+ Value.Type prefType = null;
+ Value strParam = null;
+ if(t1.isNumeric()) {
+ prefType = t1;
+ strParam = param2;
+ } else if(t2.isNumeric()) {
+ prefType = t2;
+ strParam = param1;
+ } else if(t1.isTemporal()) {
+ prefType = (cType._preferTemporal ? t1 : t1.getPreferredNumericType());
+ strParam = param2;
+ } else if(t2.isTemporal()) {
+ prefType = (cType._preferTemporal ? t2 : t2.getPreferredNumericType());
+ strParam = param1;
+ } else {
+ // no numeric type involved
+ return null;
+ }
+
+ try {
+ // see if string can be coerced to a number
+ strParam.getAsBigDecimal();
+ return prefType;
+ } catch(NumberFormatException ignored) {
+ // not a number
+ }
+
+ return null;
+ }
+
+ private static Value.Type max(Value.Type t1, Value.Type t2) {
+ return ((t1.compareTo(t2) > 0) ? t1 : t2);
+ }
+
+ static boolean isIntegral(double d) {
+ return ((d == Math.rint(d)) && !Double.isInfinite(d) && !Double.isNaN(d));
+ }
+
+ private static BigInteger getAsBigInteger(Value v) {
+ return v.getAsBigDecimal().toBigInteger();
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java
new file mode 100644
index 0000000..abc047f
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java
@@ -0,0 +1,37 @@
+/*
+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.DateFormat;
+import java.util.Date;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class DateTimeValue extends BaseDateValue
+{
+
+ public DateTimeValue(Date val, DateFormat fmt)
+ {
+ super(val, fmt);
+ }
+
+ public Type getType() {
+ return Type.DATE_TIME;
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java
new file mode 100644
index 0000000..558e3ab
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java
@@ -0,0 +1,36 @@
+/*
+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.DateFormat;
+import java.util.Date;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class DateValue extends BaseDateValue
+{
+ public DateValue(Date val, DateFormat fmt)
+ {
+ super(val, fmt);
+ }
+
+ public Type getType() {
+ return Type.DATE;
+ }
+}
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..fbcd683
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java
@@ -0,0 +1,144 @@
+/*
+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.Value;
+import com.healthmarketscience.jackcess.expr.Function;
+import com.healthmarketscience.jackcess.expr.EvalContext;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class DefaultFunctions
+{
+ private static final Map<String,Function> FUNCS =
+ new HashMap<String,Function>();
+
+ 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;
+ }
+
+ public boolean isPure() {
+ // most functions are probably pure, so make this the default
+ return true;
+ }
+
+ 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);
+ }
+ }
+
+ @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(EvalContext ctx, Value... params) {
+ validateNumParams(params);
+ return eval1(ctx, params[0]);
+ }
+
+ protected abstract Value eval1(EvalContext ctx, Value param);
+ }
+
+ public static abstract class Func2 extends BaseFunction
+ {
+ protected Func2(String name) {
+ super(name, 2, 2);
+ }
+
+ public final Value eval(EvalContext ctx, Value... params) {
+ validateNumParams(params);
+ return eval2(ctx, params[0], params[1]);
+ }
+
+ protected abstract Value eval2(EvalContext ctx, Value param1, Value param2);
+ }
+
+ public static abstract class Func3 extends BaseFunction
+ {
+ protected Func3(String name) {
+ super(name, 3, 3);
+ }
+
+ public final Value eval(EvalContext ctx, Value... params) {
+ validateNumParams(params);
+ return eval3(ctx, params[0], params[1], params[2]);
+ }
+
+ protected abstract Value eval3(EvalContext ctx,
+ Value param1, Value param2, Value param3);
+ }
+
+ public static final Function IIF = registerFunc(new Func3("IIf") {
+ @Override
+ protected Value eval3(EvalContext ctx,
+ Value param1, Value param2, Value param3) {
+ // null is false
+ return ((!param1.isNull() && param1.getAsBoolean()) ? param2 : param3);
+ }
+ });
+
+
+
+ // https://www.techonthenet.com/access/functions/
+ // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83
+
+ private static Function registerFunc(Function func) {
+ if(FUNCS.put(func.getName().toLowerCase(), func) != null) {
+ throw new IllegalStateException("Duplicate function " + func);
+ }
+ return func;
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java
new file mode 100644
index 0000000..7fcd840
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java
@@ -0,0 +1,61 @@
+/*
+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;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class DoubleValue extends BaseNumericValue
+{
+ private final Double _val;
+
+ public DoubleValue(Double val)
+ {
+ _val = val;
+ }
+
+ public Type getType() {
+ return Type.DOUBLE;
+ }
+
+ public Object get() {
+ return _val;
+ }
+
+ @Override
+ protected Number getNumber() {
+ return _val;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return (_val.doubleValue() != 0.0d);
+ }
+
+ @Override
+ public Double getAsDouble() {
+ return _val;
+ }
+
+ @Override
+ public BigDecimal getAsBigDecimal() {
+ return BigDecimal.valueOf(_val);
+ }
+}
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..596b3f0
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java
@@ -0,0 +1,673 @@
+/*
+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.DateFormat;
+import java.text.FieldPosition;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import static com.healthmarketscience.jackcess.impl.expr.Expressionator.*;
+import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.expr.TemporalConfig;
+
+
+/**
+ *
+ * @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 int AMPM_SUFFIX_LEN = 3;
+ private static final String AM_SUFFIX = " am";
+ private static final String PM_SUFFIX = " pm";
+ // access times are based on this date (not the UTC base)
+ private static final String BASE_DATE = "12/30/1899 ";
+ private static final String BASE_DATE_FMT = "M/d/yyyy";
+
+ 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<String> TWO_CHAR_COMP_OPS = new HashSet<String>(
+ 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<Token> tokenize(Type exprType, String exprStr,
+ ParseContext context) {
+
+ if(exprStr != null) {
+ exprStr = exprStr.trim();
+ }
+
+ if((exprStr == null) || (exprStr.length() == 0)) {
+ return null;
+ }
+
+ List<Token> tokens = new ArrayList<Token>();
+
+ ExprBuf buf = new ExprBuf(exprStr, context);
+
+ 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));
+ 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)
+ {
+ TemporalConfig cfg = buf.getTemporalConfig();
+ String dateStr = parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null);
+
+ boolean hasDate = (dateStr.indexOf(cfg.getDateSeparator()) >= 0);
+ boolean hasTime = (dateStr.indexOf(cfg.getTimeSeparator()) >= 0);
+ boolean hasAmPm = false;
+
+ if(hasTime) {
+ int strLen = dateStr.length();
+ hasTime = ((strLen >= AMPM_SUFFIX_LEN) &&
+ (dateStr.regionMatches(true, strLen - AMPM_SUFFIX_LEN,
+ AM_SUFFIX, 0, AMPM_SUFFIX_LEN) ||
+ dateStr.regionMatches(true, strLen - AMPM_SUFFIX_LEN,
+ PM_SUFFIX, 0, AMPM_SUFFIX_LEN)));
+ }
+
+ DateFormat sdf = null;
+ Value.Type valType = null;
+ if(hasDate && hasTime) {
+ sdf = (hasAmPm ? buf.getDateTimeFormat12() : buf.getDateTimeFormat24());
+ valType = Value.Type.DATE_TIME;
+ } else if(hasDate) {
+ sdf = buf.getDateFormat();
+ valType = Value.Type.DATE;
+ } else if(hasTime) {
+ sdf = (hasAmPm ? buf.getTimeFormat12() : buf.getTimeFormat24());
+ 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,
+ sdf);
+ } 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;
+ boolean isFp = false;
+ int expPos = -1;
+
+ try {
+
+ int c = EOF;
+ while((c = buf.peekNext()) != EOF) {
+ if(isDigit(c)) {
+ hasDigit = true;
+ sb.append((char)c);
+ buf.next();
+ } else if(c == '.') {
+ isFp = true;
+ sb.append((char)c);
+ buf.next();
+ } else if(hasDigit && (expPos < 0) && ((c == 'e') || (c == 'E'))) {
+ isFp = true;
+ sb.append((char)c);
+ expPos = sb.length();
+ buf.next();
+ } else if((expPos == sb.length()) && ((c == '-') || (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?
+ Object num = (isFp ?
+ (Number)Double.valueOf(numStr) :
+ (Number)Long.valueOf(numStr));
+ foundNum = true;
+ return new Token(TokenType.LITERAL, num, numStr,
+ (isFp ? Value.Type.DOUBLE : Value.Type.LONG));
+ } 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 <K,V> Map.Entry<K,V> newEntry(K a, V b) {
+ return new AbstractMap.SimpleImmutableEntry<K,V>(a, b);
+ }
+
+ private static final class ExprBuf
+ {
+ private final String _str;
+ private final ParseContext _ctx;
+ private int _pos;
+ private DateFormat _dateFmt;
+ private DateFormat _timeFmt12;
+ private DateFormat _dateTimeFmt12;
+ private DateFormat _timeFmt24;
+ private DateFormat _dateTimeFmt24;
+ private String _baseDate;
+ private final StringBuilder _scratch = new StringBuilder();
+
+ private ExprBuf(String str, ParseContext ctx) {
+ _str = str;
+ _ctx = ctx;
+ }
+
+ 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 TemporalConfig getTemporalConfig() {
+ return _ctx.getTemporalConfig();
+ }
+
+ public DateFormat getDateFormat() {
+ if(_dateFmt == null) {
+ _dateFmt = _ctx.createDateFormat(getTemporalConfig().getDateFormat());
+ }
+ return _dateFmt;
+ }
+
+ public DateFormat getTimeFormat12() {
+ if(_timeFmt12 == null) {
+ _timeFmt12 = new TimeFormat(
+ getDateTimeFormat12(), _ctx.createDateFormat(
+ getTemporalConfig().getTimeFormat12()),
+ getBaseDate());
+ }
+ return _timeFmt12;
+ }
+
+ public DateFormat getDateTimeFormat12() {
+ if(_dateTimeFmt12 == null) {
+ _dateTimeFmt12 = _ctx.createDateFormat(
+ getTemporalConfig().getDateTimeFormat12());
+ }
+ return _dateTimeFmt12;
+ }
+
+ public DateFormat getTimeFormat24() {
+ if(_timeFmt24 == null) {
+ _timeFmt24 = new TimeFormat(
+ getDateTimeFormat24(), _ctx.createDateFormat(
+ getTemporalConfig().getTimeFormat24()),
+ getBaseDate());
+ }
+ return _timeFmt24;
+ }
+
+ public DateFormat getDateTimeFormat24() {
+ if(_dateTimeFmt24 == null) {
+ _dateTimeFmt24 = _ctx.createDateFormat(
+ getTemporalConfig().getDateTimeFormat24());
+ }
+ return _dateTimeFmt24;
+ }
+
+ private String getBaseDate() {
+ if(_baseDate == null) {
+ String dateFmt = getTemporalConfig().getDateFormat();
+ String baseDate = BASE_DATE;
+ if(!BASE_DATE_FMT.equals(dateFmt)) {
+ try {
+ // need to reformat the base date to the relevant date format
+ DateFormat df = _ctx.createDateFormat(BASE_DATE_FMT);
+ baseDate = getDateFormat().format(df.parse(baseDate));
+ } catch(Exception e) {
+ throw new IllegalStateException("Could not parse base date", e);
+ }
+ }
+ _baseDate = baseDate + " ";
+ }
+ return _baseDate;
+ }
+
+ @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 final DateFormat _sdf;
+
+ private Token(TokenType type, String val) {
+ this(type, val, val);
+ }
+
+ private Token(TokenType type, Object val, String valStr) {
+ this(type, val, valStr, null, null);
+ }
+
+ private Token(TokenType type, Object val, String valStr, Value.Type valType) {
+ this(type, val, valStr, valType, null);
+ }
+
+ private Token(TokenType type, Object val, String valStr, Value.Type valType,
+ DateFormat sdf) {
+ _type = type;
+ _val = ((val != null) ? val : valStr);
+ _valStr = valStr;
+ _valType = valType;
+ _sdf = sdf;
+ }
+
+ public TokenType getType() {
+ return _type;
+ }
+
+ public Object getValue() {
+ return _val;
+ }
+
+ public String getValueStr() {
+ return _valStr;
+ }
+
+ public Value.Type getValueType() {
+ return _valType;
+ }
+
+ public DateFormat getDateFormat() {
+ return _sdf;
+ }
+
+ @Override
+ public String toString() {
+ if(_type == TokenType.SPACE) {
+ return "' '";
+ }
+ String str = "[" + _type + "] '" + _val + "'";
+ if(_valType != null) {
+ str += " (" + _valType + ")";
+ }
+ return str;
+ }
+ }
+
+ private static final class TimeFormat extends DateFormat
+ {
+ private static final long serialVersionUID = 0L;
+
+ private final DateFormat _parseDelegate;
+ private final DateFormat _fmtDelegate;
+ private final String _baseDate;
+
+ private TimeFormat(DateFormat parseDelegate, DateFormat fmtDelegate,
+ String baseDate)
+ {
+ _parseDelegate = parseDelegate;
+ _fmtDelegate = fmtDelegate;
+ _baseDate = baseDate;
+ }
+
+ @Override
+ public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
+ return _fmtDelegate.format(date, toAppendTo, fieldPosition);
+ }
+
+ @Override
+ public Date parse(String source, ParsePosition pos) {
+ // we parse as a full date/time in order to get the correct "base date"
+ // used by access
+ return _parseDelegate.parse(_baseDate + source, pos);
+ }
+
+ @Override
+ public Calendar getCalendar() {
+ return _fmtDelegate.getCalendar();
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return _fmtDelegate.getTimeZone();
+ }
+ }
+
+}
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..418ed74
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
@@ -0,0 +1,1961 @@
+/*
+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.DateFormat;
+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;
+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 java.util.regex.PatternSyntaxException;
+
+import com.healthmarketscience.jackcess.DatabaseBuilder;
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.Expression;
+import com.healthmarketscience.jackcess.expr.Function;
+import com.healthmarketscience.jackcess.expr.TemporalConfig;
+import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token;
+import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType;
+
+
+/**
+ *
+ * @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
+
+
+ public enum Type {
+ DEFAULT_VALUE, FIELD_VALIDATOR, RECORD_VALIDATOR;
+ }
+
+ public interface ParseContext {
+ public TemporalConfig getTemporalConfig();
+ public SimpleDateFormat createDateFormat(String formatStr);
+ public Function getExpressionFunction(String name);
+ }
+
+ public static final ParseContext DEFAULT_PARSE_CONTEXT = new ParseContext() {
+ public TemporalConfig getTemporalConfig() {
+ return TemporalConfig.US_TEMPORAL_CONFIG;
+ }
+ 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<String,WordType> WORD_TYPES =
+ new HashMap<String,WordType>();
+
+ 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(EvalContext ctx, Value param1) {
+ return BuiltinOperators.negate(ctx, param1);
+ }
+ },
+ NOT("Not", true) {
+ @Override public Value eval(EvalContext ctx, 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(EvalContext ctx, Value param1);
+ }
+
+ private enum BinaryOp implements OpType {
+ PLUS("+") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.add(ctx, param1, param2);
+ }
+ },
+ MINUS("-") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.subtract(ctx, param1, param2);
+ }
+ },
+ MULT("*") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.multiply(param1, param2);
+ }
+ },
+ DIV("/") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.divide(param1, param2);
+ }
+ },
+ INT_DIV("\\") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.intDivide(param1, param2);
+ }
+ },
+ EXP("^") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.exp(param1, param2);
+ }
+ },
+ CONCAT("&") {
+ @Override public Value eval(EvalContext ctx, Value param1, Value param2) {
+ return BuiltinOperators.concat(param1, param2);
+ }
+ },
+ MOD("Mod") {
+ @Override public Value eval(EvalContext ctx, 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(EvalContext ctx, 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<OpType, Integer> 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<Character> REGEX_SPEC_CHARS = new HashSet<Character>(
+ Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&'));
+ // this is a regular expression which will never match any string
+ private static final Pattern UNMATCHABLE_REGEX = Pattern.compile("(?!)");
+
+ private static final Expr THIS_COL_VALUE = new EThisValue();
+
+ 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<Token> 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<Token> tokens = trimSpaces(
+ ExpressionTokenizer.tokenize(exprType, exprStr, context));
+
+ if(tokens == null) {
+ // FIXME, NULL_EXPR?
+ return null;
+ }
+
+ Expr expr = parseExpression(new TokBuf(exprType, tokens, context), false);
+ return (expr.isConstant() ?
+ // for now, just cache at top-level for speed (could in theory cache
+ // intermediate values?)
+ new MemoizedExprWrapper(exprType, expr) :
+ new ExprWrapper(exprType, expr));
+ }
+
+ private static List<Token> trimSpaces(List<Token> 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(),
+ t.getDateFormat()));
+ 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<String> objNames = new LinkedList<String>();
+ 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<Expr> 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<Expr> 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<Expr> exprs = new ArrayList<Expr>(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();
+ if((t.getType() != TokenType.LITERAL) ||
+ (t.getValueType() != Value.Type.STRING)) {
+ throw new IllegalArgumentException("Missing Like pattern " + buf);
+ }
+ String patternStr = t.getValueStr();
+ specOpExpr = new ELikeOp(specOp, expr, 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<Expr> 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 extends Enum<T>> T getOpType(Token t, Class<T> 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<Token> _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<Token> tokens, ParseContext context) {
+ this(exprType, false, tokens, null, 0, context);
+ }
+
+ private TokBuf(List<Token> tokens, TokBuf parent, int parentOff) {
+ this(parent._exprType, parent._simpleExpr, tokens, parent, parentOff,
+ parent._context);
+ }
+
+ private TokBuf(Type exprType, boolean simpleExpr, List<Token> 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<Integer,List<Token>> getTopPos() {
+ int pos = _pos;
+ List<Token> 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<Integer,List<Token>> e = getTopPos();
+
+ // TODO actually format expression?
+ StringBuilder sb = new StringBuilder()
+ .append("[token ").append(e.getKey()).append("] (");
+
+ for(Iterator<Token> 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<OpType, Integer> buildPrecedenceMap(
+ OpType[]... opArrs) {
+ Map<OpType, Integer> prec = new HashMap<OpType, Integer>();
+
+ int level = 0;
+ for(OpType[] ops : opArrs) {
+ for(OpType op : ops) {
+ prec.put(op, level);
+ }
+ ++level;
+ }
+
+ return prec;
+ }
+
+ private static void exprListToString(
+ List<Expr> exprs, String sep, StringBuilder sb, boolean isDebug) {
+ Iterator<Expr> iter = exprs.iterator();
+ iter.next().toString(sb, isDebug);
+ while(iter.hasNext()) {
+ sb.append(sep);
+ iter.next().toString(sb, isDebug);
+ }
+ }
+
+ private static Value[] exprListToValues(
+ List<Expr> exprs, EvalContext 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<Expr> exprs, EvalContext 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 boolean areConstant(List<Expr> exprs) {
+ for(Expr expr : exprs) {
+ if(!expr.isConstant()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean areConstant(Expr... exprs) {
+ for(Expr expr : exprs) {
+ if(!expr.isConstant()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void literalStrToString(String str, StringBuilder sb) {
+ sb.append("\"")
+ .append(str.replace("\"", "\"\""))
+ .append("\"");
+ }
+
+ private static Pattern likePatternToRegex(String pattern) {
+
+ 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;
+ }
+ }
+
+ // access treats invalid expression like "unmatchable"
+ if(endPos == -1) {
+ return UNMATCHABLE_REGEX;
+ }
+
+ 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(']');
+ i += (endPos - startPos) + 1;
+
+ } else if(REGEX_SPEC_CHARS.contains(c)) {
+ // this char is special in regexes, so escape it
+ sb.append('\\').append(c);
+ } else {
+ sb.append(c);
+ }
+ }
+
+ try {
+ return Pattern.compile(sb.toString(),
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL |
+ Pattern.UNICODE_CASE);
+ } catch(PatternSyntaxException ignored) {
+ return UNMATCHABLE_REGEX;
+ }
+ }
+
+ private static Value toLiteralValue(Value.Type valType, Object value,
+ DateFormat sdf)
+ {
+ switch(valType) {
+ case STRING:
+ return new StringValue((String)value);
+ case DATE:
+ return new DateValue((Date)value, sdf);
+ case TIME:
+ return new TimeValue((Date)value, sdf);
+ case DATE_TIME:
+ return new DateTimeValue((Date)value, sdf);
+ case LONG:
+ return new LongValue((Long)value);
+ case DOUBLE:
+ return new DoubleValue((Double)value);
+ case BIG_DEC:
+ return new BigDecimalValue((BigDecimal)value);
+ default:
+ throw new RuntimeException("unexpected literal type " + valType);
+ }
+ }
+
+ 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 extends BaseDelayedValue
+ {
+ private final Expr _expr;
+ private final EvalContext _ctx;
+
+ private DelayedValue(Expr expr, EvalContext ctx) {
+ _expr = expr;
+ _ctx = ctx;
+ }
+
+ @Override
+ public Value eval() {
+ return _expr.eval(_ctx);
+ }
+ }
+
+
+ private static abstract class Expr
+ {
+ @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: <this>{<left>{A op1 B} op2 <right>{C}}
+ if(thisLeft instanceof RightAssocExpr) {
+
+ RightAssocExpr leftOp = (RightAssocExpr)thisLeft;
+
+ // target: <left>{A op1 <this>{B op2 <right>{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;
+ }
+
+ public abstract boolean isConstant();
+
+ public abstract Value eval(EvalContext 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
+ public boolean isConstant() {
+ return true;
+ }
+
+ @Override
+ public Value eval(EvalContext ctx) {
+ return _val;
+ }
+
+ @Override
+ protected void toExprString(StringBuilder sb, boolean isDebug) {
+ sb.append(_str);
+ }
+ }
+
+ private static final class EThisValue extends Expr
+ {
+ @Override
+ public boolean isConstant() {
+ return false;
+ }
+ @Override
+ public Value eval(EvalContext ctx) {
+ return ctx.getThisColumnValue();
+ }
+ @Override
+ protected void toExprString(StringBuilder sb, boolean isDebug) {
+ sb.append("<THIS_COL>");
+ }
+ }
+
+ private static final class ELiteralValue extends Expr
+ {
+ private final Value _val;
+
+ private ELiteralValue(Value.Type valType, Object value,
+ DateFormat sdf) {
+ _val = toLiteralValue(valType, value, sdf);
+ }
+
+ @Override
+ public boolean isConstant() {
+ return true;
+ }
+
+ @Override
+ public Value eval(EvalContext 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()) {
+ sb.append("#").append(_val.getAsString()).append("#");
+ } 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 boolean isConstant() {
+ return false;
+ }
+
+ @Override
+ public Value eval(EvalContext 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
+ public boolean isConstant() {
+ return _expr.isConstant();
+ }
+
+ @Override
+ public Value eval(EvalContext 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<Expr> _params;
+
+ private EFunc(Function func, List<Expr> params) {
+ _func = func;
+ _params = params;
+ }
+
+ @Override
+ public boolean isConstant() {
+ return _func.isPure() && areConstant(_params);
+ }
+
+ @Override
+ public Value eval(EvalContext ctx) {
+ return _func.eval(ctx, 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;
+ }
+
+ @Override
+ public boolean isConstant() {
+ return areConstant(_left, _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
+ public Value eval(EvalContext ctx) {
+ return ((BinaryOp)_op).eval(ctx, _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;
+ }
+
+ @Override
+ public boolean isConstant() {
+ return _expr.isConstant();
+ }
+
+ public OpType getOp() {
+ return _op;
+ }
+
+ public Expr getRight() {
+ return _expr;
+ }
+
+ public void setRight(Expr right) {
+ _expr = right;
+ }
+
+ @Override
+ public Value eval(EvalContext ctx) {
+ return ((UnaryOp)_op).eval(ctx, _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
+ public Value eval(EvalContext 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
+ public Value eval(final EvalContext 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;
+ }
+
+ @Override
+ public boolean isConstant() {
+ return _expr.isConstant();
+ }
+
+ 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
+ public Value eval(EvalContext 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
+ {
+ private final String _patternStr;
+ private Pattern _pattern;
+
+ private ELikeOp(SpecOp op, Expr expr, String patternStr) {
+ super(op, expr);
+ _patternStr = patternStr;
+ }
+
+ private Pattern getPattern()
+ {
+ if(_pattern == null) {
+ _pattern = likePatternToRegex(_patternStr);
+ }
+ return _pattern;
+ }
+
+ @Override
+ public Value eval(EvalContext ctx) {
+ return _op.eval(_expr.eval(ctx), getPattern(), 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(getPattern()).append(")");
+ }
+ }
+ }
+
+ private static class EInOp extends ESpecOp
+ {
+ private final List<Expr> _exprs;
+
+ private EInOp(SpecOp op, Expr expr, List<Expr> exprs) {
+ super(op, expr);
+ _exprs = exprs;
+ }
+
+ @Override
+ public boolean isConstant() {
+ return super.isConstant() && areConstant(_exprs);
+ }
+
+ @Override
+ public Value eval(EvalContext 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;
+ }
+
+ @Override
+ public boolean isConstant() {
+ return _expr.isConstant() && areConstant(_startRangeExpr, _endRangeExpr);
+ }
+
+ public Expr getRight() {
+ return _endRangeExpr;
+ }
+
+ public void setRight(Expr right) {
+ _endRangeExpr = right;
+ }
+
+ @Override
+ public Value eval(EvalContext 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);
+ }
+ }
+
+ /**
+ * Expression wrapper for an Expr which caches the result of evaluation.
+ */
+ private static class ExprWrapper implements Expression
+ {
+ private final Type _type;
+ private final Expr _expr;
+
+ private ExprWrapper(Type type, Expr expr) {
+ _type = type;
+ _expr = expr;
+ }
+
+ public Object eval(EvalContext ctx) {
+ switch(_type) {
+ case DEFAULT_VALUE:
+ return evalDefault(ctx);
+ case FIELD_VALIDATOR:
+ case RECORD_VALIDATOR:
+ return evalCondition(ctx);
+ default:
+ throw new RuntimeException("unexpected expression type " + _type);
+ }
+ }
+
+ public String toDebugString() {
+ return _expr.toDebugString();
+ }
+
+ public boolean isConstant() {
+ return _expr.isConstant();
+ }
+
+ @Override
+ public String toString() {
+ return _expr.toString();
+ }
+
+ private Object evalDefault(EvalContext ctx) {
+ Value val = _expr.eval(ctx);
+
+ if(val.isNull()) {
+ return null;
+ }
+
+ Value.Type resultType = ctx.getResultType();
+ if(resultType == null) {
+ // return as "native" type
+ return val.get();
+ }
+
+ // FIXME possibly do some type coercion. are there conversions here which don't work elsewhere? (string -> date, string -> number)?
+ switch(resultType) {
+ case STRING:
+ return val.getAsString();
+ case DATE:
+ case TIME:
+ case DATE_TIME:
+ return val.getAsDateTime(ctx);
+ case LONG:
+ return val.getAsLong();
+ case DOUBLE:
+ return val.getAsDouble();
+ case BIG_DEC:
+ return val.getAsBigDecimal();
+ default:
+ throw new IllegalStateException("unexpected result type " +
+ ctx.getResultType());
+ }
+ }
+
+ private Boolean evalCondition(EvalContext ctx) {
+ Value val = _expr.eval(ctx);
+
+ if(val.isNull()) {
+ return null;
+ }
+
+ // FIXME - field/row validator -> if top-level operator is not "boolean", then do value comparison withe coercion
+ // FIXME, is this only true for non-numeric...?
+ // 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 val.getAsBoolean();
+ }
+ }
+
+ /**
+ * Expression wrapper for a <i>pure</i> Expr which caches the result of
+ * evaluation.
+ */
+ private static final class MemoizedExprWrapper extends ExprWrapper
+ {
+ private Object _val;
+
+ private MemoizedExprWrapper(Type type, Expr expr) {
+ super(type, expr);
+ }
+
+ @Override
+ public Object eval(EvalContext ctx) {
+ if(_val == null) {
+ _val = super.eval(ctx);
+ }
+ return _val;
+ }
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java
new file mode 100644
index 0000000..16ac83d
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java
@@ -0,0 +1,61 @@
+/*
+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;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class LongValue extends BaseNumericValue
+{
+ private final Long _val;
+
+ public LongValue(Long val)
+ {
+ _val = val;
+ }
+
+ public Type getType() {
+ return Type.LONG;
+ }
+
+ public Object get() {
+ return _val;
+ }
+
+ @Override
+ protected Number getNumber() {
+ return _val;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return (_val.longValue() != 0L);
+ }
+
+ @Override
+ public Long getAsLong() {
+ return _val;
+ }
+
+ @Override
+ public BigDecimal getAsBigDecimal() {
+ return BigDecimal.valueOf(_val);
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java
new file mode 100644
index 0000000..6133139
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java
@@ -0,0 +1,87 @@
+/*
+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;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class StringValue extends BaseValue
+{
+ private static final Object NOT_A_NUMBER = new Object();
+
+ private final String _val;
+ private Object _num;
+
+ public StringValue(String val)
+ {
+ _val = val;
+ }
+
+ public Type getType() {
+ return Type.STRING;
+ }
+
+ public Object get() {
+ return _val;
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ // ms access seems to treat strings as "true"
+ return true;
+ }
+
+ @Override
+ public String getAsString() {
+ return _val;
+ }
+
+ @Override
+ public Long getAsLong() {
+ return getNumber().longValue();
+ }
+
+ @Override
+ public Double getAsDouble() {
+ return getNumber().doubleValue();
+ }
+
+ @Override
+ public BigDecimal getAsBigDecimal() {
+ return getNumber();
+ }
+
+ protected BigDecimal getNumber() {
+ if(_num instanceof BigDecimal) {
+ return (BigDecimal)_num;
+ }
+ if(_num == null) {
+ // see if it is parseable as a number
+ try {
+ _num = new BigDecimal(_val);
+ return (BigDecimal)_num;
+ } catch(NumberFormatException nfe) {
+ _num = NOT_A_NUMBER;
+ throw nfe;
+ }
+ }
+ throw new NumberFormatException();
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java
new file mode 100644
index 0000000..cedb461
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java
@@ -0,0 +1,37 @@
+/*
+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.DateFormat;
+import java.util.Date;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class TimeValue extends BaseDateValue
+{
+
+ public TimeValue(Date val, DateFormat fmt)
+ {
+ super(val, fmt);
+ }
+
+ public Type getType() {
+ return Type.TIME;
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
index a2bf31a..a564834 100644
--- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
+++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
@@ -23,6 +23,7 @@ import java.util.Arrays;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.Table;
import com.healthmarketscience.jackcess.impl.ColumnImpl;
+import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import org.apache.commons.lang.ObjectUtils;
/**
@@ -54,8 +55,9 @@ public class SimpleColumnMatcher implements ColumnMatcher {
// values and try again
DataType dataType = table.getColumn(columnName).getType();
try {
- Object internalV1 = ColumnImpl.toInternalValue(dataType, value1);
- Object internalV2 = ColumnImpl.toInternalValue(dataType, value2);
+ DatabaseImpl db = (DatabaseImpl)table.getDatabase();
+ Object internalV1 = ColumnImpl.toInternalValue(dataType, value1, db);
+ Object internalV2 = ColumnImpl.toInternalValue(dataType, value2, db);
return equals(internalV1, internalV2);
} catch(IOException e) {
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
new file mode 100644
index 0000000..4efcedb
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
@@ -0,0 +1,377 @@
+/*
+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.Date;
+
+import com.healthmarketscience.jackcess.DatabaseBuilder;
+import com.healthmarketscience.jackcess.TestUtil;
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.Expression;
+import com.healthmarketscience.jackcess.expr.Function;
+import com.healthmarketscience.jackcess.expr.TemporalConfig;
+import com.healthmarketscience.jackcess.expr.Value;
+import junit.framework.TestCase;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class ExpressionatorTest extends TestCase
+{
+ private static final double[] DBLS = {
+ -10.3d,-9.0d,-8.234d,-7.11111d,-6.99999d,-5.5d,-4.0d,-3.4159265d,-2.84d,
+ -1.0000002d,-1.0d,-0.0002013d,0.0d, 0.9234d,1.0d,1.954d,2.200032d,3.001d,
+ 4.9321d,5.0d,6.66666d,7.396d,8.1d,9.20456200d,10.325d};
+
+ public ExpressionatorTest(String name) {
+ super(name);
+ }
+
+
+ public void testParseSimpleExprs() throws Exception
+ {
+ validateExpr("\"A\"", "<ELiteralValue>{\"A\"}");
+
+ validateExpr("13", "<ELiteralValue>{13}");
+
+ validateExpr("-42", "<ELiteralValue>{-42}");
+
+ doTestSimpleBinOp("EBinaryOp", "+", "-", "*", "/", "\\", "^", "&", "Mod");
+ doTestSimpleBinOp("ECompOp", "<", "<=", ">", ">=", "=", "<>");
+ doTestSimpleBinOp("ELogicalOp", "And", "Or", "Eqv", "Xor", "Imp");
+
+ for(String constStr : new String[]{"True", "False", "Null"}) {
+ validateExpr(constStr, "<EConstValue>{" + constStr + "}");
+ }
+
+ validateExpr("[Field1]", "<EObjValue>{[Field1]}");
+
+ validateExpr("[Table2].[Field3]", "<EObjValue>{[Table2].[Field3]}");
+
+ validateExpr("Not \"A\"", "<EUnaryOp>{Not <ELiteralValue>{\"A\"}}");
+
+ validateExpr("-[Field1]", "<EUnaryOp>{- <EObjValue>{[Field1]}}");
+
+ validateExpr("\"A\" Is Null", "<ENullOp>{<ELiteralValue>{\"A\"} Is Null}");
+
+ validateExpr("\"A\" In (1,2,3)", "<EInOp>{<ELiteralValue>{\"A\"} In (<ELiteralValue>{1},<ELiteralValue>{2},<ELiteralValue>{3})}");
+
+ validateExpr("\"A\" Not Between 3 And 7", "<EBetweenOp>{<ELiteralValue>{\"A\"} Not Between <ELiteralValue>{3} And <ELiteralValue>{7}}");
+
+ validateExpr("(\"A\" Or \"B\")", "<EParen>{(<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}})}");
+
+ validateExpr("IIf(\"A\",42,False)", "<EFunc>{IIf(<ELiteralValue>{\"A\"},<ELiteralValue>{42},<EConstValue>{False})}");
+
+ validateExpr("\"A\" Like \"a*b\"", "<ELikeOp>{<ELiteralValue>{\"A\"} Like \"a*b\"(a.*b)}");
+ }
+
+ private static void doTestSimpleBinOp(String opName, String... ops) throws Exception
+ {
+ for(String op : ops) {
+ validateExpr("\"A\" " + op + " \"B\"",
+ "<" + opName + ">{<ELiteralValue>{\"A\"} " + op +
+ " <ELiteralValue>{\"B\"}}");
+ }
+ }
+
+ public void testOrderOfOperations() throws Exception
+ {
+ validateExpr("\"A\" Eqv \"B\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELiteralValue>{\"B\"}}");
+
+ validateExpr("\"A\" Eqv \"B\" Xor \"C\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELiteralValue>{\"C\"}}}");
+
+ validateExpr("\"A\" Eqv \"B\" Xor \"C\" Or \"D\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELiteralValue>{\"D\"}}}}");
+
+ validateExpr("\"A\" Eqv \"B\" Xor \"C\" Or \"D\" And \"E\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELogicalOp>{<ELiteralValue>{\"D\"} And <ELiteralValue>{\"E\"}}}}}");
+
+ validateExpr("\"A\" Or \"B\" Or \"C\"",
+ "<ELogicalOp>{<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}} Or <ELiteralValue>{\"C\"}}");
+
+ validateExpr("\"A\" & \"B\" Is Null",
+ "<ENullOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}} Is Null}");
+
+ validateExpr("\"A\" Or \"B\" Is Null",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ENullOp>{<ELiteralValue>{\"B\"} Is Null}}");
+
+ validateExpr("Not \"A\" & \"B\"",
+ "<EUnaryOp>{Not <EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}}}");
+
+ validateExpr("Not \"A\" Or \"B\"",
+ "<ELogicalOp>{<EUnaryOp>{Not <ELiteralValue>{\"A\"}} Or <ELiteralValue>{\"B\"}}");
+
+ validateExpr("\"A\" + \"B\" Not Between 37 - 15 And 52 / 4",
+ "<EBetweenOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} + <ELiteralValue>{\"B\"}} Not Between <EBinaryOp>{<ELiteralValue>{37} - <ELiteralValue>{15}} And <EBinaryOp>{<ELiteralValue>{52} / <ELiteralValue>{4}}}");
+
+ validateExpr("\"A\" + (\"B\" Not Between 37 - 15 And 52) / 4",
+ "<EBinaryOp>{<ELiteralValue>{\"A\"} + <EBinaryOp>{<EParen>{(<EBetweenOp>{<ELiteralValue>{\"B\"} Not Between <EBinaryOp>{<ELiteralValue>{37} - <ELiteralValue>{15}} And <ELiteralValue>{52}})} / <ELiteralValue>{4}}}");
+
+
+ }
+
+ public void testSimpleMathExpressions() throws Exception
+ {
+ for(long i = -10L; i <= 10L; ++i) {
+ assertEquals(-i, eval("=-(" + i + ")"));
+ }
+
+ for(double i : DBLS) {
+ assertEquals(-i, eval("=-(" + i + ")"));
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ assertEquals((i + j), eval("=" + i + " + " + j));
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ assertEquals((i + j), eval("=" + i + " + " + j));
+ }
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ assertEquals((i - j), eval("=" + i + " - " + j));
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ assertEquals((i - j), eval("=" + i + " - " + j));
+ }
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ assertEquals((i * j), eval("=" + i + " * " + j));
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ assertEquals((i * j), eval("=" + i + " * " + j));
+ }
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ if(j == 0L) {
+ evalFail("=" + i + " \\ " + j, ArithmeticException.class);
+ } else {
+ assertEquals((i / j), eval("=" + i + " \\ " + j));
+ }
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ if((long)j == 0L) {
+ evalFail("=" + i + " \\ " + j, ArithmeticException.class);
+ } else {
+ assertEquals(((long)i / (long)j), eval("=" + i + " \\ " + j));
+ }
+ }
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ if(j == 0L) {
+ evalFail("=" + i + " Mod " + j, ArithmeticException.class);
+ } else {
+ assertEquals((i % j), eval("=" + i + " Mod " + j));
+ }
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ if((long)j == 0L) {
+ evalFail("=" + i + " Mod " + j, ArithmeticException.class);
+ } else {
+ assertEquals(((long)i % (long)j), eval("=" + i + " Mod " + j));
+ }
+ }
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ if(j == 0L) {
+ evalFail("=" + i + " / " + j, ArithmeticException.class);
+ } else {
+ double result = (double)i / (double)j;
+ if((long)result == result) {
+ assertEquals((long)result, eval("=" + i + " / " + j));
+ } else {
+ assertEquals(result, eval("=" + i + " / " + j));
+ }
+ }
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ if(j == 0.0d) {
+ evalFail("=" + i + " / " + j, ArithmeticException.class);
+ } else {
+ assertEquals((i / j), eval("=" + i + " / " + j));
+ }
+ }
+ }
+
+ for(long i = -10L; i <= 10L; ++i) {
+ for(long j = -10L; j <= 10L; ++j) {
+ double result = Math.pow(i, j);
+ if((long)result == result) {
+ assertEquals((long)result, eval("=" + i + " ^ " + j));
+ } else {
+ assertEquals(result, eval("=" + i + " ^ " + j));
+ }
+ }
+ }
+
+
+ }
+
+ public void testTypeCoercion() throws Exception
+ {
+ assertEquals("foobar", eval("=\"foo\" + \"bar\""));
+
+ assertEquals("12foo", eval("=12 + \"foo\""));
+ assertEquals("foo12", eval("=\"foo\" + 12"));
+
+ assertEquals(37L, eval("=\"25\" + 12"));
+ assertEquals(37L, eval("=12 + \"25\""));
+
+ evalFail(("=12 - \"foo\""), RuntimeException.class);
+ evalFail(("=\"foo\" - 12"), RuntimeException.class);
+
+ assertEquals("foo1225", eval("=\"foo\" + 12 + 25"));
+ assertEquals("37foo", eval("=12 + 25 + \"foo\""));
+ assertEquals("foo37", eval("=\"foo\" + (12 + 25)"));
+ assertEquals("25foo12", eval("=\"25foo\" + 12"));
+
+ assertEquals(new Date(1485579600000L), eval("=#1/1/2017# + 27"));
+ assertEquals(128208L, eval("=#1/1/2017# * 3"));
+ }
+
+ public void testLikeExpression() throws Exception
+ {
+ validateExpr("Like \"[abc]*\"", "<ELikeOp>{<EThisValue>{<THIS_COL>} Like \"[abc]*\"([abc].*)}",
+ "<THIS_COL> Like \"[abc]*\"");
+ assertTrue(evalCondition("Like \"[abc]*\"", "afcd"));
+ assertFalse(evalCondition("Like \"[abc]*\"", "fcd"));
+
+ validateExpr("Like \"[abc*\"", "<ELikeOp>{<EThisValue>{<THIS_COL>} Like \"[abc*\"((?!))}",
+ "<THIS_COL> Like \"[abc*\"");
+ assertFalse(evalCondition("Like \"[abc*\"", "afcd"));
+ assertFalse(evalCondition("Like \"[abc*\"", "fcd"));
+ assertFalse(evalCondition("Like \"[abc*\"", ""));
+ }
+
+ private static void validateExpr(String exprStr, String debugStr) {
+ validateExpr(exprStr, debugStr, exprStr);
+ }
+
+ private static void validateExpr(String exprStr, String debugStr,
+ String cleanStr) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.FIELD_VALIDATOR, exprStr, null);
+ assertEquals(debugStr, expr.toDebugString());
+ assertEquals(cleanStr, expr.toString());
+ }
+
+ private static Object eval(String exprStr) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.DEFAULT_VALUE, exprStr, new TestParseContext());
+ return expr.eval(new TestEvalContext(null));
+ }
+
+ private static void evalFail(String exprStr, Class<? extends Exception> failure) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.DEFAULT_VALUE, exprStr, new TestParseContext());
+ try {
+ expr.eval(new TestEvalContext(null));
+ fail(failure + " should have been thrown");
+ } catch(Exception e) {
+ assertTrue(failure.isInstance(e));
+ }
+ }
+
+ private static Boolean evalCondition(String exprStr, String thisVal) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.FIELD_VALIDATOR, exprStr, new TestParseContext());
+ return (Boolean)expr.eval(new TestEvalContext(BuiltinOperators.toValue(thisVal)));
+ }
+
+ private static final class TestParseContext implements Expressionator.ParseContext
+ {
+ public TemporalConfig getTemporalConfig() {
+ return TemporalConfig.US_TEMPORAL_CONFIG;
+ }
+ public SimpleDateFormat createDateFormat(String formatStr) {
+ SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
+ sdf.setTimeZone(TestUtil.TEST_TZ);
+ return sdf;
+ }
+
+ public Function getExpressionFunction(String name) {
+ return null;
+ }
+ }
+
+ private static final class TestEvalContext implements EvalContext
+ {
+ private final Value _thisVal;
+
+ private TestEvalContext(Value thisVal) {
+ _thisVal = thisVal;
+ }
+
+ public Value.Type getResultType() {
+ return null;
+ }
+
+ public TemporalConfig getTemporalConfig() {
+ return TemporalConfig.US_TEMPORAL_CONFIG;
+ }
+
+ public SimpleDateFormat createDateFormat(String formatStr) {
+ SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
+ sdf.setTimeZone(TestUtil.TEST_TZ);
+ return sdf;
+ }
+
+ public Value getThisColumnValue() {
+ if(_thisVal == null) {
+ throw new UnsupportedOperationException();
+ }
+ return _thisVal;
+ }
+
+ public Value getRowValue(String collectionName, String objName,
+ String colName) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}