diff options
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(); + } + } +} |