diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2018-06-26 03:59:12 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2018-06-26 03:59:12 +0000 |
commit | 77b2229aa3f6e96c30731c8f09b01f48a281f8a8 (patch) | |
tree | a4d60afccd08e6d9ee4acfa02646606815bc4daf | |
parent | c01cc6e96c9bae9db4acc58f86ef56223c45c040 (diff) | |
parent | 175a918ed7c42a531dd8a5f08e2f9e645a02320a (diff) | |
download | jackcess-77b2229aa3f6e96c30731c8f09b01f48a281f8a8.tar.gz jackcess-77b2229aa3f6e96c30731c8f09b01f48a281f8a8.zip |
merge branch exprs changes through r1171
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1172 f203690c-595d-4dc9-a70b-905162fa7fd2
60 files changed, 10362 insertions, 482 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index 5644d09..d853fe8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Set; import java.util.TimeZone; +import com.healthmarketscience.jackcess.expr.EvalConfig; import com.healthmarketscience.jackcess.query.Query; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.util.ColumnValidatorFactory; @@ -68,7 +69,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * the default sort order for table columns. * @usage _intermediate_field_ */ - public static final Table.ColumnOrder DEFAULT_COLUMN_ORDER = + public static final Table.ColumnOrder DEFAULT_COLUMN_ORDER = Table.ColumnOrder.DATA; /** system property which can be used to set the default TimeZone used for @@ -91,7 +92,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * if unspecified. * @usage _general_field_ */ - public static final String RESOURCE_PATH_PROPERTY = + public static final String RESOURCE_PATH_PROPERTY = "com.healthmarketscience.jackcess.resourcePath"; /** (boolean) system property which can be used to indicate that the current @@ -99,7 +100,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * {@code FileChannel.transferFrom}) * @usage _intermediate_field_ */ - public static final String BROKEN_NIO_PROPERTY = + public static final String BROKEN_NIO_PROPERTY = "com.healthmarketscience.jackcess.brokenNio"; /** system property which can be used to set the default sort order for @@ -107,23 +108,30 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * values. * @usage _intermediate_field_ */ - public static final String COLUMN_ORDER_PROPERTY = + public static final String COLUMN_ORDER_PROPERTY = "com.healthmarketscience.jackcess.columnOrder"; /** system property which can be used to set the default enforcement of * foreign-key relationships. Defaults to {@code true}. * @usage _general_field_ */ - public static final String FK_ENFORCE_PROPERTY = + public static final String FK_ENFORCE_PROPERTY = "com.healthmarketscience.jackcess.enforceForeignKeys"; /** system property which can be used to set the default allow auto number * insert policy. Defaults to {@code false}. * @usage _general_field_ */ - public static final String ALLOW_AUTONUM_INSERT_PROPERTY = + public static final String ALLOW_AUTONUM_INSERT_PROPERTY = "com.healthmarketscience.jackcess.allowAutoNumberInsert"; + /** system property which can be used to enable expression evaluation + * (currently experimental). Defaults to {@code false}. + * @usage _general_field_ + */ + public static final String ENABLE_EXPRESSION_EVALUATION_PROPERTY = + "com.healthmarketscience.jackcess.enableExpressionEvaluation"; + /** * Enum which indicates which version of Access created the database. * @usage _general_class_ @@ -160,7 +168,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public String getFileExtension() { return _ext; } @Override - public String toString() { + public String toString() { return name() + " [" + DatabaseImpl.getFileFormatDetails(this).getFormat() + "]"; } } @@ -201,7 +209,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * flexible iteration of Tables. */ public TableIterableBuilder newIterable(); - + /** * @param name User table name (case-insensitive) * @return The Table, or null if it doesn't exist (or is a system table) @@ -264,7 +272,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * occassional time when access to a system table is necessary. Messing * with system tables can strip the paint off your house and give your whole * family a permanent, orange afro. You have been warned. - * + * * @param tableName Table name, may be a system table * @return The table, or {@code null} if it doesn't exist * @usage _intermediate_method_ @@ -360,14 +368,14 @@ public interface Database extends Iterable<Table>, Closeable, Flushable */ public Map<String,Database> getLinkedDatabases(); - + /** * Returns {@code true} if this Database links to the given Table, {@code * false} otherwise. * @usage _general_method_ */ public boolean isLinkedTable(Table table) throws IOException; - + /** * Gets currently configured TimeZone (always non-{@code null}). * @usage _intermediate_method_ @@ -430,7 +438,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * {@link #ALLOW_AUTONUM_INSERT_PROPERTY} system property). Note that * <i>enabling this feature should be done with care</i> to reduce the * chances of screwing up the database. - * + * * @usage _intermediate_method_ */ public boolean isAllowAutoNumberInsert(); @@ -444,6 +452,20 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert); /** + * Gets the current expression evaluation policy. Expression evaluation is + * currently an experimental feature, and is therefore disabled by default. + */ + public boolean isEvaluateExpressions(); + + /** + * Sets the current expression evaluation policy. Expression evaluation is + * currently an experimental feature, and is therefore disabled by default. + * If {@code null}, resets to the default value. + * @usage _intermediate_method_ + */ + public void setEvaluateExpressions(Boolean evaluateExpressions); + + /** * Gets currently configured ColumnValidatorFactory (always non-{@code null}). * @usage _intermediate_method_ */ @@ -457,7 +479,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * @usage _intermediate_method_ */ public void setColumnValidatorFactory(ColumnValidatorFactory newFactory); - + /** * Returns the FileFormat of this database (which may involve inspecting the * database itself). @@ -466,4 +488,8 @@ public interface Database extends Iterable<Table>, Closeable, Flushable */ public FileFormat getFileFormat() throws IOException; + /** + * Returns the EvalConfig for configuring expression evaluation. + */ + public EvalConfig getEvalConfig(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java b/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java new file mode 100644 index 0000000..adffc0f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java @@ -0,0 +1,32 @@ +/* +Copyright (c) 2018 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; + +/** + * JackcessException which indicates that an invalid column value was provided + * in a database update. + * + * @author James Ahlborn + */ +public class InvalidValueException extends JackcessException +{ + private static final long serialVersionUID = 20180428L; + + public InvalidValueException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/JackcessException.java b/src/main/java/com/healthmarketscience/jackcess/JackcessException.java index eac136b..500b87f 100644 --- a/src/main/java/com/healthmarketscience/jackcess/JackcessException.java +++ b/src/main/java/com/healthmarketscience/jackcess/JackcessException.java @@ -23,9 +23,9 @@ import java.io.IOException; * * @author James Ahlborn */ -public class JackcessException extends IOException +public class JackcessException extends IOException { - private static final long serialVersionUID = 20131123L; + private static final long serialVersionUID = 20131123L; public JackcessException(String message) { super(message); diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/EvalConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/EvalConfig.java new file mode 100644 index 0000000..07ac492 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/EvalConfig.java @@ -0,0 +1,38 @@ +/* +Copyright (c) 2018 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 javax.script.Bindings; + +/** + * + * @author James Ahlborn + */ +public interface EvalConfig +{ + public TemporalConfig getTemporalConfig(); + + public void setTemporalConfig(TemporalConfig temporal); + + public FunctionLookup getFunctionLookup(); + + public void setFunctionLookup(FunctionLookup lookup); + + public Bindings getBindings(); + + public void setBindings(Bindings bindings); +} 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..f1dbab3 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java @@ -0,0 +1,45 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.expr; + +import java.text.SimpleDateFormat; +import javax.script.Bindings; + +/** + * + * @author James Ahlborn + */ +public interface EvalContext +{ + public TemporalConfig getTemporalConfig(); + + public SimpleDateFormat createDateFormat(String formatStr); + + public float getRandom(Integer seed); + + public Value.Type getResultType(); + + public Value getThisColumnValue(); + + public Value getIdentifierValue(Identifier identifier); + + public Bindings getBindings(); + + public Object get(String key); + + public void put(String key, Object value); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/EvalException.java b/src/main/java/com/healthmarketscience/jackcess/expr/EvalException.java new file mode 100644 index 0000000..b0f8fe7 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/EvalException.java @@ -0,0 +1,40 @@ +/* +Copyright (c) 2018 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; + + +/** + * Base class for exceptions thrown during expression evaluation. + * + * @author James Ahlborn + */ +public class EvalException extends IllegalStateException +{ + private static final long serialVersionUID = 20180330L; + + public EvalException(String message) { + super(message); + } + + public EvalException(Throwable cause) { + super(cause); + } + + public EvalException(String message, Throwable cause) { + super(message, cause); + } +} 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..09fd03b --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Expression.java @@ -0,0 +1,34 @@ +/* +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.util.Collection; + +/** + * + * @author James Ahlborn + */ +public interface Expression +{ + public Object eval(EvalContext ctx); + + public String toDebugString(); + + public boolean isConstant(); + + public void collectIdentifiers(Collection<Identifier> identifiers); +} 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/FunctionLookup.java b/src/main/java/com/healthmarketscience/jackcess/expr/FunctionLookup.java new file mode 100644 index 0000000..8314c41 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/FunctionLookup.java @@ -0,0 +1,26 @@ +/* +Copyright (c) 2018 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 FunctionLookup +{ + public Function getFunction(String name); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java b/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java new file mode 100644 index 0000000..cb402e7 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java @@ -0,0 +1,84 @@ +/* +Copyright (c) 2018 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 org.apache.commons.lang.ObjectUtils; + +/** + * + * @author James Ahlborn + */ +public class Identifier +{ + private final String _collectionName; + private final String _objectName; + private final String _propertyName; + + public Identifier(String collectionName, String objectName, String propertyName) + { + _collectionName = collectionName; + _objectName = objectName; + _propertyName = propertyName; + } + + public String getCollectionName() + { + return _collectionName; + } + + public String getObjectName() + { + return _objectName; + } + + public String getPropertyName() + { + return _propertyName; + } + + @Override + public int hashCode() { + return _objectName.hashCode(); + } + + @Override + public boolean equals(Object o) { + if(!(o instanceof Identifier)) { + return false; + } + + Identifier oi = (Identifier)o; + + return (ObjectUtils.equals(_objectName, oi._objectName) && + ObjectUtils.equals(_collectionName, oi._collectionName) && + ObjectUtils.equals(_propertyName, oi._propertyName)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if(_collectionName != null) { + sb.append("[").append(_collectionName).append("]."); + } + sb.append("[").append(_objectName).append("]"); + if(_propertyName != null) { + sb.append(".[").append(_propertyName).append("]"); + } + return sb.toString(); + } + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/ParseException.java b/src/main/java/com/healthmarketscience/jackcess/expr/ParseException.java new file mode 100644 index 0000000..c4a6864 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/ParseException.java @@ -0,0 +1,39 @@ +/* +Copyright (c) 2018 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; + +/** + * Exception thrown when expression parsing fails. + * + * @author James Ahlborn + */ +public class ParseException extends EvalException +{ + private static final long serialVersionUID = 20180330L; + + public ParseException(String message) { + super(message); + } + + public ParseException(Throwable cause) { + super(cause); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} 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..39008f2 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java @@ -0,0 +1,81 @@ +/* +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() { + 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 Integer getAsLongInt(); + + public Double getAsDouble(); + + public BigDecimal getAsBigDecimal(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java new file mode 100644 index 0000000..9d72413 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java @@ -0,0 +1,211 @@ +/* +Copyright (c) 2018 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; + +import java.io.IOException; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.EnumMap; +import java.util.Map; +import javax.script.Bindings; + +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.JackcessException; +import com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Expression; +import com.healthmarketscience.jackcess.expr.Identifier; +import com.healthmarketscience.jackcess.expr.TemporalConfig; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.expr.BuiltinOperators; +import com.healthmarketscience.jackcess.impl.expr.Expressionator; + +/** + * + * @author James Ahlborn + */ +public abstract class BaseEvalContext implements EvalContext +{ + /** map of all non-string data types */ + private static final Map<DataType,Value.Type> TYPE_MAP = + new EnumMap<DataType,Value.Type>(DataType.class); + + static { + TYPE_MAP.put(DataType.BOOLEAN,Value.Type.LONG); + TYPE_MAP.put(DataType.BYTE,Value.Type.LONG); + TYPE_MAP.put(DataType.INT,Value.Type.LONG); + TYPE_MAP.put(DataType.LONG,Value.Type.LONG); + TYPE_MAP.put(DataType.MONEY,Value.Type.DOUBLE); + TYPE_MAP.put(DataType.FLOAT,Value.Type.DOUBLE); + TYPE_MAP.put(DataType.DOUBLE,Value.Type.DOUBLE); + TYPE_MAP.put(DataType.SHORT_DATE_TIME,Value.Type.DATE_TIME); + TYPE_MAP.put(DataType.NUMERIC,Value.Type.BIG_DEC); + TYPE_MAP.put(DataType.BIG_INT,Value.Type.BIG_DEC); + } + + private final DBEvalContext _dbCtx; + private Expression _expr; + + protected BaseEvalContext(DBEvalContext dbCtx) { + _dbCtx = dbCtx; + } + + void setExpr(Expressionator.Type exprType, String exprStr) { + _expr = new RawExpr(exprType, exprStr); + } + + protected DatabaseImpl getDatabase() { + return _dbCtx.getDatabase(); + } + + public TemporalConfig getTemporalConfig() { + return _dbCtx.getTemporalConfig(); + } + + public SimpleDateFormat createDateFormat(String formatStr) { + return _dbCtx.createDateFormat(formatStr); + } + + public float getRandom(Integer seed) { + return _dbCtx.getRandom(seed); + } + + public Value.Type getResultType() { + return null; + } + + public Value getThisColumnValue() { + throw new UnsupportedOperationException(); + } + + public Value getIdentifierValue(Identifier identifier) { + throw new UnsupportedOperationException(); + } + + public Bindings getBindings() { + return _dbCtx.getBindings(); + } + + public Object get(String key) { + return _dbCtx.getBindings().get(key); + } + + public void put(String key, Object value) { + _dbCtx.getBindings().put(key, value); + } + + public Object eval() throws IOException { + try { + return _expr.eval(this); + } catch(Exception e) { + String msg = withErrorContext(e.getMessage()); + throw new JackcessException(msg, e); + } + } + + public void collectIdentifiers(Collection<Identifier> identifiers) { + _expr.collectIdentifiers(identifiers); + } + + @Override + public String toString() { + return _expr.toString(); + } + + protected Value toValue(Object val, DataType dType) { + try { + val = ColumnImpl.toInternalValue(dType, val, getDatabase()); + if(val == null) { + return BuiltinOperators.NULL_VAL; + } + + Value.Type vType = toValueType(dType); + switch(vType) { + case STRING: + return BuiltinOperators.toValue(val.toString()); + case DATE: + case TIME: + case DATE_TIME: + return BuiltinOperators.toValue(this, vType, (Date)val); + case LONG: + Integer i = ((val instanceof Integer) ? (Integer)val : + ((Number)val).intValue()); + return BuiltinOperators.toValue(i); + case DOUBLE: + Double d = ((val instanceof Double) ? (Double)val : + ((Number)val).doubleValue()); + return BuiltinOperators.toValue(d); + case BIG_DEC: + BigDecimal bd = ColumnImpl.toBigDecimal(val, getDatabase()); + return BuiltinOperators.toValue(bd); + default: + throw new RuntimeException("Unexpected type " + vType); + } + } catch(IOException e) { + throw new EvalException("Failed converting value to type " + dType, e); + } + } + + protected static Value.Type toValueType(DataType dType) { + Value.Type type = TYPE_MAP.get(dType); + return ((type == null) ? Value.Type.STRING : type); + } + + protected abstract String withErrorContext(String msg); + + private class RawExpr implements Expression + { + private final Expressionator.Type _exprType; + private final String _exprStr; + + private RawExpr(Expressionator.Type exprType, String exprStr) { + _exprType = exprType; + _exprStr = exprStr; + } + + private Expression getExpr() { + // when the expression is parsed we replace the raw version + Expression expr = Expressionator.parse( + _exprType, _exprStr, getResultType(), _dbCtx); + _expr = expr; + return expr; + } + + public Object eval(EvalContext ctx) { + return getExpr().eval(ctx); + } + + public String toDebugString() { + return "<raw>{" + _exprStr + "}"; + } + + public boolean isConstant() { + return getExpr().isConstant(); + } + + public void collectIdentifiers(Collection<Identifier> identifiers) { + getExpr().collectIdentifiers(identifiers); + } + + @Override + public String toString() { + return _exprStr; + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CalcColEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/CalcColEvalContext.java new file mode 100644 index 0000000..282e5ae --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CalcColEvalContext.java @@ -0,0 +1,65 @@ +/* +Copyright (c) 2018 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; + +import java.io.IOException; + +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.expr.Expressionator; + +/** + * + * @author James Ahlborn + */ +public class CalcColEvalContext extends RowEvalContext +{ + private final ColumnImpl _col; + + public CalcColEvalContext(ColumnImpl col) { + super(col.getDatabase()); + _col = col; + } + + CalcColEvalContext setExpr(String exprStr) { + setExpr(Expressionator.Type.EXPRESSION, exprStr); + return this; + } + + @Override + protected TableImpl getTable() { + return _col.getTable(); + } + + @Override + public Value.Type getResultType() { + return toValueType(_col.getType()); + } + + public Object eval(Object[] row) throws IOException { + try { + setRow(row); + return eval(); + } finally { + reset(); + } + } + + @Override + protected String withErrorContext(String msg) { + return _col.withErrorContext(msg); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java index e77963f..fb76ad7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java @@ -21,6 +21,8 @@ import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import com.healthmarketscience.jackcess.InvalidValueException; + /** * Utility code for dealing with calculated columns. @@ -30,7 +32,7 @@ import java.nio.ByteOrder; * * @author James Ahlborn */ -class CalculatedColumnUtil +class CalculatedColumnUtil { // offset to the int which specifies the length of the actual data private static final int CALC_DATA_LEN_OFFSET = 16; @@ -51,12 +53,12 @@ class CalculatedColumnUtil /** * Creates the appropriate ColumnImpl class for a calculated column and * reads a column definition in from a buffer - * + * * @param args column construction info * @usage _advanced_method_ */ static ColumnImpl create(ColumnImpl.InitArgs args) throws IOException - { + { switch(args.type) { case BOOLEAN: return new CalcBooleanColImpl(args); @@ -71,7 +73,7 @@ class CalculatedColumnUtil if(args.type.getHasScalePrecision()) { return new CalcNumericColImpl(args); } - + return new CalcColImpl(args); } @@ -82,7 +84,7 @@ class CalculatedColumnUtil if(data.length < CALC_DATA_OFFSET) { return data; } - + ByteBuffer buffer = PageChannel.wrap(data); buffer.position(CALC_DATA_LEN_OFFSET); int dataLen = buffer.getInt(); @@ -109,7 +111,7 @@ class CalculatedColumnUtil */ private static byte[] wrapCalculatedValue(byte[] data) { int dataLen = data.length; - data = ByteUtil.copyOf(data, 0, dataLen + CALC_EXTRA_DATA_LEN, + data = ByteUtil.copyOf(data, 0, dataLen + CALC_EXTRA_DATA_LEN, CALC_DATA_OFFSET); PageChannel.wrap(data).putInt(CALC_DATA_LEN_OFFSET, dataLen); return data; @@ -126,18 +128,30 @@ class CalculatedColumnUtil buffer.position(CALC_DATA_OFFSET); return buffer; } - + /** * General calculated column implementation. */ private static class CalcColImpl extends ColumnImpl { + private CalcColEvalContext _calcCol; + CalcColImpl(InitArgs args) throws IOException { super(args); } @Override + protected CalcColEvalContext getCalculationContext() { + return _calcCol; + } + + @Override + protected void setCalcColEvalContext(CalcColEvalContext calcCol) { + _calcCol = calcCol; + } + + @Override public Object read(byte[] data, ByteOrder order) throws IOException { data = unwrapCalculatedValue(data); if((data.length == 0) && !getType().isVariableLength()) { @@ -148,7 +162,7 @@ class CalculatedColumnUtil } @Override - protected ByteBuffer writeRealData(Object obj, int remainingRowLength, + protected ByteBuffer writeRealData(Object obj, int remainingRowLength, ByteOrder order) throws IOException { @@ -165,11 +179,23 @@ class CalculatedColumnUtil */ private static class CalcBooleanColImpl extends ColumnImpl { + private CalcColEvalContext _calcCol; + CalcBooleanColImpl(InitArgs args) throws IOException { super(args); } @Override + protected CalcColEvalContext getCalculationContext() { + return _calcCol; + } + + @Override + protected void setCalcColEvalContext(CalcColEvalContext calcCol) { + _calcCol = calcCol; + } + + @Override public boolean storeInNullMask() { // calculated booleans are _not_ stored in null mask return false; @@ -185,7 +211,7 @@ class CalculatedColumnUtil } @Override - protected ByteBuffer writeRealData(Object obj, int remainingRowLength, + protected ByteBuffer writeRealData(Object obj, int remainingRowLength, ByteOrder order) throws IOException { @@ -199,11 +225,23 @@ class CalculatedColumnUtil */ private static class CalcTextColImpl extends TextColumnImpl { + private CalcColEvalContext _calcCol; + CalcTextColImpl(InitArgs args) throws IOException { super(args); } @Override + protected CalcColEvalContext getCalculationContext() { + return _calcCol; + } + + @Override + protected void setCalcColEvalContext(CalcColEvalContext calcCol) { + _calcCol = calcCol; + } + + @Override public short getLengthInUnits() { // the byte "length" includes the calculated field overhead. remove // that to get the _actual_ data length (in units) @@ -216,7 +254,7 @@ class CalculatedColumnUtil } @Override - protected ByteBuffer writeRealData(Object obj, int remainingRowLength, + protected ByteBuffer writeRealData(Object obj, int remainingRowLength, ByteOrder order) throws IOException { @@ -230,11 +268,23 @@ class CalculatedColumnUtil */ private static class CalcMemoColImpl extends MemoColumnImpl { + private CalcColEvalContext _calcCol; + CalcMemoColImpl(InitArgs args) throws IOException { super(args); } @Override + protected CalcColEvalContext getCalculationContext() { + return _calcCol; + } + + @Override + protected void setCalcColEvalContext(CalcColEvalContext calcCol) { + _calcCol = calcCol; + } + + @Override protected int getMaxLengthInUnits() { // the byte "length" includes the calculated field overhead. remove // that to get the _actual_ data length (in units) @@ -249,12 +299,12 @@ class CalculatedColumnUtil } @Override - protected ByteBuffer writeLongValue(byte[] value, int remainingRowLength) + protected ByteBuffer writeLongValue(byte[] value, int remainingRowLength) throws IOException { return super.writeLongValue( wrapCalculatedValue(value), remainingRowLength); - } + } } /** @@ -262,11 +312,23 @@ class CalculatedColumnUtil */ private static class CalcNumericColImpl extends NumericColumnImpl { + private CalcColEvalContext _calcCol; + CalcNumericColImpl(InitArgs args) throws IOException { super(args); } @Override + protected CalcColEvalContext getCalculationContext() { + return _calcCol; + } + + @Override + protected void setCalcColEvalContext(CalcColEvalContext calcCol) { + _calcCol = calcCol; + } + + @Override public byte getPrecision() { return (byte)getType().getMaxPrecision(); } @@ -282,7 +344,7 @@ class CalculatedColumnUtil } @Override - protected ByteBuffer writeRealData(Object obj, int remainingRowLength, + protected ByteBuffer writeRealData(Object obj, int remainingRowLength, ByteOrder order) throws IOException { @@ -337,14 +399,14 @@ class CalculatedColumnUtil decVal = decVal.setScale(maxScale); } int scale = decVal.scale(); - + // check precision if(decVal.precision() > getType().getMaxPrecision()) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Numeric value is too big for specified precision " + getType().getMaxPrecision() + ": " + decVal)); } - + // convert to unscaled BigInteger, big-endian bytes byte[] intValBytes = toUnscaledByteArray(decVal, dataLen - 4); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColDefaultValueEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColDefaultValueEvalContext.java new file mode 100644 index 0000000..734c908 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColDefaultValueEvalContext.java @@ -0,0 +1,41 @@ +/* +Copyright (c) 2018 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; + +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.expr.Expressionator; + +/** + * + * @author James Ahlborn + */ +public class ColDefaultValueEvalContext extends ColEvalContext +{ + public ColDefaultValueEvalContext(ColumnImpl col) { + super(col); + } + + ColDefaultValueEvalContext setExpr(String exprStr) { + setExpr(Expressionator.Type.DEFAULT_VALUE, exprStr); + return this; + } + + @Override + public Value.Type getResultType() { + return toValueType(getCol().getType()); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColEvalContext.java new file mode 100644 index 0000000..e62f0db --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColEvalContext.java @@ -0,0 +1,43 @@ +/* +Copyright (c) 2018 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; + + + + +/** + * + * @author James Ahlborn + */ +public abstract class ColEvalContext extends BaseEvalContext +{ + private final ColumnImpl _col; + + public ColEvalContext(ColumnImpl col) { + super(col.getDatabase().getEvalContext()); + _col = col; + } + + protected ColumnImpl getCol() { + return _col; + } + + @Override + protected String withErrorContext(String msg) { + return _col.withErrorContext(msg); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColValidatorEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColValidatorEvalContext.java new file mode 100644 index 0000000..521fe4d --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColValidatorEvalContext.java @@ -0,0 +1,95 @@ +/* +Copyright (c) 2018 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; + +import java.io.IOException; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.InvalidValueException; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Identifier; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.expr.Expressionator; +import com.healthmarketscience.jackcess.util.ColumnValidator; + +/** + * + * @author James Ahlborn + */ +public class ColValidatorEvalContext extends ColEvalContext +{ + private String _helpStr; + private Object _val; + + public ColValidatorEvalContext(ColumnImpl col) { + super(col); + } + + ColValidatorEvalContext setExpr(String exprStr, String helpStr) { + setExpr(Expressionator.Type.FIELD_VALIDATOR, exprStr); + _helpStr = helpStr; + return this; + } + + ColumnValidator toColumnValidator(ColumnValidator delegate) { + return new InternalColumnValidator(delegate) { + @Override + protected Object internalValidate(Column col, Object val) + throws IOException { + return ColValidatorEvalContext.this.validate(col, val); + } + @Override + protected void appendToString(StringBuilder sb) { + sb.append("expression=").append(ColValidatorEvalContext.this); + } + }; + } + + private void reset() { + _val = null; + } + + @Override + public Value getThisColumnValue() { + return toValue(_val, getCol().getType()); + } + + @Override + public Value getIdentifierValue(Identifier identifier) { + // col validators can only get "this" column, but they can refer to it by + // name + if(!getCol().isThisColumn(identifier)) { + throw new EvalException("Cannot access other fields for " + identifier); + } + return getThisColumnValue(); + } + + private Object validate(Column col, Object val) throws IOException { + try { + _val = val; + Boolean result = (Boolean)eval(); + if(!result) { + String msg = ((_helpStr != null) ? _helpStr : + "Invalid column value '" + val + "'"); + throw new InvalidValueException(withErrorContext(msg)); + } + return val; + } finally { + reset(); + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index b5a0d0e..be2bf1c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -44,11 +44,13 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.InvalidValueException; import com.healthmarketscience.jackcess.PropertyMap; import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.complex.ComplexColumnInfo; import com.healthmarketscience.jackcess.complex.ComplexValue; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl; import com.healthmarketscience.jackcess.util.ColumnValidator; import com.healthmarketscience.jackcess.util.SimpleColumnValidator; @@ -62,9 +64,9 @@ import org.apache.commons.logging.LogFactory; * @usage _intermediate_class_ */ public class ColumnImpl implements Column, Comparable<ColumnImpl> { - + protected static final Log LOG = LogFactory.getLog(ColumnImpl.class); - + /** * Placeholder object for adding rows which indicates that the caller wants * the RowId of the new row. Must be added as an extra value at the end of @@ -88,31 +90,31 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { */ static final long MILLIS_BETWEEN_EPOCH_AND_1900 = 25569L * MILLISECONDS_PER_DAY; - + /** * mask for the fixed len bit * @usage _advanced_field_ */ public static final byte FIXED_LEN_FLAG_MASK = (byte)0x01; - + /** * mask for the auto number bit * @usage _advanced_field_ */ public static final byte AUTO_NUMBER_FLAG_MASK = (byte)0x04; - + /** * mask for the auto number guid bit * @usage _advanced_field_ */ public static final byte AUTO_NUMBER_GUID_FLAG_MASK = (byte)0x40; - + /** * mask for the hyperlink bit (on memo types) * @usage _advanced_field_ */ public static final byte HYPERLINK_FLAG_MASK = (byte)0x80; - + /** * mask for the "is updatable" field bit * @usage _advanced_field_ @@ -141,7 +143,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * the "general" text sort order, latest version (access 2010+) * @usage _intermediate_field_ */ - public static final SortOrder GENERAL_SORT_ORDER = + public static final SortOrder GENERAL_SORT_ORDER = new SortOrder(GENERAL_SORT_ORDER_VALUE, (byte)1); /** pattern matching textual guid strings (allows for optional surrounding @@ -149,7 +151,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { private static final Pattern GUID_PATTERN = Pattern.compile("\\s*[{]?([\\p{XDigit}]{8})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{4})-([\\p{XDigit}]{12})[}]?\\s*"); /** header used to indicate unicode text compression */ - private static final byte[] TEXT_COMPRESSION_HEADER = + private static final byte[] TEXT_COMPRESSION_HEADER = { (byte)0xFF, (byte)0XFE }; private static final char MIN_COMPRESS_CHAR = 1; private static final char MAX_COMPRESS_CHAR = 0xFF; @@ -185,10 +187,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { /** the auto number generator for this column (if autonumber column) */ private final AutoNumberGenerator _autoNumberGenerator; /** properties for this column, if any */ - private PropertyMap _props; + private PropertyMap _props; /** Validator for writing new values */ private ColumnValidator _validator = SimpleColumnValidator.INSTANCE; - + /** default value generator */ + private ColDefaultValueEvalContext _defValue; + /** * @usage _advanced_method_ */ @@ -213,7 +217,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { _fixedDataOffset = fixedOffset; _varLenTableIndex = varLenIndex; } - + /** * Read a column definition in from a buffer * @usage _advanced_method_ @@ -225,19 +229,19 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { _name = args.name; _displayIndex = args.displayIndex; _type = args.type; - + _columnNumber = args.buffer.getShort( args.offset + getFormat().OFFSET_COLUMN_NUMBER); _columnLength = args.buffer.getShort( args.offset + getFormat().OFFSET_COLUMN_LENGTH); - + _variableLength = ((args.flags & FIXED_LEN_FLAG_MASK) == 0); - _autoNumber = ((args.flags & + _autoNumber = ((args.flags & (AUTO_NUMBER_FLAG_MASK | AUTO_NUMBER_GUID_FLAG_MASK)) != 0); _calculated = ((args.extFlags & CALCULATED_EXT_FLAG_MASK) != 0); - + _autoNumberGenerator = createAutoNumberGenerator(); - + if(_variableLength) { _varLenTableIndex = args.buffer.getShort( args.offset + getFormat().OFFSET_COLUMN_VARIABLE_TABLE_INDEX); @@ -248,7 +252,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { _varLenTableIndex = 0; } } - + /** * Creates the appropriate ColumnImpl class and reads a column definition in * from a buffer @@ -273,7 +277,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { colType = resultType; } } - + try { args.type = DataType.fromByte(colType); } catch(IOException e) { @@ -288,7 +292,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(calculated) { return CalculatedColumnUtil.create(args); } - + switch(args.type) { case TEXT: return new TextColumnImpl(args); @@ -306,7 +310,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(args.type.isLongValue()) { return new LongValueColumnImpl(args); } - + return new ColumnImpl(args); } @@ -320,7 +324,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { void collectUsageMapPages(Collection<Integer> pages) { // base does nothing } - + /** * Secondary column initialization after the table is fully loaded. */ @@ -332,10 +336,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _table; } - public DatabaseImpl getDatabase() { + public DatabaseImpl getDatabase() { return getTable().getDatabase(); } - + /** * @usage _advanced_method_ */ @@ -349,15 +353,15 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public PageChannel getPageChannel() { return getDatabase().getPageChannel(); } - + public String getName() { return _name; } - + public boolean isVariableLength() { return _variableLength; } - + public boolean isAutoNumber() { return _autoNumber; } @@ -379,7 +383,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public void setColumnIndex(int newColumnIndex) { _columnIndex = newColumnIndex; } - + /** * @usage _advanced_method_ */ @@ -390,11 +394,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public DataType getType() { return _type; } - + public int getSQLType() throws SQLException { return _type.getSQLType(); } - + public boolean isCompressedUnicode() { return false; } @@ -402,7 +406,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public byte getPrecision() { return (byte)getType().getDefaultPrecision(); } - + public byte getScale() { return (byte)getType().getDefaultScale(); } @@ -432,14 +436,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public boolean isCalculated() { return _calculated; } - + /** * @usage _advanced_method_ */ public int getVarLenTableIndex() { return _varLenTableIndex; } - + /** * @usage _advanced_method_ */ @@ -458,7 +462,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public boolean isAppendOnly() { return (getVersionHistoryColumn() != null); } - + public ColumnImpl getVersionHistoryColumn() { return null; } @@ -481,17 +485,105 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public boolean isHyperlink() { return false; } - + public ComplexColumnInfo<? extends ComplexValue> getComplexInfo() { return null; } + void initColumnValidator() throws IOException { + // first initialize any "external" (user-defined) validator + setColumnValidator(null); + + // next, initialize any "internal" (property defined) validators + reloadPropertiesValidators(); + } + + void reloadPropertiesValidators() throws IOException { + + if(isAutoNumber()) { + // none of the props stuff applies to autonumber columns + return; + } + + if(isCalculated()) { + + CalcColEvalContext calcCol = null; + + if(getDatabase().isEvaluateExpressions()) { + + // init calc col expression evaluator + PropertyMap props = getProperties(); + String calcExpr = (String)props.getValue(PropertyMap.EXPRESSION_PROP); + calcCol = new CalcColEvalContext(this).setExpr(calcExpr); + } + + setCalcColEvalContext(calcCol); + + // none of the remaining props stuff applies to calculated columns + return; + } + + // discard any existing internal validators and re-compute them + // (essentially unwrap the external validator) + _validator = getColumnValidator(); + _defValue = null; + + PropertyMap props = getProperties(); + + // if the "required" property is enabled, add appropriate validator + boolean required = (Boolean)props.getValue(PropertyMap.REQUIRED_PROP, + Boolean.FALSE); + if(required) { + _validator = new RequiredColValidator(_validator); + } + + // if the "allow zero len" property is disabled (textual columns only), + // add appropriate validator + boolean allowZeroLen = + !getType().isTextual() || + (Boolean)props.getValue(PropertyMap.ALLOW_ZERO_LEN_PROP, + Boolean.TRUE); + if(!allowZeroLen) { + _validator = new NoZeroLenColValidator(_validator); + } + + // only check for props based exprs if this is enabled + if(!getDatabase().isEvaluateExpressions()) { + return; + } + + String exprStr = PropertyMaps.getTrimmedStringProperty( + props, PropertyMap.VALIDATION_RULE_PROP); + + if(exprStr != null) { + String helpStr = PropertyMaps.getTrimmedStringProperty( + props, PropertyMap.VALIDATION_TEXT_PROP); + + _validator = new ColValidatorEvalContext(this) + .setExpr(exprStr, helpStr) + .toColumnValidator(_validator); + } + + String defValueStr = PropertyMaps.getTrimmedStringProperty( + props, PropertyMap.DEFAULT_VALUE_PROP); + if(defValueStr != null) { + _defValue = new ColDefaultValueEvalContext(this) + .setExpr(defValueStr); + } + } + + void propertiesUpdated() throws IOException { + reloadPropertiesValidators(); + } + public ColumnValidator getColumnValidator() { - return _validator; + // unwrap any "internal" validator + return ((_validator instanceof InternalColumnValidator) ? + ((InternalColumnValidator)_validator).getExternal() : _validator); } - + public void setColumnValidator(ColumnValidator newValidator) { - + if(isAutoNumber()) { // cannot set autonumber validator (autonumber values are controlled // internally) @@ -502,7 +594,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // just leave default validator instance alone return; } - + if(newValidator == null) { newValidator = getDatabase().getColumnValidatorFactory() .createValidator(this); @@ -510,13 +602,19 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { newValidator = SimpleColumnValidator.INSTANCE; } } - _validator = newValidator; + + // handle delegation if "internal" validator in use + if(_validator instanceof InternalColumnValidator) { + ((InternalColumnValidator)_validator).setExternal(newValidator); + } else { + _validator = newValidator; + } } - + byte getOriginalDataType() { return _type.getValue(); } - + private AutoNumberGenerator createAutoNumberGenerator() { if(!_autoNumber || (_type == null)) { return null; @@ -550,21 +648,21 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } return _props; } - + public Object setRowValue(Object[] rowArray, Object value) { rowArray[_columnIndex] = value; return value; } - + public Object setRowValue(Map<String,Object> rowMap, Object value) { rowMap.put(_name, value); return value; } - + public Object getRowValue(Object[] rowArray) { return rowArray[_columnIndex]; } - + public Object getRowValue(Map<String,?> rowMap) { return rowMap.get(_name); } @@ -572,7 +670,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public boolean storeInNullMask() { return (getType() == DataType.BOOLEAN); } - + public boolean writeToNullMask(Object value) { return toBooleanValue(value); } @@ -590,14 +688,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public Object read(byte[] data) throws IOException { return read(data, PageChannel.DEFAULT_BYTE_ORDER); } - + /** * Deserialize a raw byte value for this column into an Object * @param data The raw byte value * @param order Byte order in which the raw value is stored * @return The deserialized Object * @usage _advanced_method_ - */ + */ public Object read(byte[] data, ByteOrder order) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(data).order(order); @@ -641,10 +739,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { /** * Decodes "Currency" values. - * + * * @param buffer Column value that points to currency data * @return BigDecimal representing the monetary value - * @throws IOException if the value cannot be parsed + * @throws IOException if the value cannot be parsed */ private BigDecimal readCurrencyValue(ByteBuffer buffer) throws IOException @@ -652,7 +750,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(buffer.remaining() != 8) { throw new IOException(withErrorContext("Invalid money value")); } - + return new BigDecimal(BigInteger.valueOf(buffer.getLong(0)), 4); } @@ -670,7 +768,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // adjust scale (will cause the an ArithmeticException if number has too // many decimal places) decVal = decVal.setScale(4); - + // now, remove scale and convert to long (this will throw if the value is // too big) buffer.putLong(decVal.movePointRight(4).longValueExact()); @@ -738,11 +836,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // check precision if(decVal.precision() > getPrecision()) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Numeric value is too big for specified precision " + getPrecision() + ": " + decVal)); } - + // convert to unscaled BigInteger, big-endian bytes byte[] intValBytes = toUnscaledByteArray( decVal, getType().getFixedSize() - 1); @@ -770,11 +868,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // with unsigned values, so we can drop the extra leading 0 intValBytes = ByteUtil.copyOf(intValBytes, 1, maxByteLen); } else { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Too many bytes for valid BigInteger?")); } } else if(intValBytes.length < maxByteLen) { - intValBytes = ByteUtil.copyOf(intValBytes, 0, maxByteLen, + intValBytes = ByteUtil.copyOf(intValBytes, 0, maxByteLen, (maxByteLen - intValBytes.length)); } return intValBytes; @@ -791,15 +889,33 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { long time = fromDateDouble(Double.longBitsToDouble(dateBits)); return new DateExt(time, dateBits); } - + /** * Returns a java long time value converted from an access date double. * @usage _advanced_method_ */ 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) @@ -811,7 +927,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // _not_ the time distance from zero (as one would expect with "normal" // numbers). therefore, we need to do a little number logic to convert // the absolute time fraction into a normal distance from zero number. - long timePart = Math.round((Math.abs(value) % 1.0) * + long timePart = Math.round((Math.abs(value) % 1.0) * (double)MILLISECONDS_PER_DAY); long time = datePart + timePart; @@ -827,13 +943,13 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(value == null) { buffer.putDouble(0d); } else if(value instanceof DateExt) { - + // this is a Date value previously read from readDateValue(). use the // original bits to store the value so we don't lose any precision buffer.putLong(((DateExt)value).getDateBits()); - + } else { - + buffer.putDouble(toDateDouble(value)); } } @@ -845,10 +961,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); } @@ -870,7 +1006,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { /** * @return an appropriate Date long value for the given object */ - private static long toDateLong(Object value) + private static long toDateLong(Object value) { return ((value instanceof Date) ? ((Date)value).getTime() : @@ -883,28 +1019,26 @@ 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)); - } - + } + /** * 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)); return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); - } - + } + /** * Decodes a GUID value. */ @@ -949,7 +1083,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { { Matcher m = GUID_PATTERN.matcher(toCharSequence(value)); if(!m.matches()) { - throw new IOException(withErrorContext("Invalid GUID: " + value)); + throw new InvalidValueException( + withErrorContext("Invalid GUID: " + value)); } ByteBuffer origBuffer = null; @@ -966,7 +1101,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { ByteUtil.writeHexString(buffer, m.group(3)); ByteUtil.writeHexString(buffer, m.group(4)); ByteUtil.writeHexString(buffer, m.group(5)); - + if(tmpBuf != null) { // the first 3 guid components are integer components which need to // respect endianness, so swap 4-byte int, 2-byte int, 2-byte int @@ -985,13 +1120,31 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } /** + * Returns a default value for this column + */ + public Object generateDefaultValue() throws IOException { + return ((_defValue != null) ? _defValue.eval() : null); + } + + /** * Passes the given obj through the currently configured validator for this * column and returns the result. */ public Object validate(Object obj) throws IOException { return _validator.validate(this, obj); } - + + /** + * Returns the context used to manage calculated column values. + */ + protected CalcColEvalContext getCalculationContext() { + throw new UnsupportedOperationException(); + } + + protected void setCalcColEvalContext(CalcColEvalContext calcCol) { + throw new UnsupportedOperationException(); + } + /** * Serialize an Object into a raw byte value for this column in little * endian order @@ -1004,7 +1157,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { { return write(obj, remainingRowLength, PageChannel.DEFAULT_BYTE_ORDER); } - + /** * Serialize an Object into a raw byte value for this column * @param obj Object to serialize @@ -1023,14 +1176,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return writeRealData(obj, remainingRowLength, order); } - protected ByteBuffer writeRealData(Object obj, int remainingRowLength, + protected ByteBuffer writeRealData(Object obj, int remainingRowLength, ByteOrder order) throws IOException { if(!isVariableLength() || !getType().isVariableLength()) { return writeFixedLengthField(obj, order); } - + // this is an "inline" var length field switch(getType()) { case NUMERIC: @@ -1044,7 +1197,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { case TEXT: return encodeTextValue( obj, 0, getLengthInUnits(), false).order(order); - + case BINARY: case UNKNOWN_0D: case UNSUPPORTED_VARLEN: @@ -1136,7 +1289,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { case UNSUPPORTED_FIXEDLEN: byte[] bytes = toByteArray(obj); if(bytes.length != getLength()) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Invalid fixed size binary data, size " + getLength() + ", got " + bytes.length)); } @@ -1148,7 +1301,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } return buffer; } - + /** * Decodes a compressed or uncompressed text value. */ @@ -1162,7 +1315,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { (data[1] == TEXT_COMPRESSION_HEADER[1])); if(isCompressed) { - + // this is a whacky compression combo that switches back and forth // between compressed/uncompressed using a 0x00 byte (starting in // compressed mode) @@ -1180,7 +1333,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { inCompressedMode = !inCompressedMode; ++dataEnd; dataStart = dataEnd; - + } else { ++dataEnd; } @@ -1189,9 +1342,9 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { decodeTextSegment(data, dataStart, dataEnd, inCompressedMode, textBuf); return textBuf.toString(); - + } - + return decodeUncompressedText(data, getCharset()); } @@ -1200,7 +1353,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * given status of the segment (compressed/uncompressed). */ private void decodeTextSegment(byte[] data, int dataStart, int dataEnd, - boolean inCompressedMode, + boolean inCompressedMode, StringBuilder textBuf) { if(dataEnd <= dataStart) { @@ -1215,7 +1368,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { for(int i = dataStart; i < dataEnd; ++i) { tmpData[tmpIdx] = data[i]; tmpIdx += 2; - } + } data = tmpData; dataStart = 0; dataLength = data.length; @@ -1233,7 +1386,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { byte[] textBytes, int startPos, int length, Charset charset) { return charset.decode(ByteBuffer.wrap(textBytes, startPos, length)); - } + } /** * Encodes a text value, possibly compressing. @@ -1244,23 +1397,23 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { { CharSequence text = toCharSequence(obj); if((text.length() > maxChars) || (text.length() < minChars)) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Text is wrong length for " + getType() + " column, max " + maxChars + ", min " + minChars + ", got " + text.length())); } - + // may only compress if column type allows it if(!forceUncompressed && isCompressedUnicode() && (text.length() <= getFormat().MAX_COMPRESSED_UNICODE_SIZE) && isUnicodeCompressible(text)) { - byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length + + byte[] encodedChars = new byte[TEXT_COMPRESSION_HEADER.length + text.length()]; encodedChars[0] = TEXT_COMPRESSION_HEADER[0]; encodedChars[1] = TEXT_COMPRESSION_HEADER[1]; for(int i = 0; i < text.length(); ++i) { - encodedChars[i + TEXT_COMPRESSION_HEADER.length] = + encodedChars[i + TEXT_COMPRESSION_HEADER.length] = (byte)text.charAt(i); } return ByteBuffer.wrap(encodedChars); @@ -1317,7 +1470,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } return flags; } - + @Override public String toString() { ToStringBuilder sb = CustomToStringStyle.builder(this) @@ -1327,9 +1480,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { " (" + _type + ")") .append("number", _columnNumber) .append("length", _columnLength) - .append("variableLength", _variableLength); + .append("variableLength", _variableLength); if(_calculated) { - sb.append("calculated", _calculated); + sb.append("calculated", _calculated) + .append("expression", + CustomToStringStyle.ignoreNull(getCalculationContext())); } if(_type.isTextual()) { sb.append("compressedUnicode", isCompressedUnicode()) @@ -1339,10 +1494,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } if(isAppendOnly()) { sb.append("appendOnly", isAppendOnly()); - } + } if(isHyperlink()) { sb.append("hyperlink", isHyperlink()); - } + } } if(_type.getHasScalePrecision()) { sb.append("precision", getPrecision()) @@ -1351,19 +1506,21 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(_autoNumber) { sb.append("lastAutoNumber", _autoNumberGenerator.getLast()); } - if(getComplexInfo() != null) { - sb.append("complexInfo", getComplexInfo()); - } + sb.append("complexInfo", CustomToStringStyle.ignoreNull(getComplexInfo())) + .append("validator", CustomToStringStyle.ignoreNull( + ((_validator != SimpleColumnValidator.INSTANCE) ? + _validator : null))) + .append("defaultValue", CustomToStringStyle.ignoreNull(_defValue)); return sb.toString(); } - + /** * @param textBytes bytes of text to decode * @param charset relevant charset * @return the decoded string * @usage _advanced_method_ */ - public static String decodeUncompressedText(byte[] textBytes, + public static String decodeUncompressedText(byte[] textBytes, Charset charset) { return decodeUncompressedText(textBytes, 0, textBytes.length, charset) @@ -1379,12 +1536,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public static ByteBuffer encodeUncompressedText(CharSequence text, Charset charset) { - CharBuffer cb = ((text instanceof CharBuffer) ? + CharBuffer cb = ((text instanceof CharBuffer) ? (CharBuffer)text : CharBuffer.wrap(text)); return charset.encode(cb); } - + /** * Orders Columns by column number. * @usage _general_method_ @@ -1398,7 +1555,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return 0; } } - + /** * @param columns A list of columns in a table definition * @return The number of variable length columns found in the list @@ -1419,7 +1576,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; @@ -1429,6 +1596,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()); } @@ -1438,16 +1610,31 @@ 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()); } - + /** * @return an appropriate CharSequence representation of the given object. * @usage _advanced_method_ @@ -1529,10 +1716,19 @@ 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()); } - + /** * Swaps the bytes of the given numeric in place. */ @@ -1545,11 +1741,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; } @@ -1595,7 +1791,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } protected static void writeDefinition( - TableMutator mutator, ColumnBuilder col, ByteBuffer buffer) + TableMutator mutator, ColumnBuilder col, ByteBuffer buffer) throws IOException { TableMutator.ColumnOffsets colOffsets = mutator.getColumnOffsets(); @@ -1652,9 +1848,9 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { short length = col.getLength(); if(col.isCalculated()) { // calced columns have additional value overhead - if(!col.getType().isVariableLength() || + if(!col.getType().isVariableLength() || col.getType().getHasScalePrecision()) { - length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN; + length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN; } else { length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN; } @@ -1682,7 +1878,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { TableMutator.ColumnState colState = creator.getColumnState(lvalCol); buffer.putShort(lvalCol.getColumnNumber()); - + // owned pages umap (both are on same page) buffer.put(colState.getUmapOwnedRowNumber()); ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); @@ -1707,7 +1903,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // probably a file we wrote, before handling sort order return format.DEFAULT_SORT_ORDER; } - + if(value == GENERAL_SORT_ORDER_VALUE) { if(version == GENERAL_LEGACY_SORT_ORDER.getVersion()) { return GENERAL_LEGACY_SORT_ORDER; @@ -1737,7 +1933,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { int extFlagsOffset = format.OFFSET_COLUMN_EXT_FLAGS; return ((extFlagsOffset >= 0) ? buffer.get(offset + extFlagsOffset) : 0); } - + /** * Writes the sort order info to the given buffer at the current position. */ @@ -1746,7 +1942,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(sortOrder == null) { sortOrder = format.DEFAULT_SORT_ORDER; } - buffer.putShort(sortOrder.getValue()); + buffer.putShort(sortOrder.getValue()); if(format.SIZE_SORT_ORDER == 4) { buffer.put((byte)0x00); // unknown buffer.put(sortOrder.getVersion()); @@ -1766,7 +1962,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) { @@ -1777,37 +1974,37 @@ 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()); + return ((value instanceof Short) ? value : + toNumber(value, db).shortValue()); case LONG: - return ((value instanceof Integer) ? value : - toNumber(value).intValue()); + return ((value instanceof Integer) ? value : + toNumber(value, db).intValue()); case MONEY: - return toBigDecimal(value); + return toBigDecimal(value, db); case FLOAT: - return ((value instanceof Float) ? value : - toNumber(value).floatValue()); + return ((value instanceof Float) ? value : + toNumber(value, db).floatValue()); case DOUBLE: - return ((value instanceof Double) ? value : - toNumber(value).doubleValue()); + return ((value instanceof Double) ? value : + toNumber(value, db).doubleValue()); case SHORT_DATE_TIME: return ((value instanceof DateExt) ? value : new Date(toDateLong(value))); case TEXT: case MEMO: case GUID: - return ((value instanceof String) ? value : + 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; case BIG_INT: - return ((value instanceof Long) ? value : - toNumber(value).longValue()); + return ((value instanceof Long) ? value : + toNumber(value, db).longValue()); default: // some variation of binary data return toByteArray(value); @@ -1818,9 +2015,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return withErrorContext(msg, getDatabase(), getTable().getName(), getName()); } + boolean isThisColumn(Identifier identifier) { + return(getTable().isThisTable(identifier) && + getName().equalsIgnoreCase(identifier.getObjectName())); + } + private static String withErrorContext( String msg, DatabaseImpl db, String tableName, String colName) { - return msg + " (Db=" + db.getName() + ";Table=" + tableName + ";Column=" + + return msg + " (Db=" + db.getName() + ";Table=" + tableName + ";Column=" + colName + ")"; } @@ -1843,7 +2045,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public long getDateBits() { return _dateBits; } - + private Object writeReplace() throws ObjectStreamException { // if we are going to serialize this Date, convert it back to a normal // Date (in case it is restored outside of the context of jackcess) @@ -1911,14 +2113,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * "lost" for the table.</i> */ public abstract Object handleInsert( - TableImpl.WriteRowState writeRowState, Object inRowValue) + TableImpl.WriteRowState writeRowState, Object inRowValue) throws IOException; /** * Restores a previous autonumber generated by this generator. */ public abstract void restoreLast(Object last); - + /** * Returns the type of values generated by this generator. */ @@ -1943,12 +2145,13 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { @Override public Object handleInsert(TableImpl.WriteRowState writeRowState, - Object inRowValue) + Object inRowValue) throws IOException { int inAutoNum = toNumber(inRowValue).intValue(); - if(inAutoNum <= INVALID_AUTO_NUMBER && !getTable().isAllowAutoNumberInsert()) { - throw new IOException(withErrorContext( + if(inAutoNum <= INVALID_AUTO_NUMBER && + !getTable().isAllowAutoNumberInsert()) { + throw new InvalidValueException(withErrorContext( "Invalid auto number value " + inAutoNum)); } // the table stores the last long autonumber used @@ -1962,7 +2165,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { getTable().restoreLastLongAutoNumber((Integer)last); } } - + @Override public DataType getType() { return DataType.LONG; @@ -1989,7 +2192,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { @Override public Object handleInsert(TableImpl.WriteRowState writeRowState, - Object inRowValue) + Object inRowValue) throws IOException { _lastAutoNumber = toCharSequence(inRowValue); @@ -2000,7 +2203,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public void restoreLast(Object last) { _lastAutoNumber = null; } - + @Override public DataType getType() { return DataType.GUID; @@ -2026,13 +2229,13 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { nextComplexAutoNum = getTable().getNextComplexTypeAutoNumber(); writeRowState.setComplexAutoNumber(nextComplexAutoNum); } - return new ComplexValueForeignKeyImpl(ColumnImpl.this, + return new ComplexValueForeignKeyImpl(ColumnImpl.this, nextComplexAutoNum); } @Override public Object handleInsert(TableImpl.WriteRowState writeRowState, - Object inRowValue) + Object inRowValue) throws IOException { ComplexValueForeignKey inComplexFK = null; @@ -2044,12 +2247,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } if(inComplexFK.getColumn() != ColumnImpl.this) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Wrong column for complex value foreign key, found " + inComplexFK.getColumn().getName())); } if(inComplexFK.get() < 1) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Invalid complex value foreign key value " + inComplexFK.get())); } // same value is shared across all ComplexType values in a row @@ -2057,7 +2260,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { if(prevRowValue <= INVALID_AUTO_NUMBER) { writeRowState.setComplexAutoNumber(inComplexFK.get()); } else if(prevRowValue != inComplexFK.get()) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Inconsistent complex value foreign key values: found " + prevRowValue + ", given " + inComplexFK)); } @@ -2075,21 +2278,21 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { ((ComplexValueForeignKey)last).get()); } } - + @Override public DataType getType() { return DataType.COMPLEX_TYPE; } } - + private final class UnsupportedAutoNumberGenerator extends AutoNumberGenerator { private final DataType _genType; - + private UnsupportedAutoNumberGenerator(DataType genType) { _genType = genType; } - + @Override public Object getLast() { return null; @@ -2110,14 +2313,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { public void restoreLast(Object last) { throw new UnsupportedOperationException(); } - + @Override public DataType getType() { return _genType; } } - + /** * Information about the sort order (collation) for a textual column. * @usage _intermediate_class_ @@ -2126,7 +2329,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { { private final short _value; private final byte _version; - + public SortOrder(short value, byte version) { _value = value; _version = version; @@ -2183,10 +2386,68 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { this.offset = offset; this.name = name; this.displayIndex = displayIndex; - + this.colType = buffer.get(offset + table.getFormat().OFFSET_COLUMN_TYPE); this.flags = buffer.get(offset + table.getFormat().OFFSET_COLUMN_FLAGS); this.extFlags = readExtraFlags(buffer, offset, table.getFormat()); } } + + /** + * "Internal" column validator for columns with the "required" property + * enabled. + */ + private static final class RequiredColValidator extends InternalColumnValidator + { + private RequiredColValidator(ColumnValidator delegate) { + super(delegate); + } + + @Override + protected Object internalValidate(Column col, Object val) + throws IOException + { + if(val == null) { + throw new InvalidValueException( + ((ColumnImpl)col).withErrorContext( + "Missing value for required column")); + } + return val; + } + + @Override + protected void appendToString(StringBuilder sb) { + sb.append("required=true"); + } + } + + /** + * "Internal" column validator for text columns with the "allow zero len" + * property disabled. + */ + private static final class NoZeroLenColValidator extends InternalColumnValidator + { + private NoZeroLenColValidator(ColumnValidator delegate) { + super(delegate); + } + + @Override + protected Object internalValidate(Column col, Object val) + throws IOException + { + CharSequence valStr = ColumnImpl.toCharSequence(val); + // oddly enough null is allowed for non-zero len strings + if((valStr != null) && valStr.length() == 0) { + throw new InvalidValueException( + ((ColumnImpl)col).withErrorContext( + "Zero length string is not allowed")); + } + return valStr; + } + + @Override + protected void appendToString(StringBuilder sb) { + sb.append("allowZeroLength=false"); + } + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java b/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java index 870cb54..707e163 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java @@ -30,14 +30,15 @@ import org.apache.commons.lang.builder.ToStringBuilder; * * @author James Ahlborn */ -public class CustomToStringStyle extends StandardToStringStyle +public class CustomToStringStyle extends StandardToStringStyle { private static final long serialVersionUID = 0L; private static final String ML_FIELD_SEP = SystemUtils.LINE_SEPARATOR + " "; private static final String IMPL_SUFFIX = "Impl"; private static final int MAX_BYTE_DETAIL_LEN = 20; - + private static final Object IGNORE_ME = new Object(); + public static final CustomToStringStyle INSTANCE = new CustomToStringStyle() { private static final long serialVersionUID = 0L; { @@ -59,7 +60,7 @@ public class CustomToStringStyle extends StandardToStringStyle } }; - private CustomToStringStyle() { + private CustomToStringStyle() { } public static ToStringBuilder builder(Object obj) { @@ -71,6 +72,15 @@ public class CustomToStringStyle extends StandardToStringStyle } @Override + public void append(StringBuffer buffer, String fieldName, Object value, + Boolean fullDetail) { + if(value == IGNORE_ME) { + return; + } + super.append(buffer, fieldName, value, fullDetail); + } + + @Override protected void appendClassName(StringBuffer buffer, Object obj) { if(obj instanceof String) { // the caller gave an "explicit" class name @@ -84,7 +94,7 @@ public class CustomToStringStyle extends StandardToStringStyle protected String getShortClassName(Class clss) { String shortName = super.getShortClassName(clss); if(shortName.endsWith(IMPL_SUFFIX)) { - shortName = shortName.substring(0, + shortName = shortName.substring(0, shortName.length() - IMPL_SUFFIX.length()); } int idx = shortName.lastIndexOf('.'); @@ -95,7 +105,7 @@ public class CustomToStringStyle extends StandardToStringStyle } @Override - protected void appendDetail(StringBuffer buffer, String fieldName, + protected void appendDetail(StringBuffer buffer, String fieldName, Object value) { if(value instanceof ByteBuffer) { appendDetail(buffer, (ByteBuffer)value); @@ -105,7 +115,7 @@ public class CustomToStringStyle extends StandardToStringStyle } @Override - protected void appendDetail(StringBuffer buffer, String fieldName, + protected void appendDetail(StringBuffer buffer, String fieldName, Collection value) { buffer.append("["); @@ -167,32 +177,36 @@ public class CustomToStringStyle extends StandardToStringStyle } @Override - protected void appendDetail(StringBuffer buffer, String fieldName, + protected void appendDetail(StringBuffer buffer, String fieldName, byte[] array) { appendDetail(buffer, PageChannel.wrap(array)); } - private void appendValueDetail(StringBuffer buffer, String fieldName, + private void appendValueDetail(StringBuffer buffer, String fieldName, Object value) { if (value == null) { appendNullText(buffer, fieldName); } else { appendInternal(buffer, fieldName, value, true); - } + } } private static void appendDetail(StringBuffer buffer, ByteBuffer bb) { int len = bb.remaining(); buffer.append("(").append(len).append(") "); - buffer.append(ByteUtil.toHexString(bb, bb.position(), + buffer.append(ByteUtil.toHexString(bb, bb.position(), Math.min(len, MAX_BYTE_DETAIL_LEN))); if(len > MAX_BYTE_DETAIL_LEN) { buffer.append(" ..."); - } + } } private static String indent(Object obj) { return ((obj != null) ? obj.toString().replaceAll( SystemUtils.LINE_SEPARATOR, ML_FIELD_SEP) : null); } + + public static Object ignoreNull(Object obj) { + return ((obj != null) ? obj : IGNORE_ME); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java new file mode 100644 index 0000000..ab8a2d4 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java @@ -0,0 +1,99 @@ +/* +Copyright (c) 2018 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; + +import java.text.SimpleDateFormat; +import java.util.Map; +import javax.script.Bindings; +import javax.script.SimpleBindings; + +import com.healthmarketscience.jackcess.expr.EvalConfig; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.FunctionLookup; +import com.healthmarketscience.jackcess.expr.TemporalConfig; +import com.healthmarketscience.jackcess.impl.expr.DefaultFunctions; +import com.healthmarketscience.jackcess.impl.expr.Expressionator; +import com.healthmarketscience.jackcess.impl.expr.RandomContext; + +/** + * + * @author James Ahlborn + */ +public class DBEvalContext implements Expressionator.ParseContext, EvalConfig +{ + private static final int MAX_CACHE_SIZE = 10; + + private final DatabaseImpl _db; + private FunctionLookup _funcs = DefaultFunctions.LOOKUP; + private Map<String,SimpleDateFormat> _sdfs; + private TemporalConfig _temporal; + private final RandomContext _rndCtx = new RandomContext(); + private Bindings _bindings = new SimpleBindings(); + + public DBEvalContext(DatabaseImpl db) + { + _db = db; + } + + protected DatabaseImpl getDatabase() { + return _db; + } + + public TemporalConfig getTemporalConfig() { + return _temporal; + } + + public void setTemporalConfig(TemporalConfig temporal) { + _temporal = temporal; + } + + public FunctionLookup getFunctionLookup() { + return _funcs; + } + + public void setFunctionLookup(FunctionLookup lookup) { + _funcs = lookup; + } + + public Bindings getBindings() { + return _bindings; + } + + public void setBindings(Bindings bindings) { + _bindings = bindings; + } + + public SimpleDateFormat createDateFormat(String formatStr) { + if(_sdfs == null) { + _sdfs = new SimpleCache<String,SimpleDateFormat>(MAX_CACHE_SIZE); + } + SimpleDateFormat sdf = _sdfs.get(formatStr); + if(formatStr == null) { + sdf = _db.createDateFormat(formatStr); + _sdfs.put(formatStr, sdf); + } + return sdf; + } + + public float getRandom(Integer seed) { + return _rndCtx.getRandom(seed); + } + + public Function getExpressionFunction(String name) { + return _funcs.getFunction(name); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index cc76ccb..5878a47 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; @@ -38,7 +39,6 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -63,6 +63,7 @@ import com.healthmarketscience.jackcess.RuntimeIOException; import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.TableBuilder; import com.healthmarketscience.jackcess.TableMetaData; +import com.healthmarketscience.jackcess.expr.EvalConfig; import com.healthmarketscience.jackcess.impl.query.QueryImpl; import com.healthmarketscience.jackcess.query.Query; import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; @@ -83,7 +84,7 @@ import org.apache.commons.logging.LogFactory; * @usage _intermediate_class_ */ public class DatabaseImpl implements Database -{ +{ private static final Log LOG = LogFactory.getLog(DatabaseImpl.class); /** this is the default "userId" used if we cannot find existing info. this @@ -94,11 +95,11 @@ public class DatabaseImpl implements Database /** the default value for the resource path used to load classpath * resources. */ - public static final String DEFAULT_RESOURCE_PATH = + public static final String DEFAULT_RESOURCE_PATH = "com/healthmarketscience/jackcess/"; /** the resource path to be used when loading classpath resources */ - static final String RESOURCE_PATH = + static final String RESOURCE_PATH = System.getProperty(RESOURCE_PATH_PROPERTY, DEFAULT_RESOURCE_PATH); /** whether or not this jvm has "broken" nio support */ @@ -119,7 +120,7 @@ public class DatabaseImpl implements Database addFileFormatDetails(FileFormat.V2016, "empty2016", JetFormat.VERSION_16); addFileFormatDetails(FileFormat.MSISAM, null, JetFormat.VERSION_MSISAM); } - + /** System catalog always lives on page 2 */ private static final int PAGE_SYSTEM_CATALOG = 2; /** Name of the system catalog */ @@ -155,7 +156,7 @@ public class DatabaseImpl implements Database private static final String REL_COL_FROM_TABLE = "szReferencedObject"; /** Relationship table column name of the relationship */ private static final String REL_COL_NAME = "szRelationship"; - + /** System catalog column name of the page on which system object definitions are stored */ private static final String CAT_COL_ID = "Id"; @@ -192,7 +193,7 @@ public class DatabaseImpl implements Database /** this object is hidden */ public static final int HIDDEN_OBJECT_FLAG = 0x08; /** all flags which seem to indicate some type of system object */ - static final int SYSTEM_OBJECT_FLAGS = + static final int SYSTEM_OBJECT_FLAGS = SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG; /** read-only channel access mode */ @@ -238,17 +239,17 @@ public class DatabaseImpl implements Database CAT_COL_FLAGS, CAT_COL_PARENT_ID)); /** the columns to read when finding table details */ private static Collection<String> SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS = - new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, - CAT_COL_FLAGS, CAT_COL_PARENT_ID, + new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, + CAT_COL_FLAGS, CAT_COL_PARENT_ID, CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME)); /** the columns to read when getting object propertyes */ private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS = new HashSet<String>(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS)); /** regex matching characters which are invalid in identifier names */ - private static final Pattern INVALID_IDENTIFIER_CHARS = + private static final Pattern INVALID_IDENTIFIER_CHARS = Pattern.compile("[\\p{Cntrl}.!`\\]\\[]"); - + /** the File of the database */ private final File _file; /** the simple name of the database */ @@ -265,13 +266,7 @@ public class DatabaseImpl implements Database * MAX_CACHED_LOOKUP_TABLES). */ private final Map<String, TableInfo> _tableLookup = - new LinkedHashMap<String, TableInfo>() { - private static final long serialVersionUID = 0L; - @Override - protected boolean removeEldestEntry(Map.Entry<String, TableInfo> e) { - return(size() > MAX_CACHED_LOOKUP_TABLES); - } - }; + new SimpleCache<String,TableInfo>(MAX_CACHED_LOOKUP_TABLES); /** set of table names as stored in the mdb file, created on demand */ private Set<String> _tableNames; /** Reads and writes database pages */ @@ -312,6 +307,8 @@ public class DatabaseImpl implements Database private boolean _enforceForeignKeys; /** whether or not auto numbers can be directly inserted by the user */ private boolean _allowAutoNumInsert; + /** whether or not to evaluate expressions */ + private boolean _evaluateExpressions; /** factory for ColumnValidators */ private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; /** cache of in-use tables */ @@ -337,6 +334,8 @@ public class DatabaseImpl implements Database FKEnforcer.initSharedState(); /** Calendar for use interpreting dates/times in Columns */ private Calendar _calendar; + /** shared context for evaluating expressions */ + private DBEvalContext _evalCtx; /** * Open an existing Database. If the existing file is not writeable or the @@ -363,14 +362,14 @@ public class DatabaseImpl implements Database */ public static DatabaseImpl open( File mdbFile, boolean readOnly, FileChannel channel, - boolean autoSync, Charset charset, TimeZone timeZone, + boolean autoSync, Charset charset, TimeZone timeZone, CodecProvider provider) throws IOException { boolean closeChannel = false; if(channel == null) { if(!mdbFile.exists() || !mdbFile.canRead()) { - throw new FileNotFoundException("given file does not exist: " + + throw new FileNotFoundException("given file does not exist: " + mdbFile); } @@ -410,7 +409,7 @@ public class DatabaseImpl implements Database } } } - + /** * Create a new Database for the given fileFormat * @param fileFormat version of new database. @@ -431,18 +430,18 @@ public class DatabaseImpl implements Database * @param timeZone TimeZone to use, if {@code null}, uses default * @usage _advanced_method_ */ - public static DatabaseImpl create(FileFormat fileFormat, File mdbFile, + public static DatabaseImpl create(FileFormat fileFormat, File mdbFile, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone) throws IOException { FileFormatDetails details = getFileFormatDetails(fileFormat); if (details.getFormat().READ_ONLY) { - throw new IOException("File format " + fileFormat + + throw new IOException("File format " + fileFormat + " does not support writing for " + mdbFile); } if(details.getEmptyFilePath() == null) { - throw new IOException("File format " + fileFormat + + throw new IOException("File format " + fileFormat + " does not support file creation for " + mdbFile); } @@ -457,7 +456,7 @@ public class DatabaseImpl implements Database channel.truncate(0); transferDbFrom(channel, getResourceAsStream(details.getEmptyFilePath())); channel.force(true); - DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, + DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, fileFormat, charset, timeZone, null); success = true; return db; @@ -488,10 +487,10 @@ public class DatabaseImpl implements Database final String mode = (readOnly ? RO_CHANNEL_MODE : RW_CHANNEL_MODE); return new RandomAccessFile(mdbFile, mode).getChannel(); } - + /** * Create a new database by reading it in from a FileChannel. - * @param file the File to which the channel is connected + * @param file the File to which the channel is connected * @param channel File channel of the database. This needs to be a * FileChannel instead of a ReadableByteChannel because we need to * randomly jump around to various points in the file. @@ -520,6 +519,7 @@ public class DatabaseImpl implements Database _columnOrder = getDefaultColumnOrder(); _enforceForeignKeys = getDefaultEnforceForeignKeys(); _allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); + _evaluateExpressions = getDefaultEvaluateExpressions(); _fileFormat = fileFormat; _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); @@ -555,7 +555,7 @@ public class DatabaseImpl implements Database public JetFormat getFormat() { return _format; } - + /** * @return The system catalog table * @usage _advanced_method_ @@ -563,7 +563,7 @@ public class DatabaseImpl implements Database public TableImpl getSystemCatalog() { return _systemCatalog; } - + /** * @return The system Access Control Entries table (loaded on demand) * @usage _advanced_method_ @@ -592,7 +592,7 @@ public class DatabaseImpl implements Database public void setErrorHandler(ErrorHandler newErrorHandler) { _dbErrorHandler = newErrorHandler; - } + } public LinkResolver getLinkResolver() { return((_linkResolver != null) ? _linkResolver : LinkResolver.DEFAULT); @@ -600,10 +600,10 @@ public class DatabaseImpl implements Database public void setLinkResolver(LinkResolver newLinkResolver) { _linkResolver = newLinkResolver; - } + } public Map<String,Database> getLinkedDatabases() { - return ((_linkedDbs == null) ? Collections.<String,Database>emptyMap() : + return ((_linkedDbs == null) ? Collections.<String,Database>emptyMap() : Collections.unmodifiableMap(_linkedDbs)); } @@ -625,7 +625,7 @@ public class DatabaseImpl implements Database // but, the local table name may not match the remote table name, so we // need to do a search if the common case fails return _tableFinder.isLinkedTable(table); - } + } private boolean matchesLinkedTable(Table table, String linkedTableName, String linkedDbName) { @@ -633,7 +633,7 @@ public class DatabaseImpl implements Database (_linkedDbs != null) && (_linkedDbs.get(linkedDbName) == table.getDatabase())); } - + public TimeZone getTimeZone() { return _timeZone; } @@ -645,7 +645,7 @@ public class DatabaseImpl implements Database _timeZone = newTimeZone; // clear cached calendar when timezone is changed _calendar = null; - } + } public Charset getCharset() { @@ -692,6 +692,16 @@ public class DatabaseImpl implements Database _allowAutoNumInsert = allowAutoNumInsert; } + public boolean isEvaluateExpressions() { + return _evaluateExpressions; + } + + public void setEvaluateExpressions(Boolean evaluateExpressions) { + if(evaluateExpressions == null) { + evaluateExpressions = getDefaultEvaluateExpressions(); + } + _evaluateExpressions = evaluateExpressions; + } public ColumnValidatorFactory getColumnValidatorFactory() { return _validatorFactory; @@ -703,7 +713,7 @@ public class DatabaseImpl implements Database } _validatorFactory = newFactory; } - + /** * @usage _advanced_method_ */ @@ -722,6 +732,32 @@ public class DatabaseImpl implements Database return _calendar; } + public EvalConfig getEvalConfig() { + return getEvalContext(); + } + + /** + * @usage _advanced_method_ + */ + DBEvalContext getEvalContext() { + if(_evalCtx == null) { + _evalCtx = new DBEvalContext(this); + } + return _evalCtx; + } + + /** + * 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 @@ -755,9 +791,9 @@ public class DatabaseImpl implements Database // no access version, fall back to "generic" accessVersion = null; } - + _fileFormat = possibleFileFormats.get(accessVersion); - + if(_fileFormat == null) { throw new IllegalStateException(withErrorContext( "Could not determine FileFormat")); @@ -796,7 +832,7 @@ public class DatabaseImpl implements Database // just need one shared buffer _buffer = buffer; } - + /** * @return the currently configured database default language sort order for * textual columns @@ -837,7 +873,7 @@ public class DatabaseImpl implements Database releaseSharedBuffer(buffer); } } - + /** * @return a PropertyMaps instance decoded from the given bytes (always * returns non-{@code null} result). @@ -845,11 +881,11 @@ public class DatabaseImpl implements Database */ public PropertyMaps readProperties(byte[] propsBytes, int objectId, RowIdImpl rowId) - throws IOException + throws IOException { - return getPropsHandler().read(propsBytes, objectId, rowId); + return getPropsHandler().read(propsBytes, objectId, rowId, null); } - + /** * Read the system catalog */ @@ -876,10 +912,10 @@ public class DatabaseImpl implements Database .toCursor()); } - _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID, + _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID, SYSTEM_OBJECT_NAME_TABLES); - if(_tableParentId == null) { + if(_tableParentId == null) { throw new IOException(withErrorContext( "Did not find required parent table id")); } @@ -889,7 +925,7 @@ public class DatabaseImpl implements Database "Finished reading system catalog. Tables: " + getTableNames())); } } - + public Set<String> getTableNames() throws IOException { if(_tableNames == null) { _tableNames = getTableNames(true, false, true); @@ -932,14 +968,14 @@ public class DatabaseImpl implements Database public TableIterableBuilder newIterable() { return new TableIterableBuilder(this); } - + public TableImpl getTable(String name) throws IOException { return getTable(name, false); } public TableMetaData getTableMetaData(String name) throws IOException { return getTableInfo(name, true); - } + } /** * @param tableDefPageNumber the page number of a table definition @@ -953,7 +989,7 @@ public class DatabaseImpl implements Database if(table != null) { return table; } - + // lookup table info from system catalog Row objectRow = _tableFinder.getObjectRow( tableDefPageNumber, SYSTEM_CATALOG_COLUMNS); @@ -972,19 +1008,19 @@ public class DatabaseImpl implements Database * @param includeSystemTables whether to consider returning a system table * @return The table, or null if it doesn't exist */ - protected TableImpl getTable(String name, boolean includeSystemTables) - throws IOException + protected TableImpl getTable(String name, boolean includeSystemTables) + throws IOException { TableInfo tableInfo = getTableInfo(name, includeSystemTables); - return ((tableInfo != null) ? + return ((tableInfo != null) ? getTable(tableInfo, includeSystemTables) : null); } - private TableInfo getTableInfo(String name, boolean includeSystemTables) - throws IOException + private TableInfo getTableInfo(String name, boolean includeSystemTables) + throws IOException { TableInfo tableInfo = lookupTable(name); - + if ((tableInfo == null) || (tableInfo.pageNumber == null)) { return null; } @@ -995,8 +1031,8 @@ public class DatabaseImpl implements Database return tableInfo; } - private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables) - throws IOException + private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables) + throws IOException { if(tableInfo.isLinked()) { @@ -1011,15 +1047,15 @@ public class DatabaseImpl implements Database linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName); _linkedDbs.put(linkedDbName, linkedDb); } - - return ((DatabaseImpl)linkedDb).getTable(linkedTableName, + + return ((DatabaseImpl)linkedDb).getTable(linkedTableName, includeSystemTables); } return readTable(tableInfo.tableName, tableInfo.pageNumber, tableInfo.flags); } - + /** * Create a new table in this database * @param name Name of the table to create @@ -1051,28 +1087,28 @@ public class DatabaseImpl implements Database .toTable(this); } - public void createLinkedTable(String name, String linkedDbName, + public void createLinkedTable(String name, String linkedDbName, String linkedTableName) throws IOException { if(lookupTable(name) != null) { throw new IllegalArgumentException(withErrorContext( - "Cannot create linked table with name of existing table '" + name + + "Cannot create linked table with name of existing table '" + name + "'")); } validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); - validateName(linkedDbName, DataType.MEMO.getMaxSize(), + validateName(linkedDbName, DataType.MEMO.getMaxSize(), "linked database"); - validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH, + validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH, "linked table"); getPageChannel().startWrite(); try { - + int linkedTableId = _tableFinder.getNextFreeSyntheticId(); - addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName, + addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName, linkedTableName); } finally { @@ -1083,16 +1119,16 @@ public class DatabaseImpl implements Database /** * Adds a newly created table to the relevant internal database structures. */ - void addNewTable(String name, int tdefPageNumber, Short type, - String linkedDbName, String linkedTableName) - throws IOException + void addNewTable(String name, int tdefPageNumber, Short type, + String linkedDbName, String linkedTableName) + throws IOException { //Add this table to our internal list. addTable(name, Integer.valueOf(tdefPageNumber), type, linkedDbName, linkedTableName); - + //Add this table to system tables - addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, + addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, linkedTableName, _tableParentId); addToAccessControlEntries(tdefPageNumber, _tableParentId, _newTableSIDs); } @@ -1120,7 +1156,7 @@ public class DatabaseImpl implements Database table1 = table2; table2 = tmp; } - + return getRelationshipsImpl(table1, table2, true); } @@ -1134,25 +1170,25 @@ public class DatabaseImpl implements Database // all tables return getRelationshipsImpl((TableImpl)table, null, true); } - + public List<Relationship> getRelationships() throws IOException { return getRelationshipsImpl(null, null, false); } - + public List<Relationship> getSystemRelationships() throws IOException { return getRelationshipsImpl(null, null, true); } - + private List<Relationship> getRelationshipsImpl( TableImpl table1, TableImpl table2, boolean includeSystemTables) throws IOException { initRelationships(); - + List<Relationship> relationships = new ArrayList<Relationship>(); if(table1 != null) { @@ -1168,15 +1204,15 @@ public class DatabaseImpl implements Database collectRelationships(new CursorBuilder(_relationships).toCursor(), null, null, relationships, includeSystemTables); } - + return relationships; } - RelationshipImpl writeRelationship(RelationshipCreator creator) + RelationshipImpl writeRelationship(RelationshipCreator creator) throws IOException { initRelationships(); - + String name = createRelationshipName(creator); RelationshipImpl newRel = creator.createRelationshipImpl(name); @@ -1206,13 +1242,13 @@ public class DatabaseImpl implements Database getPageChannel().startWrite(); try { - + int relObjId = _tableFinder.getNextFreeSyntheticId(); _relationships.addRows(rows); addToSystemCatalog(name, relObjId, TYPE_RELATIONSHIP, null, null, _relParentId); addToAccessControlEntries(relObjId, _relParentId, _newRelSIDs); - + } finally { getPageChannel().finishWrite(); } @@ -1224,14 +1260,14 @@ public class DatabaseImpl implements Database // the relationships table does not get loaded until first accessed if(_relationships == null) { // need the parent id of the relationships objects - _relParentId = _tableFinder.findObjectId(DB_PARENT_ID, + _relParentId = _tableFinder.findObjectId(DB_PARENT_ID, SYSTEM_OBJECT_NAME_RELATIONSHIPS); _relationships = getRequiredSystemTable(TABLE_SYSTEM_RELATIONSHIPS); } } private String createRelationshipName(RelationshipCreator creator) - throws IOException + throws IOException { // ensure that the final identifier name does not get too long // - the primary name is limited to ((max / 2) - 3) @@ -1253,7 +1289,7 @@ public class DatabaseImpl implements Database // now ensure name is unique Set<String> names = new HashSet<String>(); - + // collect the names of all relationships for uniqueness check for(Row row : CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames( @@ -1270,7 +1306,7 @@ public class DatabaseImpl implements Database // check those names as well for(Index idx : creator.getSecondaryTable().getIndexes()) { names.add(toLookupName(idx.getName())); - } + } } String baseName = toLookupName(origName); @@ -1282,7 +1318,7 @@ public class DatabaseImpl implements Database return ((i == 0) ? origName : (origName + i)); } - + public List<Query> getQueries() throws IOException { // the queries table does not get loaded until first accessed @@ -1292,7 +1328,7 @@ public class DatabaseImpl implements Database // find all the queries from the system catalog List<Row> queryInfo = new ArrayList<Row>(); - Map<Integer,List<QueryImpl.Row>> queryRowMap = + Map<Integer,List<QueryImpl.Row>> queryRowMap = new HashMap<Integer,List<QueryImpl.Row>>(); for(Row row : CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames( @@ -1340,10 +1376,10 @@ public class DatabaseImpl implements Database private TableImpl getRequiredSystemTable(String tableName) throws IOException { TableImpl table = getSystemTable(tableName); - if(table == null) { + if(table == null) { throw new IOException(withErrorContext( "Could not find system table " + tableName)); - } + } return table; } @@ -1372,26 +1408,21 @@ public class DatabaseImpl implements Database * @return the PropertyMaps for the object with the given id * @usage _advanced_method_ */ - public PropertyMaps getPropertiesForObject(int objectId) + public PropertyMaps getPropertiesForObject( + int objectId, PropertyMaps.Owner owner) throws IOException { - Row objectRow = _tableFinder.getObjectRow( - objectId, SYSTEM_CATALOG_PROPS_COLUMNS); - byte[] propsBytes = null; - RowIdImpl rowId = null; - if(objectRow != null) { - propsBytes = objectRow.getBytes(CAT_COL_PROPS); - rowId = (RowIdImpl)objectRow.getId(); - } - return readProperties(propsBytes, objectId, rowId); + return readProperties( + objectId, _tableFinder.getObjectRow( + objectId, SYSTEM_CATALOG_PROPS_COLUMNS), owner); } private Integer getDbParentId() throws IOException { if(_dbParentId == null) { // need the parent id of the databases objects - _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, + _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, SYSTEM_OBJECT_NAME_DATABASES); - if(_dbParentId == null) { + if(_dbParentId == null) { throw new IOException(withErrorContext( "Did not find required parent db id")); } @@ -1412,7 +1443,7 @@ public class DatabaseImpl implements Database if(msysDbRow != null) { owner = msysDbRow.getBytes(CAT_COL_OWNER); } - _newObjOwner = (((owner != null) && (owner.length > 0)) ? + _newObjOwner = (((owner != null) && (owner.length > 0)) ? owner : SYS_DEFAULT_SID); } return _newObjOwner; @@ -1424,17 +1455,23 @@ public class DatabaseImpl implements Database private PropertyMaps getPropertiesForDbObject(String dbName) throws IOException { - Row objectRow = _tableFinder.getObjectRow( - getDbParentId(), dbName, SYSTEM_CATALOG_PROPS_COLUMNS); + return readProperties( + -1, _tableFinder.getObjectRow( + getDbParentId(), dbName, SYSTEM_CATALOG_PROPS_COLUMNS), null); + } + + private PropertyMaps readProperties(int objectId, Row objectRow, + PropertyMaps.Owner owner) + throws IOException + { byte[] propsBytes = null; - int objectId = -1; RowIdImpl rowId = null; if(objectRow != null) { propsBytes = objectRow.getBytes(CAT_COL_PROPS); objectId = objectRow.getInt(CAT_COL_ID); rowId = (RowIdImpl)objectRow.getId(); } - return readProperties(propsBytes, objectId, rowId); + return getPropsHandler().read(propsBytes, objectId, rowId, owner); } public String getDatabasePassword() throws IOException @@ -1456,7 +1493,7 @@ public class DatabaseImpl implements Database pwdBytes[i] ^= pwdMask[i % pwdMask.length]; } } - + boolean hasPassword = false; for(int i = 0; i < pwdBytes.length; ++i) { if(pwdBytes[i] != 0) { @@ -1498,14 +1535,14 @@ public class DatabaseImpl implements Database for(Row row : cursor) { String fromName = row.getString(REL_COL_FROM_TABLE); String toName = row.getString(REL_COL_TO_TABLE); - - if(((fromTableName == null) || + + if(((fromTableName == null) || fromTableName.equalsIgnoreCase(fromName)) && - ((toTableName == null) || + ((toTableName == null) || toTableName.equalsIgnoreCase(toName))) { String relName = row.getString(REL_COL_NAME); - + // found more info for a relationship. see if we already have some // info for this relationship Relationship rel = null; @@ -1552,15 +1589,15 @@ public class DatabaseImpl implements Database rel.getFromColumns().set(colIdx, fromCol); rel.getToColumns().set(colIdx, toCol); } - } + } } - + /** * Add a new table to the system catalog * @param name Table name * @param objectId id of the new object */ - private void addToSystemCatalog(String name, int objectId, Short type, + private void addToSystemCatalog(String name, int objectId, Short type, String linkedDbName, String linkedTableName, Integer parentId) throws IOException @@ -1601,8 +1638,8 @@ public class DatabaseImpl implements Database * Adds a new object to the system's access control entries */ private void addToAccessControlEntries( - Integer objectId, Integer parentId, List<byte[]> sids) - throws IOException + Integer objectId, Integer parentId, List<byte[]> sids) + throws IOException { if(sids.isEmpty()) { collectNewObjectSIDs(parentId, sids); @@ -1624,20 +1661,20 @@ public class DatabaseImpl implements Database sidCol.setRowValue(aceRow, sid); aceRows.add(aceRow); } - acEntries.addRows(aceRows); + acEntries.addRows(aceRows); } /** * Find collection of SIDs for the given parent id. */ - private void collectNewObjectSIDs(Integer parentId, List<byte[]> sids) + private void collectNewObjectSIDs(Integer parentId, List<byte[]> sids) throws IOException { // search for ACEs matching the given parentId. use the index on the // objectId column if found (should be there) Cursor cursor = createCursorWithOptionalIndex( getAccessControlEntries(), ACE_COL_OBJECT_ID, parentId); - + for(Row row : cursor) { Integer objId = row.getInt(ACE_COL_OBJECT_ID); if(parentId.equals(objId)) { @@ -1662,7 +1699,7 @@ public class DatabaseImpl implements Database if(table != null) { return table; } - + ByteBuffer buffer = takeSharedBuffer(); try { // need to load table from db @@ -1697,12 +1734,12 @@ public class DatabaseImpl implements Database if(LOG.isDebugEnabled()) { LOG.debug(withErrorContext( "Could not find expected index on table " + table.getName())); - } + } } // use table scan instead return CursorImpl.createCursor(table); } - + public void flush() throws IOException { if(_linkedDbs != null) { for(Database linkedDb : _linkedDbs.values()) { @@ -1711,7 +1748,7 @@ public class DatabaseImpl implements Database } _pageChannel.flush(); } - + public void close() throws IOException { if(_linkedDbs != null) { for(Database linkedDb : _linkedDbs.values()) { @@ -1729,7 +1766,7 @@ public class DatabaseImpl implements Database "Cannot create table with name of existing table '" + name + "'")); } } - + /** * Validates an identifier name. * @@ -1741,7 +1778,7 @@ public class DatabaseImpl implements Database * <li>Can't begin with leading spaces.</li> * <li>Can't include control characters (ASCII values 0 through 31).</li> * </ul> - * + * * @usage _advanced_method_ */ public static void validateIdentifierName(String name, @@ -1788,7 +1825,19 @@ public class DatabaseImpl implements Database public static boolean isBlank(String name) { return((name == null) || (name.trim().length() == 0)); } - + + /** + * Returns the given string trimmed, or {@code null} if the string is {@code + * null} or empty. + */ + public static String trimToNull(String str) { + if(str == null) { + return null; + } + str = str.trim(); + return((str.length() > 0) ? str : null); + } + @Override public String toString() { return ToStringBuilder.reflectionToString(this); @@ -1797,11 +1846,11 @@ public class DatabaseImpl implements Database /** * Adds a table to the _tableLookup and resets the _tableNames set */ - private void addTable(String tableName, Integer pageNumber, Short type, + private void addTable(String tableName, Integer pageNumber, Short type, String linkedDbName, String linkedTableName) { _tableLookup.put(toLookupName(tableName), - createTableInfo(tableName, pageNumber, 0, type, + createTableInfo(tableName, pageNumber, 0, type, linkedDbName, linkedTableName)); // clear this, will be created next time needed _tableNames = null; @@ -1811,7 +1860,7 @@ public class DatabaseImpl implements Database * Creates a TableInfo instance appropriate for the given table data. */ private static TableInfo createTableInfo( - String tableName, Integer pageNumber, int flags, Short type, + String tableName, Integer pageNumber, int flags, Short type, String linkedDbName, String linkedTableName) { if(TYPE_LINKED_TABLE.equals(type)) { @@ -1877,7 +1926,7 @@ public class DatabaseImpl implements Database // use system default return TimeZone.getDefault(); } - + /** * Returns the default Charset for the given JetFormat. This may or may not * be platform specific, depending on the format, but can be overridden @@ -1900,7 +1949,7 @@ public class DatabaseImpl implements Database // use format default return format.CHARSET; } - + /** * Returns the default Table.ColumnOrder. This defaults to * {@link Database#DEFAULT_COLUMN_ORDER}, but can be overridden using the system @@ -1920,7 +1969,7 @@ public class DatabaseImpl implements Database // use default order return DEFAULT_COLUMN_ORDER; } - + /** * Returns the default enforce foreign-keys policy. This defaults to * {@code true}, but can be overridden using the system @@ -1935,7 +1984,7 @@ public class DatabaseImpl implements Database } return true; } - + /** * Returns the default allow auto number insert policy. This defaults to * {@code false}, but can be overridden using the system @@ -1950,7 +1999,22 @@ public class DatabaseImpl implements Database } return false; } - + + /** + * Returns the default enable expression evaluation policy. This defaults to + * {@code false}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#ENABLE_EXPRESSION_EVALUATION_PROPERTY}. + * @usage _advanced_method_ + */ + public static boolean getDefaultEvaluateExpressions() + { + String prop = System.getProperty(ENABLE_EXPRESSION_EVALUATION_PROPERTY); + if(prop != null) { + return Boolean.TRUE.toString().equalsIgnoreCase(prop); + } + return false; + } + /** * Copies the given db InputStream to the given channel using the most * efficient means possible. @@ -1961,7 +2025,7 @@ public class DatabaseImpl implements Database ReadableByteChannel readChannel = Channels.newChannel(in); if(!BROKEN_NIO) { // sane implementation - channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE); + channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE); } else { // do things the hard way for broken vms ByteBuffer bb = ByteBuffer.allocate(8096); @@ -2000,12 +2064,12 @@ public class DatabaseImpl implements Database { InputStream stream = DatabaseImpl.class.getClassLoader() .getResourceAsStream(resourceName); - + if(stream == null) { - + stream = Thread.currentThread().getContextClassLoader() .getResourceAsStream(resourceName); - + if(stream == null) { throw new IOException("Could not load jackcess resource " + resourceName); @@ -2026,8 +2090,8 @@ public class DatabaseImpl implements Database private static void addFileFormatDetails( FileFormat fileFormat, String emptyFileName, JetFormat format) { - String emptyFile = - ((emptyFileName != null) ? + String emptyFile = + ((emptyFileName != null) ? RESOURCE_PATH + emptyFileName + fileFormat.getFileExtension() : null); FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format)); } @@ -2035,7 +2099,7 @@ public class DatabaseImpl implements Database private static String getName(File file) { if(file == null) { return "<UNKNOWN.DB>"; - } + } return file.getName(); } @@ -2065,14 +2129,14 @@ public class DatabaseImpl implements Database public String getName() { return tableName; } - + public boolean isLinked() { return false; } public boolean isSystem() { return isSystemObject(flags); - } + } public String getLinkedTableName() { return null; @@ -2110,8 +2174,8 @@ public class DatabaseImpl implements Database private final String linkedDbName; private final String linkedTableName; - private LinkedTableInfo(Integer newPageNumber, String newTableName, - int newFlags, String newLinkedDbName, + private LinkedTableInfo(Integer newPageNumber, String newTableName, + int newFlags, String newLinkedDbName, String newLinkedTableName) { super(newPageNumber, newTableName, newFlags); linkedDbName = newLinkedDbName; @@ -2170,11 +2234,11 @@ public class DatabaseImpl implements Database */ private abstract class TableFinder { - public Integer findObjectId(Integer parentId, String name) - throws IOException + public Integer findObjectId(Integer parentId, String name) + throws IOException { Cursor cur = findRow(parentId, name); - if(cur == null) { + if(cur == null) { return null; } ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); @@ -2182,8 +2246,8 @@ public class DatabaseImpl implements Database } public Row getObjectRow(Integer parentId, String name, - Collection<String> columns) - throws IOException + Collection<String> columns) + throws IOException { Cursor cur = findRow(parentId, name); return ((cur != null) ? cur.getCurrentRow(columns) : null); @@ -2240,7 +2304,7 @@ public class DatabaseImpl implements Database if(TYPE_LINKED_TABLE.equals(type) && matchesLinkedTable(table, linkedTableName, linkedDbName)) { return true; - } + } } return false; } @@ -2248,7 +2312,7 @@ public class DatabaseImpl implements Database protected abstract Cursor findRow(Integer parentId, String name) throws IOException; - protected abstract Cursor findRow(Integer objectId) + protected abstract Cursor findRow(Integer objectId) throws IOException; protected abstract Cursor getTableNamesCursor() throws IOException; @@ -2281,7 +2345,7 @@ public class DatabaseImpl implements Database private DefaultTableFinder(IndexCursor systemCatalogCursor) { _systemCatalogCursor = systemCatalogCursor; } - + private void initIdCursor() throws IOException { if(_systemCatalogIdCursor == null) { _systemCatalogIdCursor = _systemCatalog.newCursor() @@ -2291,15 +2355,15 @@ public class DatabaseImpl implements Database } @Override - protected Cursor findRow(Integer parentId, String name) - throws IOException + protected Cursor findRow(Integer parentId, String name) + throws IOException { return (_systemCatalogCursor.findFirstRowByEntry(parentId, name) ? _systemCatalogCursor : null); } @Override - protected Cursor findRow(Integer objectId) throws IOException + protected Cursor findRow(Integer objectId) throws IOException { initIdCursor(); return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ? @@ -2330,7 +2394,7 @@ public class DatabaseImpl implements Database return createTableInfo(realName, pageNumber, flags, type, linkedDbName, linkedTableName); } - + @Override protected Cursor getTableNamesCursor() throws IOException { return _systemCatalogCursor.getIndex().newCursor() @@ -2354,7 +2418,7 @@ public class DatabaseImpl implements Database return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol); } } - + /** * Fallback table lookup handler, using catalog table scans. */ @@ -2367,18 +2431,18 @@ public class DatabaseImpl implements Database } @Override - protected Cursor findRow(Integer parentId, String name) - throws IOException + protected Cursor findRow(Integer parentId, String name) + throws IOException { Map<String,Object> rowPat = new HashMap<String,Object>(); - rowPat.put(CAT_COL_PARENT_ID, parentId); + rowPat.put(CAT_COL_PARENT_ID, parentId); rowPat.put(CAT_COL_NAME, name); return (_systemCatalogCursor.findFirstRow(rowPat) ? _systemCatalogCursor : null); } @Override - protected Cursor findRow(Integer objectId) throws IOException + protected Cursor findRow(Integer objectId) throws IOException { ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (_systemCatalogCursor.findFirstRow(idCol, objectId) ? @@ -2417,7 +2481,7 @@ public class DatabaseImpl implements Database return null; } - + @Override protected Cursor getTableNamesCursor() throws IOException { return _systemCatalogCursor; @@ -2447,7 +2511,7 @@ public class DatabaseImpl implements Database { private final Integer _pageNumber; - private WeakTableReference(Integer pageNumber, TableImpl table, + private WeakTableReference(Integer pageNumber, TableImpl table, ReferenceQueue<TableImpl> queue) { super(table, queue); _pageNumber = pageNumber; @@ -2463,9 +2527,9 @@ public class DatabaseImpl implements Database */ private static final class TableCache { - private final Map<Integer,WeakTableReference> _tables = + private final Map<Integer,WeakTableReference> _tables = new HashMap<Integer,WeakTableReference>(); - private final ReferenceQueue<TableImpl> _queue = + private final ReferenceQueue<TableImpl> _queue = new ReferenceQueue<TableImpl>(); public TableImpl get(Integer pageNumber) { @@ -2475,7 +2539,7 @@ public class DatabaseImpl implements Database public TableImpl put(TableImpl table) { purgeOldRefs(); - + Integer pageNumber = table.getTableDefPageNumber(); WeakTableReference ref = new WeakTableReference( pageNumber, table, _queue); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java b/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java new file mode 100644 index 0000000..3d4dab9 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java @@ -0,0 +1,81 @@ +/* +Copyright (c) 2018 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; + +import java.io.IOException; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.util.ColumnValidator; +import com.healthmarketscience.jackcess.util.SimpleColumnValidator; + +/** + * Base class for ColumnValidator instances handling "internal" validation + * functionality, which are wrappers around any "external" behavior. + * + * @author James Ahlborn + */ +abstract class InternalColumnValidator implements ColumnValidator +{ + private ColumnValidator _delegate; + + protected InternalColumnValidator(ColumnValidator delegate) + { + _delegate = delegate; + } + + ColumnValidator getExternal() { + ColumnValidator extValidator = _delegate; + while(extValidator instanceof InternalColumnValidator) { + extValidator = ((InternalColumnValidator)extValidator)._delegate; + } + return extValidator; + } + + void setExternal(ColumnValidator extValidator) { + InternalColumnValidator intValidator = this; + while(intValidator._delegate instanceof InternalColumnValidator) { + intValidator = (InternalColumnValidator)intValidator._delegate; + } + intValidator._delegate = extValidator; + } + + public final Object validate(Column col, Object val) throws IOException { + val = _delegate.validate(col, val); + return internalValidate(col, val); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("{"); + if(_delegate instanceof InternalColumnValidator) { + ((InternalColumnValidator)_delegate).appendToString(sb); + } else if(_delegate != SimpleColumnValidator.INSTANCE) { + sb.append("custom=").append(_delegate); + } + if(sb.length() > 1) { + sb.append(";"); + } + appendToString(sb); + sb.append("}"); + return sb.toString(); + } + + protected abstract void appendToString(StringBuilder sb); + + protected abstract Object internalValidate(Column col, Object val) + throws IOException; +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java index 73648b3..f878562 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java @@ -17,19 +17,19 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collection; +import com.healthmarketscience.jackcess.InvalidValueException; /** * ColumnImpl subclass which is used for long value data types. - * + * * @author James Ahlborn * @usage _advanced_class_ */ -class LongValueColumnImpl extends ColumnImpl +class LongValueColumnImpl extends ColumnImpl { /** * Long value (LVAL) type that indicates that the value is stored on the @@ -60,12 +60,12 @@ class LongValueColumnImpl extends ColumnImpl { super(args); } - + @Override public int getOwnedPageCount() { return ((_lvalBufferH == null) ? 0 : _lvalBufferH.getOwnedPageCount()); } - + @Override void setUsageMaps(UsageMap ownedPages, UsageMap freeSpacePages) { _lvalBufferH = new UmapLongValueBufferHolder(ownedPages, freeSpacePages); @@ -75,7 +75,7 @@ class LongValueColumnImpl extends ColumnImpl void collectUsageMapPages(Collection<Integer> pages) { _lvalBufferH.collectUsageMapPages(pages); } - + @Override void postTableLoadInit() throws IOException { if(_lvalBufferH == null) { @@ -104,7 +104,7 @@ class LongValueColumnImpl extends ColumnImpl default: throw new RuntimeException(withErrorContext( "unexpected var length, long value type: " + getType())); - } + } } @Override @@ -122,12 +122,12 @@ class LongValueColumnImpl extends ColumnImpl default: throw new RuntimeException(withErrorContext( "unexpected var length, long value type: " + getType())); - } + } // create long value buffer return writeLongValue(toByteArray(obj), remainingRowLength); - } - + } + /** * @param lvalDefinition Column value that points to an LVAL record * @return The LVAL data @@ -152,7 +152,7 @@ class LongValueColumnImpl extends ColumnImpl if(rowLen < length) { // warn the caller, but return whatever we can LOG.warn(withErrorContext( - "Value may be truncated: expected length " + + "Value may be truncated: expected length " + length + " found " + rowLen)); rtn = new byte[rowLen]; } @@ -172,7 +172,7 @@ class LongValueColumnImpl extends ColumnImpl int rowNum = ByteUtil.getUnsignedByte(def); int pageNum = ByteUtil.get3ByteInt(def, def.position()); ByteBuffer lvalPage = getPageChannel().createPageBuffer(); - + switch (type) { case LONG_VALUE_TYPE_OTHER_PAGE: { @@ -185,16 +185,16 @@ class LongValueColumnImpl extends ColumnImpl if(rowLen < length) { // warn the caller, but return whatever we can LOG.warn(withErrorContext( - "Value may be truncated: expected length " + + "Value may be truncated: expected length " + length + " found " + rowLen)); rtn = new byte[rowLen]; } - + lvalPage.position(rowStart); lvalPage.get(rtn); } break; - + case LONG_VALUE_TYPE_OTHER_PAGES: ByteBuffer rtnBuf = ByteBuffer.wrap(rtn); @@ -205,7 +205,7 @@ class LongValueColumnImpl extends ColumnImpl short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat()); - + // read next page information lvalPage.position(rowStart); rowNum = ByteUtil.getUnsignedByte(lvalPage); @@ -218,22 +218,22 @@ class LongValueColumnImpl extends ColumnImpl chunkLength = remainingLen; } remainingLen -= chunkLength; - + lvalPage.limit(rowEnd); rtnBuf.put(lvalPage); } - + break; - + default: throw new IOException(withErrorContext( "Unrecognized long value type: " + type)); } } - + return rtn; } - + /** * @param lvalDefinition Column value that points to an LVAL record * @return The LVAL data @@ -259,11 +259,11 @@ class LongValueColumnImpl extends ColumnImpl * value (unless written to other pages) * @usage _advanced_method_ */ - protected ByteBuffer writeLongValue(byte[] value, int remainingRowLength) + protected ByteBuffer writeLongValue(byte[] value, int remainingRowLength) throws IOException { if(value.length > getType().getMaxSize()) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "value too big for column, max " + getType().getMaxSize() + ", got " + value.length)); } @@ -292,11 +292,11 @@ class LongValueColumnImpl extends ColumnImpl def.putInt(0); //Unknown def.put(value); } else { - + ByteBuffer lvalPage = null; int firstLvalPageNum = PageChannel.INVALID_PAGE_NUMBER; byte firstLvalRow = 0; - + // write other page(s) switch(type) { case LONG_VALUE_TYPE_OTHER_PAGE: @@ -335,7 +335,7 @@ class LongValueColumnImpl extends ColumnImpl nextLvalPage = _lvalBufferH.getLongValuePage( (remainingLen - chunkLength) + 4); nextLvalPageNum = _lvalBufferH.getPageNumber(); - nextLvalRowNum = TableImpl.getRowsOnDataPage(nextLvalPage, + nextLvalRowNum = TableImpl.getRowsOnDataPage(nextLvalPage, getFormat()); } else { nextLvalPage = null; @@ -345,7 +345,7 @@ class LongValueColumnImpl extends ColumnImpl // add row to this page TableImpl.addDataPageRow(lvalPage, chunkLength + 4, getFormat(), 0); - + // write next page info lvalPage.put((byte)nextLvalRowNum); // row number ByteUtil.put3ByteInt(lvalPage, nextLvalPageNum); // page number @@ -373,9 +373,9 @@ class LongValueColumnImpl extends ColumnImpl def.put(firstLvalRow); ByteUtil.put3ByteInt(def, firstLvalPageNum); def.putInt(0); //Unknown - + } - + def.flip(); return def; } @@ -499,10 +499,10 @@ class LongValueColumnImpl extends ColumnImpl @Override protected ByteBuffer findNewPage(int dataLength) throws IOException { - // grab last owned page and check for free space. - ByteBuffer newPage = TableImpl.findFreeRowSpace( + // grab last owned page and check for free space. + ByteBuffer newPage = TableImpl.findFreeRowSpace( _ownedPages, _freeSpacePages, _longValueBufferH); - + if(newPage != null) { if(TableImpl.rowFitsOnDataPage(dataLength, newPage, getFormat())) { return newPage; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java b/src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java new file mode 100644 index 0000000..358a0a6 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java @@ -0,0 +1,178 @@ +/* +Copyright (c) 2018 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; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParsePosition; + +/** + * + * @author James Ahlborn + */ +public class NumberFormatter +{ + public static final RoundingMode ROUND_MODE = RoundingMode.HALF_EVEN; + + private static final int FLT_SIG_DIGITS = 7; + private static final int DBL_SIG_DIGITS = 15; + private static final int DEC_SIG_DIGITS = 28; + + public static final MathContext FLT_MATH_CONTEXT = + new MathContext(FLT_SIG_DIGITS, ROUND_MODE); + public static final MathContext DBL_MATH_CONTEXT = + new MathContext(DBL_SIG_DIGITS, ROUND_MODE); + public static final MathContext DEC_MATH_CONTEXT = + new MathContext(DEC_SIG_DIGITS, ROUND_MODE); + + // note, java doesn't distinguish between pos/neg NaN + private static final String NAN_STR = "1.#QNAN"; + private static final String POS_INF_STR = "1.#INF"; + private static final String NEG_INf_STR = "-1.#INF"; + + private static final ThreadLocal<NumberFormatter> INSTANCE = + new ThreadLocal<NumberFormatter>() { + @Override + protected NumberFormatter initialValue() { + return new NumberFormatter(); + } + }; + + private final TypeFormatter _fltFmt = new TypeFormatter(FLT_SIG_DIGITS); + private final TypeFormatter _dblFmt = new TypeFormatter(DBL_SIG_DIGITS); + private final TypeFormatter _decFmt = new TypeFormatter(DEC_SIG_DIGITS); + + private NumberFormatter() {} + + public static String format(float f) { + return INSTANCE.get().formatImpl(f); + } + + public static String format(double d) { + return INSTANCE.get().formatImpl(d); + } + + public static String format(BigDecimal bd) { + return INSTANCE.get().formatImpl(bd); + } + + private String formatImpl(float f) { + + if(Float.isNaN(f)) { + return NAN_STR; + } + if(Float.isInfinite(f)) { + return ((f < 0f) ? NEG_INf_STR : POS_INF_STR); + } + + return _fltFmt.format(new BigDecimal(f, FLT_MATH_CONTEXT)); + } + + private String formatImpl(double d) { + + if(Double.isNaN(d)) { + return NAN_STR; + } + if(Double.isInfinite(d)) { + return ((d < 0d) ? NEG_INf_STR : POS_INF_STR); + } + + return _dblFmt.format(new BigDecimal(d, DBL_MATH_CONTEXT)); + } + + private String formatImpl(BigDecimal bd) { + return _decFmt.format(bd.round(DEC_MATH_CONTEXT)); + } + + private static final class TypeFormatter + { + private final DecimalFormat _df = new DecimalFormat("0.#"); + private final BetterDecimalFormat _dfS; + private final int _prec; + + private TypeFormatter(int prec) { + _prec = prec; + _df.setMaximumIntegerDigits(prec); + _df.setMaximumFractionDigits(prec); + _df.setRoundingMode(ROUND_MODE); + _dfS = new BetterDecimalFormat("0.#E00", prec); + } + + public String format(BigDecimal bd) { + bd = bd.stripTrailingZeros(); + int prec = bd.precision(); + int scale = bd.scale(); + + int sigDigits = prec; + if(scale < 0) { + sigDigits -= scale; + } else if(scale > prec) { + sigDigits += (scale - prec); + } + + return ((sigDigits > _prec) ? _dfS.format(bd) : _df.format(bd)); + } + } + + private static final class BetterDecimalFormat extends NumberFormat + { + private static final long serialVersionUID = 0L; + + private final DecimalFormat _df; + + private BetterDecimalFormat(String pat, int prec) { + super(); + _df = new DecimalFormat(pat); + _df.setMaximumIntegerDigits(1); + _df.setMaximumFractionDigits(prec); + _df.setRoundingMode(ROUND_MODE); + } + + @Override + public StringBuffer format(Object number, StringBuffer toAppendTo, + FieldPosition pos) + { + StringBuffer sb = _df.format(number, toAppendTo, pos); + int idx = sb.lastIndexOf("E"); + if(sb.charAt(idx + 1) != '-') { + sb.insert(idx + 1, '+'); + } + return sb; + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, + FieldPosition pos) { + throw new UnsupportedOperationException(); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, + FieldPosition pos) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java index 4ac9e9a..61e1e07 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java @@ -46,16 +46,19 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> /** maps the PropertyMap name (case-insensitive) to the PropertyMap instance */ - private final Map<String,PropertyMapImpl> _maps = + private final Map<String,PropertyMapImpl> _maps = new LinkedHashMap<String,PropertyMapImpl>(); private final int _objectId; private final RowIdImpl _rowId; private final Handler _handler; + private final Owner _owner; - public PropertyMaps(int objectId, RowIdImpl rowId, Handler handler) { + public PropertyMaps(int objectId, RowIdImpl rowId, Handler handler, + Owner owner) { _objectId = objectId; _rowId = rowId; _handler = handler; + _owner = owner; } public int getObjectId() { @@ -110,6 +113,9 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> public void save() throws IOException { _handler.save(this); + if(_owner != null) { + _owner.propertiesUpdated(); + } } @Override @@ -119,6 +125,12 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> .toString(); } + public static String getTrimmedStringProperty( + PropertyMap props, String propName) + { + return DatabaseImpl.trimToNull((String)props.getValue(propName)); + } + /** * Utility class for reading/writing property blocks. */ @@ -129,7 +141,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> /** the system table "property" column */ private final ColumnImpl _propCol; /** cache of PropColumns used to read/write property values */ - private final Map<DataType,PropColumn> _columns = + private final Map<DataType,PropColumn> _columns = new HashMap<DataType,PropColumn>(); Handler(DatabaseImpl database) { @@ -142,11 +154,11 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> * @return a PropertyMaps instance decoded from the given bytes (always * returns non-{@code null} result). */ - public PropertyMaps read(byte[] propBytes, int objectId, - RowIdImpl rowId) - throws IOException + public PropertyMaps read(byte[] propBytes, int objectId, + RowIdImpl rowId, Owner owner) + throws IOException { - PropertyMaps maps = new PropertyMaps(objectId, rowId, this); + PropertyMaps maps = new PropertyMaps(objectId, rowId, this, owner); if((propBytes == null) || (propBytes.length == 0)) { return maps; } @@ -176,7 +188,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> short type = bb.getShort(); int endPos = bb.position() + len - 6; - ByteBuffer bbBlock = PageChannel.narrowBuffer(bb, bb.position(), + ByteBuffer bbBlock = PageChannel.narrowBuffer(bb, bb.position(), endPos); if(type == PROPERTY_NAME_LIST) { @@ -226,7 +238,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> writeBlock(propMap, propNames, propMap.getType(), bab); } } - + return bab.toArray(); } @@ -260,12 +272,12 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> writePropertyNames(propNames, bab); } else { writePropertyValues(propMap, propNames, bab); - } + } int len = bab.position() - blockStartPos; bab.putInt(blockStartPos, len); } - + /** * @return the property names parsed from the given data chunk */ @@ -281,7 +293,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> ByteArrayBuilder bab) { for(String propName : propNames) { writePropName(propName, bab); - } + } } /** @@ -290,7 +302,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> */ private PropertyMapImpl readPropertyValues( ByteBuffer bbBlock, List<String> propNames, short blockType, - PropertyMaps maps) + PropertyMaps maps) throws IOException { String mapName = DEFAULT_NAME; @@ -305,13 +317,13 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> } bbBlock.position(endPos); } - + PropertyMapImpl map = maps.get(mapName, blockType); // read the values while(bbBlock.hasRemaining()) { - int valLen = bbBlock.getShort(); + int valLen = bbBlock.getShort(); int endPos = bbBlock.position() + valLen - 2; boolean isDdl = (bbBlock.get() != 0); DataType dataType = DataType.fromByte(bbBlock.get()); @@ -333,9 +345,9 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> } private void writePropertyValues( - PropertyMapImpl propMap, Set<String> propNames, ByteArrayBuilder bab) + PropertyMapImpl propMap, Set<String> propNames, ByteArrayBuilder bab) throws IOException - { + { // write the map name, if any String mapName = propMap.getName(); int blockStartPos = bab.position(); @@ -384,7 +396,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> /** * Reads a property name from the given data block */ - private String readPropName(ByteBuffer buffer) { + private String readPropName(ByteBuffer buffer) { int nameLength = buffer.getShort(); byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); return ColumnImpl.decodeUncompressedText(nameBytes, _database.getCharset()); @@ -404,8 +416,8 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> * Gets a PropColumn capable of reading/writing a property of the given * DataType */ - private PropColumn getColumn(DataType dataType, String propName, - int dataSize, Object value) + private PropColumn getColumn(DataType dataType, String propName, + int dataSize, Object value) throws IOException { @@ -426,7 +438,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> } // create column with ability to read/write the given data type - col = ((colType == DataType.BOOLEAN) ? + col = ((colType == DataType.BOOLEAN) ? new BooleanPropColumn() : new PropColumn(colType)); _columns.put(dataType, col); @@ -436,11 +448,11 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> } private static boolean isPseudoGuidColumn( - DataType dataType, String propName, int dataSize, Object value) + DataType dataType, String propName, int dataSize, Object value) throws IOException { // guids seem to be marked as "binary" fields - return((dataType == DataType.BINARY) && + return((dataType == DataType.BINARY) && ((dataSize == DataType.GUID.getFixedSize()) || ((dataSize == -1) && ColumnImpl.isGUIDValue(value))) && PropertyMap.GUID_PROP.equalsIgnoreCase(propName)); @@ -454,7 +466,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> private PropColumn(DataType type) { super(null, null, type, 0, 0, 0); } - + @Override public DatabaseImpl getDatabase() { return _database; @@ -487,4 +499,15 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> } } } + + /** + * Utility interface for the object which owns the PropertyMaps + */ + static interface Owner { + + /** + * Invoked when new properties are saved. + */ + public void propertiesUpdated() throws IOException; + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowEvalContext.java new file mode 100644 index 0000000..8489ffe --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowEvalContext.java @@ -0,0 +1,64 @@ +/* +Copyright (c) 2018 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; + +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Identifier; +import com.healthmarketscience.jackcess.expr.Value; + +/** + * + * @author James Ahlborn + */ +public abstract class RowEvalContext extends BaseEvalContext +{ + private Object[] _row; + + public RowEvalContext(DatabaseImpl db) { + super(db.getEvalContext()); + } + + protected void setRow(Object[] row) { + _row = row; + } + + protected void reset() { + _row = null; + } + + @Override + public Value getIdentifierValue(Identifier identifier) { + + TableImpl table = getTable(); + + // we only support getting column values in this table from the current + // row + if(!table.isThisTable(identifier) || + (identifier.getPropertyName() != null)) { + throw new EvalException("Cannot access fields outside this table for " + + identifier); + } + + ColumnImpl col = table.getColumn(identifier.getObjectName()); + + Object val = col.getRowValue(_row); + + return toValue(val, col.getType()); + } + + protected abstract TableImpl getTable(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowValidatorEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowValidatorEvalContext.java new file mode 100644 index 0000000..07b54f0 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowValidatorEvalContext.java @@ -0,0 +1,66 @@ +/* +Copyright (c) 2018 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; + +import java.io.IOException; + +import com.healthmarketscience.jackcess.InvalidValueException; +import com.healthmarketscience.jackcess.impl.expr.Expressionator; + +/** + * + * @author James Ahlborn + */ +public class RowValidatorEvalContext extends RowEvalContext +{ + private final TableImpl _table; + private String _helpStr; + + public RowValidatorEvalContext(TableImpl table) { + super(table.getDatabase()); + _table = table; + } + + RowValidatorEvalContext setExpr(String exprStr, String helpStr) { + setExpr(Expressionator.Type.RECORD_VALIDATOR, exprStr); + _helpStr = helpStr; + return this; + } + + @Override + protected TableImpl getTable() { + return _table; + } + + public void validate(Object[] row) throws IOException { + try { + setRow(row); + Boolean result = (Boolean)eval(); + if(!result) { + String msg = ((_helpStr != null) ? _helpStr : "Invalid row"); + throw new InvalidValueException(withErrorContext(msg)); + } + } finally { + reset(); + } + } + + @Override + protected String withErrorContext(String msg) { + return _table.withErrorContext(msg); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/SimpleCache.java b/src/main/java/com/healthmarketscience/jackcess/impl/SimpleCache.java new file mode 100644 index 0000000..fef2f68 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/SimpleCache.java @@ -0,0 +1,46 @@ +/* +Copyright (c) 2018 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; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Simple LRU cache implementation which keeps at most the configured maximum + * number of elements. + * @author James Ahlborn + */ +public class SimpleCache<K,V> extends LinkedHashMap<K,V> +{ + private static final long serialVersionUID = 20180313L; + + private final int _maxSize; + + public SimpleCache(int maxSize) { + super(16, 0.75f, true); + _maxSize = maxSize; + } + + protected int getMaxSize() { + return _maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> e) { + return(size() > _maxSize); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 74c27f5..15a0c8c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -43,13 +43,16 @@ import com.healthmarketscience.jackcess.ConstraintViolationException; import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; +import com.healthmarketscience.jackcess.InvalidValueException; import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.PropertyMap; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.RowId; import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.util.ErrorHandler; import com.healthmarketscience.jackcess.util.ExportUtil; +import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -61,7 +64,7 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class TableImpl implements Table +public class TableImpl implements Table, PropertyMaps.Owner { private static final Log LOG = LogFactory.getLog(TableImpl.class); @@ -133,6 +136,8 @@ public class TableImpl implements Table private final List<ColumnImpl> _varColumns = new ArrayList<ColumnImpl>(); /** List of autonumber columns in this table, ordered by column number */ private final List<ColumnImpl> _autoNumColumns = new ArrayList<ColumnImpl>(1); + /** handler for calculated columns */ + private final CalcColEvaluator _calcColEval = new CalcColEvaluator(); /** List of indexes on this table (multiple logical indexes may be backed by the same index data) */ private final List<IndexImpl> _indexes = new ArrayList<IndexImpl>(); @@ -178,6 +183,8 @@ public class TableImpl implements Table private Boolean _allowAutoNumInsert; /** foreign-key enforcer for this table */ private final FKEnforcer _fkEnforcer; + /** table validator if any (and enabled) */ + private RowValidatorEvalContext _rowValidator; /** default cursor for iterating through the table, kept here for basic table traversal */ @@ -280,11 +287,36 @@ public class TableImpl implements Table _fkEnforcer = new FKEnforcer(this); if(!isSystem()) { - // after fully constructed, allow column validator to be configured (but - // only for user tables) + // after fully constructed, allow column/row validators to be configured + // (but only for user tables) for(ColumnImpl col : _columns) { - col.setColumnValidator(null); + col.initColumnValidator(); } + + reloadRowValidator(); + } + } + + private void reloadRowValidator() throws IOException { + + // reset table row validator before proceeding + _rowValidator = null; + + if(!getDatabase().isEvaluateExpressions()) { + return; + } + + PropertyMap props = getProperties(); + + String exprStr = PropertyMaps.getTrimmedStringProperty( + props, PropertyMap.VALIDATION_RULE_PROP); + + if(exprStr != null) { + String helpStr = PropertyMaps.getTrimmedStringProperty( + props, PropertyMap.VALIDATION_TEXT_PROP); + + _rowValidator = new RowValidatorEvalContext(this) + .setExpr(exprStr, helpStr); } } @@ -437,11 +469,24 @@ public class TableImpl implements Table public PropertyMaps getPropertyMaps() throws IOException { if(_propertyMaps == null) { _propertyMaps = getDatabase().getPropertiesForObject( - _tableDefPageNumber); + _tableDefPageNumber, this); } return _propertyMaps; } + public void propertiesUpdated() throws IOException { + // propagate update to columns + for(ColumnImpl col : _columns) { + col.propertiesUpdated(); + } + + reloadRowValidator(); + + // calculated columns will need to be re-sorted (their expressions may + // have changed when their properties were updated) + _calcColEval.reSort(); + } + public List<IndexImpl> getIndexes() { return Collections.unmodifiableList(_indexes); } @@ -1283,6 +1328,9 @@ public class TableImpl implements Table if(newCol.isAutoNumber()) { _autoNumColumns.add(newCol); } + if(newCol.isCalculated()) { + _calcColEval.add(newCol); + } if(umapPos >= 0) { // read column usage map @@ -1295,7 +1343,7 @@ public class TableImpl implements Table if(!isSystem()) { // after fully constructed, allow column validator to be configured (but // only for user tables) - newCol.setColumnValidator(null); + newCol.initColumnValidator(); } // save any column properties @@ -1924,6 +1972,7 @@ public class TableImpl implements Table Collections.sort(_columns); initAutoNumberColumns(); + initCalculatedColumns(); // setup the data index for the columns int colIdx = 0; @@ -2099,7 +2148,7 @@ public class TableImpl implements Table addRow(rowValues); - returnRowValues(row, rowValues, _autoNumColumns); + returnRowValues(row, rowValues, _columns); return row; } @@ -2119,12 +2168,10 @@ public class TableImpl implements Table addRows(rowValuesList); - if(!_autoNumColumns.isEmpty()) { - for(int i = 0; i < rowValuesList.size(); ++i) { - Map<String,Object> row = rows.get(i); - Object[] rowValues = rowValuesList.get(i); - returnRowValues(row, rowValues, _autoNumColumns); - } + for(int i = 0; i < rowValuesList.size(); ++i) { + Map<String,Object> row = rows.get(i); + Object[] rowValues = rowValuesList.get(i); + returnRowValues(row, rowValues, _columns); } return rows; } @@ -2186,8 +2233,12 @@ public class TableImpl implements Table // handle various value massaging activities for(ColumnImpl column : _columns) { if(!column.isAutoNumber()) { + Object val = column.getRowValue(row); + if(val == null) { + val = column.generateDefaultValue(); + } // pass input value through column validator - column.setRowValue(row, column.validate(column.getRowValue(row))); + column.setRowValue(row, column.validate(val)); } } @@ -2195,13 +2246,22 @@ public class TableImpl implements Table handleAutoNumbersForAdd(row, writeRowState); ++autoNumAssignCount; + // need to assign calculated values after all the other fields are + // filled in but before final validation + _calcColEval.calculate(row); + + // run row validation if enabled + if(_rowValidator != null) { + _rowValidator.validate(row); + } + // write the row of data to a temporary buffer ByteBuffer rowData = createRow( row, _writeRowBufferH.getPageBuffer(getPageChannel())); int rowSize = rowData.remaining(); if (rowSize > getFormat().MAX_ROW_SIZE) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Row size " + rowSize + " is too large")); } @@ -2439,13 +2499,22 @@ public class TableImpl implements Table // fill in autonumbers handleAutoNumbersForUpdate(row, rowBuffer, rowState); + // need to assign calculated values after all the other fields are + // filled in but before final validation + _calcColEval.calculate(row); + + // run row validation if enabled + if(_rowValidator != null) { + _rowValidator.validate(row); + } + // generate new row bytes ByteBuffer newRowData = createRow( row, _writeRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, keepRawVarValues); if (newRowData.limit() > getFormat().MAX_ROW_SIZE) { - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Row size " + newRowData.limit() + " is too large")); } @@ -2660,6 +2729,7 @@ public class TableImpl implements Table return dataPage; } + // exposed for unit tests protected ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) throws IOException { @@ -2784,7 +2854,7 @@ public class TableImpl implements Table } catch(BufferOverflowException e) { // if the data is too big for the buffer, then we have gone over // the max row size - throw new IOException(withErrorContext( + throw new InvalidValueException(withErrorContext( "Row size " + buffer.limit() + " is too large")); } } @@ -2983,6 +3053,7 @@ public class TableImpl implements Table .append("columnCount", _columns.size()) .append("indexCount(data)", _indexCount) .append("logicalIndexCount", _logicalIndexCount) + .append("validator", CustomToStringStyle.ignoreNull(_rowValidator)) .append("columns", _columns) .append("indexes", _indexes) .append("ownedPages", _ownedPages) @@ -3163,6 +3234,20 @@ public class TableImpl implements Table } } + private void initCalculatedColumns() { + for(ColumnImpl c : _columns) { + if(c.isCalculated()) { + _calcColEval.add(c); + } + } + } + + boolean isThisTable(Identifier identifier) { + String collectionName = identifier.getCollectionName(); + return ((collectionName == null) || + collectionName.equalsIgnoreCase(getName())); + } + /** * Returns {@code true} if a row of the given size will fit on the given * data page, {@code false} otherwise. @@ -3189,7 +3274,7 @@ public class TableImpl implements Table return copy; } - private String withErrorContext(String msg) { + String withErrorContext(String msg) { return withErrorContext(msg, getDatabase(), getName()); } @@ -3492,4 +3577,73 @@ public class TableImpl implements Table } } + /** + * Utility for managing calculated columns. Calculated columns need to be + * evaluated in dependency order. + */ + private class CalcColEvaluator + { + /** List of calculated columns in this table, ordered by calculation + dependency */ + private final List<ColumnImpl> _calcColumns = new ArrayList<ColumnImpl>(1); + private boolean _sorted; + + public void add(ColumnImpl col) { + if(!getDatabase().isEvaluateExpressions()) { + return; + } + _calcColumns.add(col); + // whenever we add new columns, we need to re-sort + _sorted = false; + } + + public void reSort() { + // mark columns for re-sort on next use + _sorted = false; + } + + public void calculate(Object[] row) throws IOException { + if(!_sorted) { + sortColumnsByDeps(); + _sorted = true; + } + + for(ColumnImpl col : _calcColumns) { + Object rowValue = col.getCalculationContext().eval(row); + col.setRowValue(row, rowValue); + } + } + + private void sortColumnsByDeps() { + + // a topological sort sorts nodes where A -> B such that A ends up in + // the list before B (assuming that we are working with a DAG). In our + // case, we return "descendent" info as Field1 -> Field2 (where Field1 + // uses Field2 in its calculation). This means that in order to + // correctly calculate Field1, we need to calculate Field2 first, and + // hence essentially need the reverse topo sort (a list where Field2 + // comes before Field1). + (new TopoSorter<ColumnImpl>(_calcColumns, TopoSorter.REVERSE) { + @Override + protected void getDescendents(ColumnImpl from, + List<ColumnImpl> descendents) { + + Set<Identifier> identifiers = new LinkedHashSet<Identifier>(); + from.getCalculationContext().collectIdentifiers(identifiers); + + for(Identifier identifier : identifiers) { + if(isThisTable(identifier)) { + String colName = identifier.getObjectName(); + for(ColumnImpl calcCol : _calcColumns) { + // we only care if the identifier is another calc field + if(calcCol.getName().equalsIgnoreCase(colName)) { + descendents.add(calcCol); + } + } + } + } + } + }).sort(); + } + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TopoSorter.java b/src/main/java/com/healthmarketscience/jackcess/impl/TopoSorter.java new file mode 100644 index 0000000..5ba0b07 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TopoSorter.java @@ -0,0 +1,114 @@ +/* +Copyright (c) 2018 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; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author James Ahlborn + */ +public abstract class TopoSorter<E> +{ + public static final boolean REVERSE = true; + + // https://en.wikipedia.org/wiki/Topological_sorting + private static final int UNMARKED = 0; + private static final int TEMP_MARK = 1; + private final static int PERM_MARK = 2; + + private final List<E> _values; + private final List<Node<E>> _nodes = new ArrayList<Node<E>>(); + private final boolean _reverse; + + protected TopoSorter(List<E> values, boolean reverse) { + _values = values; + _reverse = reverse; + } + + public void sort() { + + for(E val : _values) { + Node<E> node = new Node<E>(val); + getDescendents(val, node._descs); + + // build the internal list in reverse so that we maintain the "original" + // order of items which we don't need to re-arrange + _nodes.add(0, node); + } + + _values.clear(); + + for(Node<E> node : _nodes) { + if(node._mark != UNMARKED) { + continue; + } + + visit(node); + } + } + + private void visit(Node<E> node) { + + if(node._mark == PERM_MARK) { + return; + } + + if(node._mark == TEMP_MARK) { + throw new IllegalStateException("Cycle detected"); + } + + node._mark = TEMP_MARK; + + for(E descVal : node._descs) { + Node<E> desc = findDescendent(descVal); + visit(desc); + } + + node._mark = PERM_MARK; + + if(_reverse) { + _values.add(node._val); + } else { + _values.add(0, node._val); + } + } + + private Node<E> findDescendent(E val) { + for(Node<E> node : _nodes) { + if(node._val == val) { + return node; + } + } + throw new IllegalStateException("Unknown descendent " + val); + } + + protected abstract void getDescendents(E from, List<E> descendents); + + + private static class Node<E> + { + private final E _val; + private final List<E> _descs = new ArrayList<E>(); + private int _mark = UNMARKED; + + private Node(E val) { + _val = val; + } + } +} 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..188416a --- /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 Integer getAsLongInt() { + return roundToLongInt(); + } + + @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..e8ae339 --- /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 Integer getAsLongInt() { + return getDelegate().getAsLongInt(); + } + + 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..eb1ac7e --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java @@ -0,0 +1,56 @@ +/* +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 Integer getAsLongInt() { + return roundToLongInt(); + } + + @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..0f081dd --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java @@ -0,0 +1,75 @@ +/* +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; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.impl.NumberFormatter; + +/** + * + * @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 Integer getAsLongInt() { + throw invalidConversion(Value.Type.LONG); + } + + public Double getAsDouble() { + throw invalidConversion(Value.Type.DOUBLE); + } + + public BigDecimal getAsBigDecimal() { + throw invalidConversion(Value.Type.BIG_DEC); + } + + private EvalException invalidConversion(Value.Type newType) { + return new EvalException( + getType() + " value cannot be converted to " + newType); + } + + protected Integer roundToLongInt() { + return getAsBigDecimal().setScale(0, NumberFormatter.ROUND_MODE) + .intValueExact(); + } + + @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..89e4004 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java @@ -0,0 +1,63 @@ +/* +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 com.healthmarketscience.jackcess.impl.NumberFormatter; + +/** + * + * @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 NumberFormatter.format(_val); + } + + @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..e0f6e25 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java @@ -0,0 +1,794 @@ +/* +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 java.util.regex.Pattern; + +import com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.NumberFormatter; + + +/** + * + * @author James Ahlborn + */ +public class BuiltinOperators +{ + private static final String DIV_BY_ZERO = "/ by zero"; + + private static final double MIN_INT = Integer.MIN_VALUE; + private static final double MAX_INT = Integer.MAX_VALUE; + + 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(-1); + public static final Value FALSE_VAL = new LongValue(0); + public static final Value EMPTY_STR_VAL = new StringValue(""); + public static final Value ZERO_VAL = FALSE_VAL; + + + 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 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.getAsLongInt()); + case DOUBLE: + return toValue(-param1.getAsDouble()); + case STRING: + case BIG_DEC: + return toValue(param1.getAsBigDecimal().negate( + NumberFormatter.DEC_MATH_CONTEXT)); + default: + throw new EvalException("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.getAsLongInt() + param2.getAsLongInt()); + case DOUBLE: + return toValue(param1.getAsDouble() + param2.getAsDouble()); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().add( + param2.getAsBigDecimal(), + NumberFormatter.DEC_MATH_CONTEXT)); + default: + throw new EvalException("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.getAsLongInt() - param2.getAsLongInt()); + case DOUBLE: + return toValue(param1.getAsDouble() - param2.getAsDouble()); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().subtract( + param2.getAsBigDecimal(), + NumberFormatter.DEC_MATH_CONTEXT)); + default: + throw new EvalException("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.getAsLongInt() * param2.getAsLongInt()); + case DOUBLE: + return toValue(param1.getAsDouble() * param2.getAsDouble()); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().multiply( + param2.getAsBigDecimal(), + NumberFormatter.DEC_MATH_CONTEXT)); + default: + throw new EvalException("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: + int lp1 = param1.getAsLongInt(); + int lp2 = param2.getAsLongInt(); + 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(divide(param1.getAsBigDecimal(), param2.getAsBigDecimal())); + default: + throw new EvalException("Unexpected type " + mathType); + } + } + + 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); + if(mathType == Value.Type.STRING) { + throw new EvalException("Unexpected type " + mathType); + } + return toValue(param1.getAsLongInt() / param2.getAsLongInt()); + } + + 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); + + if(mathType == Value.Type.BIG_DEC) { + // see if we can handle the limited options supported for BigDecimal + // (must be a positive int exponent) + try { + BigDecimal result = param1.getAsBigDecimal().pow( + param2.getAsBigDecimal().intValueExact(), + NumberFormatter.DEC_MATH_CONTEXT); + return toValue(result); + } catch(ArithmeticException ae) { + // fall back to general handling via doubles... + } + } + + // 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((int)result); + } + + return toValue(result); + } + + 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); + + if(mathType == Value.Type.STRING) { + throw new EvalException("Unexpected type " + mathType); + } + return toValue(param1.getAsLongInt() % param2.getAsLongInt()); + } + + 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 EvalException("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.getAsLongInt().compareTo(param2.getAsLongInt()); + case DOUBLE: + return param1.getAsDouble().compareTo(param2.getAsDouble()); + case BIG_DEC: + return param1.getAsBigDecimal().compareTo(param2.getAsBigDecimal()); + default: + throw new EvalException("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(int i) { + return new LongValue(i); + } + + public static Value toValue(Integer i) { + return new LongValue(i); + } + + public static Value toValue(float f) { + return new DoubleValue((double)f); + } + + public static Value toValue(double s) { + return new DoubleValue(s); + } + + public static Value toValue(Double s) { + return new DoubleValue(s); + } + + public static Value toValue(BigDecimal s) { + return new BigDecimalValue(normalize(s)); + } + + public static Value toValue(Value.Type type, double dd, DateFormat fmt) { + return toValue(type, new Date(ColumnImpl.fromDateDouble( + dd, fmt.getCalendar())), fmt); + } + + public static Value toValue(EvalContext ctx, Value.Type type, Date d) { + return toValue(type, d, getDateFormatForType(ctx, type)); + } + + public static Value toValue(Value.Type type, Date d, DateFormat fmt) { + 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 EvalException("Unexpected date/time type " + type); + } + } + + 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 { + fmt = getDateFormatForType(ctx, type); + } + + Date d = new Date(ColumnImpl.fromDateDouble(v, fmt.getCalendar())); + + return toValue(type, d, fmt); + } + + static DateFormat getDateFormatForType(EvalContext ctx, Value.Type type) { + 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 EvalException("Unexpected date/time type " + type); + } + return ctx.createDateFormat(fmtStr); + } + + 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); + } + + return getPreferredNumericType(t1.getPreferredNumericType(), + t2.getPreferredNumericType()); + } + + private static Value.Type getPreferredNumericType(Value.Type t1, Value.Type t2) + { + // 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(); + if(prefType.isNumeric()) { + // seems like when strings are coerced to numbers, they are usually + // doubles, unless the current context is decimal + prefType = ((prefType == Value.Type.BIG_DEC) ? + Value.Type.BIG_DEC : Value.Type.DOUBLE); + } + 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 BigDecimal divide(BigDecimal num, BigDecimal denom) { + return num.divide(denom, NumberFormatter.DEC_MATH_CONTEXT); + } + + static boolean isIntegral(double d) { + double id = Math.rint(d); + return ((d == id) && (d >= MIN_INT) && (d <= MAX_INT) && + !Double.isInfinite(d) && !Double.isNaN(d)); + } + + /** + * Converts the given BigDecimal to the minimal scale >= 0; + */ + static BigDecimal normalize(BigDecimal bd) { + if(bd.scale() == 0) { + return bd; + } + // handle a bug in the jdk which doesn't strip zero values + if(bd.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + bd = bd.stripTrailingZeros(); + if(bd.scale() < 0) { + bd = bd.setScale(0); + } + return bd; + } +} 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/DefaultDateFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java new file mode 100644 index 0000000..75fa68c --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java @@ -0,0 +1,276 @@ +/* +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.text.DateFormat; +import java.util.Calendar; +import java.util.Date; + +import com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; + +/** + * + * @author James Ahlborn + */ +public class DefaultDateFunctions +{ + // min, valid, recognizable date: January 1, 100 A.D. 00:00:00 + private static final double MIN_DATE = -657434.0d; + // max, valid, recognizable date: December 31, 9999 A.D. 23:59:59 + private static final double MAX_DATE = 2958465.999988426d; + + private static final long SECONDS_PER_DAY = 24L * 60L * 60L; + private static final double DSECONDS_PER_DAY = SECONDS_PER_DAY; + + private static final long SECONDS_PER_HOUR = 60L * 60L; + private static final long SECONDS_PER_MINUTE = 60L; + private static final long MILLIS_PER_SECOND = 1000L; + + private DefaultDateFunctions() {} + + static void init() { + // dummy method to ensure this class is loaded + } + + public static final Function DATE = registerFunc(new Func0("Date") { + @Override + protected Value eval0(EvalContext ctx) { + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.DATE); + double dd = dateOnly(currentTimeDouble(fmt)); + return BuiltinOperators.toValue(Value.Type.DATE, dd, fmt); + } + }); + + public static final Function DATEVALUE = registerFunc(new Func1NullIsNull("DateValue") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Value dv = nonNullToDateValue(ctx, param1); + if(dv.getType() == Value.Type.DATE) { + return dv; + } + double dd = dateOnly(dv.getAsDouble()); + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.DATE); + return BuiltinOperators.toValue(Value.Type.DATE, dd, fmt); + } + }); + + public static final Function NOW = registerFunc(new Func0("Now") { + @Override + protected Value eval0(EvalContext ctx) { + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.DATE_TIME); + return BuiltinOperators.toValue(Value.Type.DATE_TIME, new Date(), fmt); + } + }); + + public static final Function TIME = registerFunc(new Func0("Time") { + @Override + protected Value eval0(EvalContext ctx) { + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.TIME); + double dd = timeOnly(currentTimeDouble(fmt)); + return BuiltinOperators.toValue(Value.Type.TIME, dd, fmt); + } + }); + + public static final Function TIMEVALUE = registerFunc(new Func1NullIsNull("TimeValue") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Value dv = nonNullToDateValue(ctx, param1); + if(dv.getType() == Value.Type.TIME) { + return dv; + } + double dd = timeOnly(dv.getAsDouble()); + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.TIME); + return BuiltinOperators.toValue(Value.Type.TIME, dd, fmt); + } + }); + + public static final Function TIMER = registerFunc(new Func0("Timer") { + @Override + protected Value eval0(EvalContext ctx) { + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.TIME); + double dd = timeOnly(currentTimeDouble(fmt)) * DSECONDS_PER_DAY; + return BuiltinOperators.toValue(dd); + } + }); + + public static final Function TIMESERIAL = registerFunc(new Func3("TimeSerial") { + @Override + protected Value eval3(EvalContext ctx, Value param1, Value param2, Value param3) { + int hours = param1.getAsLongInt(); + int minutes = param2.getAsLongInt(); + int seconds = param3.getAsLongInt(); + + long totalSeconds = (hours * SECONDS_PER_HOUR) + + (minutes * SECONDS_PER_MINUTE) + seconds; + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, Value.Type.TIME); + double dd = totalSeconds / DSECONDS_PER_DAY; + return BuiltinOperators.toValue(Value.Type.TIME, dd, fmt); + } + }); + + public static final Function HOUR = registerFunc(new Func1NullIsNull("Hour") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue( + nonNullToCalendarField(ctx, param1, Calendar.HOUR_OF_DAY)); + } + }); + + public static final Function MINUTE = registerFunc(new Func1NullIsNull("Minute") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue( + nonNullToCalendarField(ctx, param1, Calendar.MINUTE)); + } + }); + + public static final Function SECOND = registerFunc(new Func1NullIsNull("Second") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue( + nonNullToCalendarField(ctx, param1, Calendar.SECOND)); + } + }); + + public static final Function YEAR = registerFunc(new Func1NullIsNull("Year") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + // convert from 0 based to 1 based value + return BuiltinOperators.toValue( + nonNullToCalendarField(ctx, param1, Calendar.YEAR) + 1); + } + }); + + public static final Function MONTH = registerFunc(new Func1NullIsNull("Month") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + // convert from 0 based to 1 based value + return BuiltinOperators.toValue( + nonNullToCalendarField(ctx, param1, Calendar.MONTH) + 1); + } + }); + + public static final Function DAY = registerFunc(new Func1NullIsNull("Day") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue( + nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_MONTH)); + } + }); + + public static final Function WEEKDAY = registerFunc(new FuncVar("Weekday", 1, 2) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + if(param1 == null) { + return null; + } + int day = nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_WEEK); + if(params.length > 1) { + // TODO handle first day of week + // int firstDay = params[1].getAsLong(); + throw new UnsupportedOperationException(); + } + return BuiltinOperators.toValue(day); + } + }); + + + private static int nonNullToCalendarField(EvalContext ctx, Value param, + int field) { + return nonNullToCalendar(ctx, param).get(field); + } + + private static Calendar nonNullToCalendar(EvalContext ctx, Value param) { + param = nonNullToDateValue(ctx, param); + if(param == null) { + // not a date/time + throw new EvalException("Invalid date/time expression '" + param + "'"); + } + + Calendar cal = getDateValueFormat(ctx, param).getCalendar(); + cal.setTime(param.getAsDateTime(ctx)); + return cal; + } + + static Value nonNullToDateValue(EvalContext ctx, Value param) { + Value.Type type = param.getType(); + if(type.isTemporal()) { + return param; + } + + if(type == Value.Type.STRING) { + // see if we can coerce to date/time + + // FIXME use ExpressionatorTokenizer to detect explicit date/time format + + try { + return numberToDateValue(ctx, param.getAsDouble()); + } catch(NumberFormatException ignored) { + // not a number + return null; + } + } + + // must be a number + return numberToDateValue(ctx, param.getAsDouble()); + } + + private static Value numberToDateValue(EvalContext ctx, double dd) { + if((dd < MIN_DATE) || (dd > MAX_DATE)) { + // outside valid date range + return null; + } + + boolean hasDate = (dateOnly(dd) != 0.0d); + boolean hasTime = (timeOnly(dd) != 0.0d); + + Value.Type type = (hasDate ? (hasTime ? Value.Type.DATE_TIME : Value.Type.DATE) : + Value.Type.TIME); + DateFormat fmt = BuiltinOperators.getDateFormatForType(ctx, type); + return BuiltinOperators.toValue(type, dd, fmt); + } + + private static DateFormat getDateValueFormat(EvalContext ctx, Value param) { + return ((param instanceof BaseDateValue) ? + ((BaseDateValue)param).getFormat() : + BuiltinOperators.getDateFormatForType(ctx, param.getType())); + } + + private static double dateOnly(double dd) { + // the integral part of the date/time double is the date value. discard + // the fractional portion + return (long)dd; + } + + private static double timeOnly(double dd) { + // the fractional part of the date/time double is the time value. discard + // the integral portion and convert to seconds + return new BigDecimal(dd).remainder(BigDecimal.ONE).doubleValue(); + } + + private static double currentTimeDouble(DateFormat fmt) { + return ColumnImpl.toDateDouble(System.currentTimeMillis(), fmt.getCalendar()); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFinancialFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFinancialFunctions.java new file mode 100644 index 0000000..4fe59ec --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFinancialFunctions.java @@ -0,0 +1,440 @@ +/* +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 com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.Value; +import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; + +/** + * + * @author James Ahlborn + */ +public class DefaultFinancialFunctions +{ + /** 0 - payment end of month (default) */ + private static final int PMT_END_MNTH = 0; + /** 1 - payment start of month */ + private static final int PMT_BEG_MNTH = 1; + + + private DefaultFinancialFunctions() {} + + static void init() { + // dummy method to ensure this class is loaded + } + + + public static final Function NPER = registerFunc(new FuncVar("NPer", 3, 5) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + double rate = params[0].getAsDouble(); + double pmt = params[1].getAsDouble(); + double pv = params[2].getAsDouble(); + + double fv = 0d; + if(params.length > 3) { + fv = params[3].getAsDouble(); + } + + int pmtType = PMT_END_MNTH; + if(params.length > 4) { + pmtType = params[4].getAsLongInt(); + } + + double result = calculateLoanPaymentPeriods(rate, pmt, pv, pmtType); + + if(fv != 0d) { + result += calculateAnnuityPaymentPeriods(rate, pmt, fv, pmtType); + } + + return BuiltinOperators.toValue(result); + } + }); + + public static final Function FV = registerFunc(new FuncVar("FV", 3, 5) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + double rate = params[0].getAsDouble(); + double nper = params[1].getAsDouble(); + double pmt = params[2].getAsDouble(); + + double pv = 0d; + if(params.length > 3) { + pv = params[3].getAsDouble(); + } + + int pmtType = PMT_END_MNTH; + if(params.length > 4) { + pmtType = params[4].getAsLongInt(); + } + + if(pv != 0d) { + nper -= calculateLoanPaymentPeriods(rate, pmt, pv, pmtType); + } + + double result = calculateFutureValue(rate, nper, pmt, pmtType); + + return BuiltinOperators.toValue(result); + } + }); + + public static final Function PV = registerFunc(new FuncVar("PV", 3, 5) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + double rate = params[0].getAsDouble(); + double nper = params[1].getAsDouble(); + double pmt = params[2].getAsDouble(); + + double fv = 0d; + if(params.length > 3) { + fv = params[3].getAsDouble(); + } + + int pmtType = PMT_END_MNTH; + if(params.length > 4) { + pmtType = params[4].getAsLongInt(); + } + + if(fv != 0d) { + nper -= calculateAnnuityPaymentPeriods(rate, pmt, fv, pmtType); + } + + double result = calculatePresentValue(rate, nper, pmt, pmtType); + + return BuiltinOperators.toValue(result); + } + }); + + public static final Function PMT = registerFunc(new FuncVar("Pmt", 3, 5) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + double rate = params[0].getAsDouble(); + double nper = params[1].getAsDouble(); + double pv = params[2].getAsDouble(); + + double fv = 0d; + if(params.length > 3) { + fv = params[3].getAsDouble(); + } + + int pmtType = PMT_END_MNTH; + if(params.length > 4) { + pmtType = params[4].getAsLongInt(); + } + + double result = calculateLoanPayment(rate, nper, pv, pmtType); + + if(fv != 0d) { + result += calculateAnnuityPayment(rate, nper, fv, pmtType); + } + + return BuiltinOperators.toValue(result); + } + }); + + // FIXME not working for all param combos + // public static final Function IPMT = registerFunc(new FuncVar("IPmt", 4, 6) { + // @Override + // protected Value evalVar(EvalContext ctx, Value[] params) { + // double rate = params[0].getAsDouble(); + // double per = params[1].getAsDouble(); + // double nper = params[2].getAsDouble(); + // double pv = params[3].getAsDouble(); + + // double fv = 0d; + // if(params.length > 4) { + // fv = params[4].getAsDouble(); + // } + + // int pmtType = PMT_END_MNTH; + // if(params.length > 5) { + // pmtType = params[5].getAsLongInt(); + // } + + // double pmt = calculateLoanPayment(rate, nper, pv, pmtType); + + // if(fv != 0d) { + // pmt += calculateAnnuityPayment(rate, nper, fv, pmtType); + // } + + // double result = calculateInterestPayment(pmt, rate, per, pv, pmtType); + + // return BuiltinOperators.toValue(result); + // } + // }); + + // FIXME untested + // public static final Function PPMT = registerFunc(new FuncVar("PPmt", 4, 6) { + // @Override + // protected Value evalVar(EvalContext ctx, Value[] params) { + // double rate = params[0].getAsDouble(); + // double per = params[1].getAsDouble(); + // double nper = params[2].getAsDouble(); + // double pv = params[3].getAsDouble(); + + // double fv = 0d; + // if(params.length > 4) { + // fv = params[4].getAsDouble(); + // } + + // int pmtType = PMT_END_MNTH; + // if(params.length > 5) { + // pmtType = params[5].getAsLongInt(); + // } + + // double pmt = calculateLoanPayment(rate, nper, pv, pmtType); + + // if(fv != 0d) { + // pmt += calculateAnnuityPayment(rate, nper, fv, pmtType); + // } + + // double result = pmt - calculateInterestPayment(pmt, rate, per, pv, + // pmtType); + + // return BuiltinOperators.toValue(result); + // } + // }); + + // FIXME, doesn't work for partial days + // public static final Function DDB = registerFunc(new FuncVar("DDB", 4, 5) { + // @Override + // protected Value evalVar(EvalContext ctx, Value[] params) { + // double cost = params[0].getAsDouble(); + // double salvage = params[1].getAsDouble(); + // double life = params[2].getAsDouble(); + // double period = params[3].getAsDouble(); + + // double factor = 2d; + // if(params.length > 4) { + // factor = params[4].getAsDouble(); + // } + + // double result = 0d; + + // // fractional value always rounds up to one year + // if(period < 1d) { + // period = 1d; + // } + + // // FIXME? apply partial period _first_ + // // double partPeriod = period % 1d; + // // if(partPeriod != 0d) { + // // result = calculateDoubleDecliningBalance( + // // cost, salvage, life, factor) * partPeriod; + // // period -= partPeriod; + // // cost -= result; + // // } + // double prevResult = 0d; + // while(period > 0d) { + // prevResult = result; + // double remPeriod = Math.min(period, 1d); + // result = calculateDoubleDecliningBalance( + // cost, salvage, life, factor); + // if(remPeriod < 1d) { + // result = (prevResult + result) / 2d; + // } + // period -= 1d; + // cost -= result; + // } + + // return BuiltinOperators.toValue(result); + // } + // }); + + // FIXME, untested + public static final Function SLN = registerFunc(new FuncVar("SLN", 3, 3) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + double cost = params[0].getAsDouble(); + double salvage = params[1].getAsDouble(); + double life = params[2].getAsDouble(); + + double result = calculateStraightLineDepreciation(cost, salvage, life); + + return BuiltinOperators.toValue(result); + } + }); + + // FIXME, untested + public static final Function SYD = registerFunc(new FuncVar("SYD", 4, 4) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + double cost = params[0].getAsDouble(); + double salvage = params[1].getAsDouble(); + double life = params[2].getAsDouble(); + double period = params[3].getAsDouble(); + + double result = calculateSumOfYearsDepreciation( + cost, salvage, life, period); + + return BuiltinOperators.toValue(result); + } + }); + + + private static double calculateLoanPaymentPeriods( + double rate, double pmt, double pv, int pmtType) { + + // https://brownmath.com/bsci/loan.htm + // http://financeformulas.net/Number-of-Periods-of-Annuity-from-Present-Value.html + + if(pmtType == PMT_BEG_MNTH) { + pv += pmt; + } + + double v1 = Math.log(1d + (rate * pv / pmt)); + + double v2 = Math.log(1d + rate); + + double result = -v1 / v2; + + if(pmtType == PMT_BEG_MNTH) { + result += 1d; + } + + return result; + } + + private static double calculateAnnuityPaymentPeriods( + double rate, double pmt, double fv, int pmtType) { + + // https://brownmath.com/bsci/loan.htm + // http://financeformulas.net/Number-of-Periods-of-Annuity-from-Future-Value.html + // https://accountingexplained.com/capital/tvm/fv-annuity + + if(pmtType == PMT_BEG_MNTH) { + fv *= (1d + rate); + } + + double v1 = Math.log(1d - (rate * fv / pmt)); + + double v2 = Math.log(1d + rate); + + double result = v1 / v2; + + if(pmtType == PMT_BEG_MNTH) { + result -= 1d; + } + + return result; + } + + private static double calculateFutureValue( + double rate, double nper, double pmt, int pmtType) { + + double result = -pmt * ((Math.pow((1d + rate), nper) - 1d) / rate); + + if(pmtType == PMT_BEG_MNTH) { + result *= (1d + rate); + } + + return result; + } + + private static double calculatePresentValue( + double rate, double nper, double pmt, int pmtType) { + + if(pmtType == PMT_BEG_MNTH) { + nper -= 1d; + } + + double result = -pmt * ((1d - Math.pow((1d + rate), -nper)) / rate); + + if(pmtType == PMT_BEG_MNTH) { + result -= pmt; + } + + return result; + } + + private static double calculateLoanPayment( + double rate, double nper, double pv, int pmtType) { + + double result = -(rate * pv) / (1d - Math.pow((1d + rate), -nper)); + + if(pmtType == PMT_BEG_MNTH) { + result /= (1d + rate); + } + + return result; + } + + private static double calculateAnnuityPayment( + double rate, double nper, double fv, int pmtType) { + + double result = -(fv * rate) / (Math.pow((1d + rate), nper) - 1d); + + if(pmtType == PMT_BEG_MNTH) { + result /= (1d + rate); + } + + return result; + } + + private static double calculateInterestPayment( + double pmt, double rate, double per, double pv, int pmtType) { + + // http://www.tvmcalcs.com/index.php/calculators/apps/excel_loan_amortization + // http://financeformulas.net/Remaining_Balance_Formula.html + + double pvPer = per; + double fvPer = per; + if(pmtType == PMT_END_MNTH) { + pvPer -= 1d; + fvPer -= 1d; + } else { + pvPer -= 2d; + fvPer -= 1d; + } + + double remBalance = (pv * Math.pow((1d + rate), pvPer)) - + // FIXME, always use pmtType of 0? + calculateFutureValue(rate, fvPer, pmt, PMT_END_MNTH); + + double result = -(remBalance * rate); + + return result; + } + + private static double calculateDoubleDecliningBalance( + double cost, double salvage, double life, double factor) { + + double result1 = cost * (factor/life); + double result2 = cost - salvage; + + return Math.min(result1, result2); + } + + private static double calculateStraightLineDepreciation( + double cost, double salvage, double life) { + return ((cost - salvage) / life); + } + + private static double calculateSumOfYearsDepreciation( + double cost, double salvage, double life, double period) { + + double sumOfYears = (period * (period + 1)) / 2d; + double result = ((cost - salvage) * ((life + 1 - period) / sumOfYears)); + + return result; + } + +} + 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..7acca2c --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java @@ -0,0 +1,554 @@ +/* +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.Arrays; +import java.util.HashMap; +import java.util.Map; + +import com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.FunctionLookup; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.NumberFormatter; + +/** + * + * @author James Ahlborn + */ +public class DefaultFunctions +{ + private static final Map<String,Function> FUNCS = + new HashMap<String,Function>(); + + private static final char NON_VAR_SUFFIX = '$'; + + static { + // load all default functions + DefaultTextFunctions.init(); + DefaultNumberFunctions.init(); + DefaultDateFunctions.init(); + DefaultFinancialFunctions.init(); + } + + public static final FunctionLookup LOOKUP = new FunctionLookup() { + public Function getFunction(String name) { + return DefaultFunctions.getFunction(name); + } + }; + + private DefaultFunctions() {} + + public static Function getFunction(String name) { + return FUNCS.get(DatabaseImpl.toLookupName(name)); + } + + 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 EvalException( + "Invalid number of parameters " + + num + " passed, expected " + range); + } + } + + protected IllegalStateException invalidFunctionCall( + Throwable t, Value[] params) + { + String paramStr = Arrays.toString(params); + String msg = "Invalid function call {" + _name + "(" + + paramStr.substring(1, paramStr.length() - 1) + ")}"; + return new IllegalStateException(msg, t); + } + + @Override + public String toString() { + return getName() + "()"; + } + } + + public static abstract class Func0 extends BaseFunction + { + protected Func0(String name) { + super(name, 0, 0); + } + + @Override + public boolean isPure() { + // 0-arg functions are usually not pure + return false; + } + + public final Value eval(EvalContext ctx, Value... params) { + try { + validateNumParams(params); + return eval0(ctx); + } catch(Exception e) { + throw invalidFunctionCall(e, params); + } + } + + protected abstract Value eval0(EvalContext ctx); + } + + public static abstract class Func1 extends BaseFunction + { + protected Func1(String name) { + super(name, 1, 1); + } + + public final Value eval(EvalContext ctx, Value... params) { + try { + validateNumParams(params); + return eval1(ctx, params[0]); + } catch(Exception e) { + throw invalidFunctionCall(e, params); + } + } + + protected abstract Value eval1(EvalContext ctx, Value param); + } + + public static abstract class Func1NullIsNull extends BaseFunction + { + protected Func1NullIsNull(String name) { + super(name, 1, 1); + } + + public final Value eval(EvalContext ctx, Value... params) { + try { + validateNumParams(params); + Value param1 = params[0]; + if(param1.isNull()) { + return param1; + } + return eval1(ctx, param1); + } catch(Exception e) { + throw invalidFunctionCall(e, params); + } + } + + 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) { + try { + validateNumParams(params); + return eval2(ctx, params[0], params[1]); + } catch(Exception e) { + throw invalidFunctionCall(e, params); + } + } + + 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) { + try { + validateNumParams(params); + return eval3(ctx, params[0], params[1], params[2]); + } catch(Exception e) { + throw invalidFunctionCall(e, params); + } + } + + protected abstract Value eval3(EvalContext ctx, + Value param1, Value param2, Value param3); + } + + public static abstract class FuncVar extends BaseFunction + { + protected FuncVar(String name) { + super(name, 0, Integer.MAX_VALUE); + } + + protected FuncVar(String name, int minParams, int maxParams) { + super(name, minParams, maxParams); + } + + public final Value eval(EvalContext ctx, Value... params) { + try { + validateNumParams(params); + return evalVar(ctx, params); + } catch(Exception e) { + throw invalidFunctionCall(e, params); + } + } + + protected abstract Value evalVar(EvalContext ctx, Value[] params); + } + + public static class StringFuncWrapper implements Function + { + private final String _name; + private final Function _delegate; + + public StringFuncWrapper(Function delegate) { + _delegate = delegate; + _name = _delegate.getName() + NON_VAR_SUFFIX; + } + + public String getName() { + return _name; + } + + public boolean isPure() { + return _delegate.isPure(); + } + + public Value eval(EvalContext ctx, Value... params) { + Value result = _delegate.eval(ctx, params); + if(result.isNull()) { + // non-variant version does not do null-propagation, so force + // exception to be thrown here + result.getAsString(); + } + return result; + } + + @Override + public String toString() { + return getName() + "()"; + } + } + + + 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); + } + }); + + public static final Function HEX = registerStringFunc(new Func1NullIsNull("Hex") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + if((param1.getType() == Value.Type.STRING) && + (param1.getAsString().length() == 0)) { + return BuiltinOperators.ZERO_VAL; + } + int lv = param1.getAsLongInt(); + return BuiltinOperators.toValue(Integer.toHexString(lv).toUpperCase()); + } + }); + + public static final Function NZ = registerFunc(new FuncVar("Nz", 1, 2) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + if(!param1.isNull()) { + return param1; + } + if(params.length > 1) { + return params[1]; + } + Value.Type resultType = ctx.getResultType(); + return (((resultType == null) || + (resultType == Value.Type.STRING)) ? + BuiltinOperators.EMPTY_STR_VAL : BuiltinOperators.ZERO_VAL); + } + }); + + public static final Function CHOOSE = registerFunc(new FuncVar("Choose", 1, Integer.MAX_VALUE) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + int idx = param1.getAsLongInt(); + if((idx < 1) || (idx >= params.length)) { + return BuiltinOperators.NULL_VAL; + } + return params[idx]; + } + }); + + public static final Function SWITCH = registerFunc(new FuncVar("Switch") { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + if((params.length % 2) != 0) { + throw new EvalException("Odd number of parameters"); + } + for(int i = 0; i < params.length; i+=2) { + if(params[i].getAsBoolean()) { + return params[i + 1]; + } + } + return BuiltinOperators.NULL_VAL; + } + }); + + public static final Function OCT = registerStringFunc(new Func1NullIsNull("Oct") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + if((param1.getType() == Value.Type.STRING) && + (param1.getAsString().length() == 0)) { + return BuiltinOperators.ZERO_VAL; + } + int lv = param1.getAsLongInt(); + return BuiltinOperators.toValue(Integer.toOctalString(lv)); + } + }); + + public static final Function CBOOL = registerFunc(new Func1("CBool") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + boolean b = param1.getAsBoolean(); + return BuiltinOperators.toValue(b); + } + }); + + public static final Function CBYTE = registerFunc(new Func1("CByte") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int lv = param1.getAsLongInt(); + if((lv < 0) || (lv > 255)) { + throw new EvalException("Byte code '" + lv + "' out of range "); + } + return BuiltinOperators.toValue(lv); + } + }); + + public static final Function CCUR = registerFunc(new Func1("CCur") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + BigDecimal bd = param1.getAsBigDecimal(); + bd = bd.setScale(4, NumberFormatter.ROUND_MODE); + return BuiltinOperators.toValue(bd); + } + }); + + public static final Function CDATE = registerFunc(new Func1("CDate") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return DefaultDateFunctions.nonNullToDateValue(ctx, param1); + } + }); + static { + registerFunc("CVDate", CDATE); + } + + public static final Function CDBL = registerFunc(new Func1("CDbl") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Double dv = param1.getAsDouble(); + return BuiltinOperators.toValue(dv); + } + }); + + public static final Function CDEC = registerFunc(new Func1("CDec") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + BigDecimal bd = param1.getAsBigDecimal(); + return BuiltinOperators.toValue(bd); + } + }); + + public static final Function CINT = registerFunc(new Func1("CInt") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int lv = param1.getAsLongInt(); + if((lv < Short.MIN_VALUE) || (lv > Short.MAX_VALUE)) { + throw new EvalException("Int value '" + lv + "' out of range "); + } + return BuiltinOperators.toValue(lv); + } + }); + + public static final Function CLNG = registerFunc(new Func1("CLng") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int lv = param1.getAsLongInt(); + return BuiltinOperators.toValue(lv); + } + }); + + public static final Function CSNG = registerFunc(new Func1("CSng") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Double dv = param1.getAsDouble(); + if((dv < Float.MIN_VALUE) || (dv > Float.MAX_VALUE)) { + throw new EvalException("Single value '" + dv + "' out of range "); + } + return BuiltinOperators.toValue(dv.floatValue()); + } + }); + + public static final Function CSTR = registerFunc(new Func1("CStr") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(param1.getAsString()); + } + }); + + public static final Function CVAR = registerFunc(new Func1("CVar") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return param1; + } + }); + + public static final Function ISNULL = registerFunc(new Func1("IsNull") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(param1.isNull()); + } + }); + + public static final Function ISDATE = registerFunc(new Func1("IsDate") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue( + !param1.isNull() && + (DefaultDateFunctions.nonNullToDateValue(ctx, param1) != null)); + } + }); + + public static final Function VARTYPE = registerFunc(new Func1("VarType") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Value.Type type = param1.getType(); + int vType = 0; + switch(type) { + case NULL: + // vbNull + vType = 1; + break; + case STRING: + // vbString + vType = 8; + break; + case DATE: + case TIME: + case DATE_TIME: + // vbDate + vType = 7; + break; + case LONG: + // vbLong + vType = 3; + break; + case DOUBLE: + // vbDouble + vType = 5; + break; + case BIG_DEC: + // vbDecimal + vType = 14; + break; + default: + throw new EvalException("Unknown type " + type); + } + return BuiltinOperators.toValue(vType); + } + }); + + public static final Function TYPENAME = registerFunc(new Func1("TypeName") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Value.Type type = param1.getType(); + String tName = null; + switch(type) { + case NULL: + tName = "Null"; + break; + case STRING: + tName = "String"; + break; + case DATE: + case TIME: + case DATE_TIME: + tName = "Date"; + break; + case LONG: + tName = "Long"; + break; + case DOUBLE: + tName = "Double"; + break; + case BIG_DEC: + tName = "Decimal"; + break; + default: + throw new EvalException("Unknown type " + type); + } + return BuiltinOperators.toValue(tName); + } + }); + + + + // https://www.techonthenet.com/access/functions/ + // https://support.office.com/en-us/article/Access-Functions-by-category-b8b136c3-2716-4d39-94a2-658ce330ed83 + + static Function registerFunc(Function func) { + registerFunc(func.getName(), func); + return func; + } + + static Function registerStringFunc(Function func) { + registerFunc(func.getName(), func); + registerFunc(new StringFuncWrapper(func)); + return func; + } + + private static void registerFunc(String fname, Function func) { + String lookupFname = DatabaseImpl.toLookupName(fname); + if(FUNCS.put(lookupFname, func) != null) { + throw new IllegalStateException("Duplicate function " + fname); + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java new file mode 100644 index 0000000..4389d9f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java @@ -0,0 +1,195 @@ +/* +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 com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.NumberFormatter; +import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; + +/** + * + * @author James Ahlborn + */ +public class DefaultNumberFunctions +{ + + private DefaultNumberFunctions() {} + + static void init() { + // dummy method to ensure this class is loaded + } + + public static final Function ABS = registerFunc(new Func1NullIsNull("Abs") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + Value.Type mathType = param1.getType(); + + switch(mathType) { + case DATE: + case TIME: + case DATE_TIME: + // dates/times get converted to date doubles for arithmetic + double result = Math.abs(param1.getAsDouble()); + return BuiltinOperators.toDateValue(ctx, mathType, result, param1, null); + case LONG: + return BuiltinOperators.toValue(Math.abs(param1.getAsLongInt())); + case DOUBLE: + return BuiltinOperators.toValue(Math.abs(param1.getAsDouble())); + case STRING: + case BIG_DEC: + return BuiltinOperators.toValue(param1.getAsBigDecimal().abs( + NumberFormatter.DEC_MATH_CONTEXT)); + default: + throw new EvalException("Unexpected type " + mathType); + } + } + }); + + public static final Function ATAN = registerFunc(new Func1("Atan") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(Math.atan(param1.getAsDouble())); + } + }); + + public static final Function COS = registerFunc(new Func1("Cos") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(Math.cos(param1.getAsDouble())); + } + }); + + public static final Function EXP = registerFunc(new Func1("Exp") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(Math.exp(param1.getAsDouble())); + } + }); + + public static final Function FIX = registerFunc(new Func1NullIsNull("Fix") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + if(param1.getType().isIntegral()) { + return param1; + } + return BuiltinOperators.toValue(param1.getAsDouble().intValue()); + } + }); + + public static final Function INT = registerFunc(new Func1NullIsNull("Int") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + if(param1.getType().isIntegral()) { + return param1; + } + return BuiltinOperators.toValue((int)Math.floor(param1.getAsDouble())); + } + }); + + public static final Function LOG = registerFunc(new Func1("Log") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(Math.log(param1.getAsDouble())); + } + }); + + public static final Function RND = registerFunc(new FuncVar("Rnd", 0, 1) { + @Override + public boolean isPure() { + return false; + } + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Integer seed = ((params.length > 0) ? params[0].getAsLongInt() : null); + return BuiltinOperators.toValue(ctx.getRandom(seed)); + } + }); + + public static final Function ROUND = registerFunc(new FuncVar("Round", 1, 2) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + if(param1.isNull()) { + return null; + } + if(param1.getType().isIntegral()) { + return param1; + } + int scale = 0; + if(params.length > 1) { + scale = params[1].getAsLongInt(); + } + BigDecimal bd = param1.getAsBigDecimal() + .setScale(scale, NumberFormatter.ROUND_MODE); + return BuiltinOperators.toValue(bd); + } + }); + + public static final Function SGN = registerFunc(new Func1NullIsNull("Sgn") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int signum = 0; + if(param1.getType().isIntegral()) { + int lv = param1.getAsLongInt(); + signum = ((lv > 0) ? 1 : ((lv < 0) ? -1 : 0)); + } else { + signum = param1.getAsBigDecimal().signum(); + } + return BuiltinOperators.toValue(signum); + } + }); + + public static final Function SQR = registerFunc(new Func1("Sqr") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + double dv = param1.getAsDouble(); + if(dv < 0.0d) { + throw new EvalException("Invalid value '" + dv + "'"); + } + return BuiltinOperators.toValue(Math.sqrt(dv)); + } + }); + + public static final Function SIN = registerFunc(new Func1("Sin") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(Math.sin(param1.getAsDouble())); + } + }); + + public static final Function TAN = registerFunc(new Func1("Tan") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + return BuiltinOperators.toValue(Math.tan(param1.getAsDouble())); + } + }); + + + // public static final Function Val = registerFunc(new Func1("Val") { + // @Override + // protected Value eval1(EvalContext ctx, Value param1) { + // // FIXME, maybe leverage ExpressionTokenizer.maybeParseNumberLiteral (note, leading - or + is valid, exponent form is valid) + // } + // }); + + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java new file mode 100644 index 0000000..b419f70 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java @@ -0,0 +1,380 @@ +/* +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 com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.Value; +import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; + +/** + * + * @author James Ahlborn + */ +public class DefaultTextFunctions +{ + + private DefaultTextFunctions() {} + + static void init() { + // dummy method to ensure this class is loaded + } + + public static final Function ASC = registerFunc(new Func1("Asc") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + int len = str.length(); + if(len == 0) { + throw new EvalException("No characters in string"); + } + int lv = str.charAt(0); + if((lv < 0) || (lv > 255)) { + throw new EvalException("Character code '" + lv + + "' out of range "); + } + return BuiltinOperators.toValue(lv); + } + }); + + public static final Function ASCW = registerFunc(new Func1("AscW") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + int len = str.length(); + if(len == 0) { + throw new EvalException("No characters in string"); + } + int lv = str.charAt(0); + return BuiltinOperators.toValue(lv); + } + }); + + public static final Function CHR = registerStringFunc(new Func1NullIsNull("Chr") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int lv = param1.getAsLongInt(); + if((lv < 0) || (lv > 255)) { + throw new EvalException("Character code '" + lv + + "' out of range "); + } + char[] cs = Character.toChars(lv); + return BuiltinOperators.toValue(new String(cs)); + } + }); + + public static final Function CHRW = registerStringFunc(new Func1NullIsNull("ChrW") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int lv = param1.getAsLongInt(); + char[] cs = Character.toChars(lv); + return BuiltinOperators.toValue(new String(cs)); + } + }); + + public static final Function STR = registerStringFunc(new Func1NullIsNull("Str") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + BigDecimal bd = param1.getAsBigDecimal(); + String str = bd.toPlainString(); + if(bd.compareTo(BigDecimal.ZERO) >= 0) { + str = " " + str; + } + return BuiltinOperators.toValue(str); + } + }); + + public static final Function INSTR = registerFunc(new FuncVar("InStr", 2, 4) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + int idx = 0; + int start = 0; + if(params.length > 2) { + // 1 based offsets + start = params[0].getAsLongInt() - 1; + ++idx; + } + Value param1 = params[idx++]; + if(param1.isNull()) { + return param1; + } + String s1 = param1.getAsString(); + int s1Len = s1.length(); + if(s1Len == 0) { + return BuiltinOperators.ZERO_VAL; + } + Value param2 = params[idx++]; + if(param2.isNull()) { + return param2; + } + String s2 = param2.getAsString(); + int s2Len = s2.length(); + if(s2Len == 0) { + // 1 based offsets + return BuiltinOperators.toValue(start + 1); + } + boolean ignoreCase = true; + if(params.length > 3) { + ignoreCase = doIgnoreCase(params[3]); + } + int end = s1Len - s2Len; + while(start < end) { + if(s1.regionMatches(ignoreCase, start, s2, 0, s2Len)) { + // 1 based offsets + return BuiltinOperators.toValue(start + 1); + } + ++start; + } + return BuiltinOperators.ZERO_VAL; + } + }); + + public static final Function INSTRREV = registerFunc(new FuncVar("InStrRev", 2, 4) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + if(param1.isNull()) { + return param1; + } + String s1 = param1.getAsString(); + int s1Len = s1.length(); + if(s1Len == 0) { + return BuiltinOperators.ZERO_VAL; + } + Value param2 = params[1]; + if(param2.isNull()) { + return param2; + } + String s2 = param2.getAsString(); + int s2Len = s2.length(); + int start = s1Len - 1; + if(s2Len == 0) { + // 1 based offsets + return BuiltinOperators.toValue(start + 1); + } + if(params.length > 2) { + start = params[2].getAsLongInt(); + if(start == -1) { + start = s1Len; + } + // 1 based offsets + --start; + } + boolean ignoreCase = true; + if(params.length > 3) { + ignoreCase = doIgnoreCase(params[3]); + } + start = Math.min(s1Len - s2Len, start - s2Len + 1); + while(start >= 0) { + if(s1.regionMatches(ignoreCase, start, s2, 0, s2Len)) { + // 1 based offsets + return BuiltinOperators.toValue(start + 1); + } + --start; + } + return BuiltinOperators.ZERO_VAL; + } + }); + + public static final Function LCASE = registerStringFunc(new Func1NullIsNull("LCase") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue(str.toLowerCase()); + } + }); + + public static final Function UCASE = registerStringFunc(new Func1NullIsNull("UCase") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue(str.toUpperCase()); + } + }); + + public static final Function LEFT = registerStringFunc(new Func2("Left") { + @Override + protected Value eval2(EvalContext ctx, Value param1, Value param2) { + if(param1.isNull()) { + return param1; + } + String str = param1.getAsString(); + int len = Math.min(str.length(), param2.getAsLongInt()); + return BuiltinOperators.toValue(str.substring(0, len)); + } + }); + + public static final Function RIGHT = registerStringFunc(new Func2("Right") { + @Override + protected Value eval2(EvalContext ctx, Value param1, Value param2) { + if(param1.isNull()) { + return param1; + } + String str = param1.getAsString(); + int strLen = str.length(); + int len = Math.min(strLen, param2.getAsLongInt()); + return BuiltinOperators.toValue(str.substring(strLen - len, strLen)); + } + }); + + public static final Function MID = registerStringFunc(new FuncVar("Mid", 2, 3) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + if(param1.isNull()) { + return param1; + } + String str = param1.getAsString(); + int strLen = str.length(); + // 1 based offsets + int start = Math.max(strLen, params[1].getAsLongInt() - 1); + int len = Math.max( + ((params.length > 2) ? params[2].getAsLongInt() : strLen), + (strLen - start)); + return BuiltinOperators.toValue(str.substring(start, start + len)); + } + }); + + public static final Function LEN = registerFunc(new Func1NullIsNull("Len") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue(str.length()); + } + }); + + public static final Function LTRIM = registerStringFunc(new Func1NullIsNull("LTrim") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue(trim(str, true, false)); + } + }); + + public static final Function RTRIM = registerStringFunc(new Func1NullIsNull("RTrim") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue(trim(str, false, true)); + } + }); + + public static final Function TRIM = registerStringFunc(new Func1NullIsNull("Trim") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue(trim(str, true, true)); + } + }); + + public static final Function SPACE = registerStringFunc(new Func1("Space") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + int lv = param1.getAsLongInt(); + return BuiltinOperators.toValue(nchars(lv, ' ')); + } + }); + + public static final Function STRCOMP = registerFunc(new FuncVar("StrComp", 2, 3) { + @Override + protected Value evalVar(EvalContext ctx, Value[] params) { + Value param1 = params[0]; + Value param2 = params[1]; + if(param1.isNull() || param2.isNull()) { + return BuiltinOperators.NULL_VAL; + } + String s1 = param1.getAsString(); + String s2 = param2.getAsString(); + boolean ignoreCase = true; + if(params.length > 2) { + ignoreCase = doIgnoreCase(params[2]); + } + int cmp = (ignoreCase ? + s1.compareToIgnoreCase(s2) : s1.compareTo(s2)); + return BuiltinOperators.toValue(cmp); + } + }); + + public static final Function STRING = registerStringFunc(new Func2("String") { + @Override + protected Value eval2(EvalContext ctx, Value param1, Value param2) { + if(param1.isNull() || param2.isNull()) { + return BuiltinOperators.NULL_VAL; + } + int lv = param1.getAsLongInt(); + char c = (char)(param2.getAsString().charAt(0) % 256); + return BuiltinOperators.toValue(nchars(lv, c)); + } + }); + + public static final Function STRREVERSE = registerFunc(new Func1("StrReverse") { + @Override + protected Value eval1(EvalContext ctx, Value param1) { + String str = param1.getAsString(); + return BuiltinOperators.toValue( + new StringBuilder(str).reverse().toString()); + } + }); + + + private static String nchars(int num, char c) { + StringBuilder sb = new StringBuilder(num); + for(int i = 0; i < num; ++i) { + sb.append(c); + } + return sb.toString(); + } + + private static String trim(String str, boolean doLeft, boolean doRight) { + int start = 0; + int end = str.length(); + + if(doLeft) { + while((start < end) && (str.charAt(start) == ' ')) { + ++start; + } + } + if(doRight) { + while((start < end) && (str.charAt(end - 1) == ' ')) { + --end; + } + } + return str.substring(start, end); + } + + private static boolean doIgnoreCase(Value paramCmp) { + int cmpType = paramCmp.getAsLongInt(); + switch(cmpType) { + case -1: + // vbUseCompareOption -> default is binary + case 0: + // vbBinaryCompare + return false; + case 1: + // vbTextCompare + return true; + default: + // vbDatabaseCompare -> unsupported + throw new EvalException("Unsupported compare type " + cmpType); + } + } + + +} 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..7f68ad8 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.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 com.healthmarketscience.jackcess.impl.NumberFormatter; + +/** + * + * @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); + } + + @Override + public String getAsString() { + return NumberFormatter.format(_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..c2eb177 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -0,0 +1,658 @@ +/* +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.FieldPosition; +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; +import com.healthmarketscience.jackcess.expr.ParseException; + + +/** + * + * @author James Ahlborn + */ +class ExpressionTokenizer +{ + private static final int EOF = -1; + static final char QUOTED_STR_CHAR = '"'; + private static final char SINGLE_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: + + // all simple operator chars are single character operators + tokens.add(new Token(TokenType.OP, String.valueOf(c))); + break; + + case IS_COMP_FLAG: + + // special case for default values + if((exprType == Type.DEFAULT_VALUE) && (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; + } + + tokens.add(new Token(TokenType.OP, parseCompOp(c, buf))); + 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: + case SINGLE_QUOTED_STR_CHAR: + tokens.add(new Token(TokenType.LITERAL, null, + parseQuotedString(buf, c), Value.Type.STRING)); + break; + case DATE_LIT_QUOTE_CHAR: + tokens.add(parseDateLiteral(buf)); + break; + case OBJ_NAME_START_CHAR: + tokens.add(new Token(TokenType.OBJ_NAME, parseObjNameString(buf))); + break; + default: + throw new ParseException( + "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, char quoteChar) { + return parseStringUntil(buf, quoteChar, null, true); + } + + private static String parseObjNameString(ExprBuf buf) { + return parseStringUntil(buf, OBJ_NAME_END_CHAR, OBJ_NAME_START_CHAR, false); + } + + private static String parseDateLiteralString(ExprBuf buf) { + return parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null, false); + } + + private static String parseStringUntil(ExprBuf buf, char endChar, + Character startChar, + boolean allowDoubledEscape) + { + StringBuilder sb = buf.getScratchBuffer(); + + boolean complete = false; + while(buf.hasNext()) { + char c = buf.next(); + if(c == endChar) { + if(allowDoubledEscape && (buf.peekNext() == endChar)) { + sb.append(endChar); + buf.next(); + } else { + complete = true; + break; + } + } else if((startChar != null) && + (startChar == c)) { + throw new ParseException("Missing closing '" + endChar + + "' for quoted string " + buf); + } + + sb.append(c); + } + + if(!complete) { + throw new ParseException("Missing closing '" + endChar + + "' for quoted string " + buf); + } + + return sb.toString(); + } + + private static Token parseDateLiteral(ExprBuf buf) + { + TemporalConfig cfg = buf.getTemporalConfig(); + String dateStr = parseDateLiteralString(buf); + + boolean hasDate = (dateStr.indexOf(cfg.getDateSeparator()) >= 0); + boolean hasTime = (dateStr.indexOf(cfg.getTimeSeparator()) >= 0); + boolean hasAmPm = false; + + if(hasTime) { + int strLen = dateStr.length(); + hasAmPm = ((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 ParseException("Invalid date time literal " + dateStr + + " " + buf); + } + + try { + return new Token(TokenType.LITERAL, sdf.parse(dateStr), dateStr, valType, + sdf); + } catch(java.text.ParseException pe) { + throw new ParseException( + "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 { + Number num = null; + Value.Type numType = null; + + if(!isFp) { + try { + // try to parse as int. if that fails, fall back to BigDecimal + // (this will handle the case of int overflow) + num = Integer.valueOf(numStr); + numType = Value.Type.LONG; + } catch(NumberFormatException ne) { + // fallback to decimal + } + } + + if(num == null) { + num = new BigDecimal(numStr); + numType = Value.Type.BIG_DEC; + } + + foundNum = true; + return new Token(TokenType.LITERAL, num, numStr, numType); + } catch(NumberFormatException ne) { + throw new ParseException( + "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 ParseException("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..75b7950 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java @@ -0,0 +1,2142 @@ +/* +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.Collection; +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.EvalException; +import com.healthmarketscience.jackcess.expr.Expression; +import com.healthmarketscience.jackcess.expr.Function; +import com.healthmarketscience.jackcess.expr.Identifier; +import com.healthmarketscience.jackcess.expr.ParseException; +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, EXPRESSION, 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); + } + @Override public UnaryOp getUnaryNumOp() { + return UnaryOp.NEG_NUM; + } + }, + POS("+", false) { + @Override public Value eval(EvalContext ctx, Value param1) { + // basically a no-op + return param1; + } + @Override public UnaryOp getUnaryNumOp() { + return UnaryOp.POS_NUM; + } + }, + NOT("Not", true) { + @Override public Value eval(EvalContext ctx, Value param1) { + return BuiltinOperators.not(param1); + } + }, + // when a '-' immediately precedes a number, it needs "highest" precedence + NEG_NUM("-", false) { + @Override public Value eval(EvalContext ctx, Value param1) { + return BuiltinOperators.negate(ctx, param1); + } + }, + // when a '+' immediately precedes a number, it needs "highest" precedence + POS_NUM("+", false) { + @Override public Value eval(EvalContext ctx, Value param1) { + // basically a no-op + return 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 UnaryOp getUnaryNumOp() { + return null; + } + + 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[]{UnaryOp.NEG_NUM, UnaryOp.POS_NUM}, + new OpType[]{BinaryOp.EXP}, + new OpType[]{UnaryOp.NEG, UnaryOp.POS}, + 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() {} + + public static Expression parse(Type exprType, String exprStr, + Value.Type resultType, + ParseContext context) { + + if(context == null) { + context = DEFAULT_PARSE_CONTEXT; + } + + List<Token> tokens = trimSpaces( + ExpressionTokenizer.tokenize(exprType, exprStr, context)); + + if(tokens == null) { + throw new ParseException("null/empty expression"); + } + + TokBuf buf = new TokBuf(exprType, tokens, context); + + if(isLiteralDefaultValue(buf, resultType, exprStr)) { + + // this is handled as a literal string value, not an expression. no + // need to memo-ize cause it's a simple literal value + return new ExprWrapper( + new ELiteralValue(Value.Type.STRING, exprStr, null), resultType); + } + + // normal expression handling + Expr expr = parseExpression(buf, false); + + if((exprType == Type.FIELD_VALIDATOR) && !expr.isConditionalExpr()) { + // a non-conditional expression for a FIELD_VALIDATOR treats the result + // as an equality comparison with the field in question. so, transform + // the expression accordingly + expr = new EImplicitCompOp(expr); + } + + switch(exprType) { + case DEFAULT_VALUE: + case EXPRESSION: + return (expr.isConstant() ? + // for now, just cache at top-level for speed (could in theory + // cache intermediate values?) + new MemoizedExprWrapper(expr, resultType) : + new ExprWrapper(expr, resultType)); + case FIELD_VALIDATOR: + case RECORD_VALIDATOR: + return (expr.isConstant() ? + // for now, just cache at top-level for speed (could in theory + // cache intermediate values?) + new MemoizedCondExprWrapper(expr) : + new CondExprWrapper(expr)); + default: + throw new ParseException("unexpected expression type " + exprType); + } + } + + 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 ParseException("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 ParseException("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 ParseException("Unexpected STRING word type " + + wordType); + } + } + + break; + + case SPACE: + // top-level space is irrelevant (and we strip them anyway) + break; + + default: + throw new ParseException("unknown token type " + t); + } + + if(singleExpr && buf.hasPendingExpr()) { + break; + } + } + + Expr expr = buf.takePendingExpr(); + if(expr == null) { + throw new ParseException("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 '.'. Apparently '!' is actually a special late-bind + // operator (not sure it makes a difference for this code?), see: + // http://bytecomb.com/the-bang-exclamation-operator-in-vba/ + 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; + } + + int numNames = objNames.size(); + if(atSep || (numNames > 3)) { + throw new ParseException("Invalid object reference " + buf); + } + + // names are in reverse order + String propName = null; + if(numNames == 3) { + propName = objNames.poll(); + } + String objName = objNames.poll(); + String collectionName = objNames.poll(); + + buf.setPendingExpr( + new EObjValue(new Identifier(collectionName, objName, propName))); + } + + 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 ParseException("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 ParseException("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 ParseException("Missing closing '" + CLOSE_PAREN + + " " + buf); + } + + private static void parseOperatorExpression(Token t, TokBuf buf) { + + // most ops are two argument except that '-' could be negation, "+" could + // be pos-ation + if(buf.hasPendingExpr()) { + parseBinaryOpExpression(t, buf); + } else if(isEitherOp(t, "-", "+")) { + parseUnaryOpExpression(t, buf); + } else { + throw new ParseException( + "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); + + UnaryOp numOp = op.getUnaryNumOp(); + if(numOp != null) { + // if this operator is immediately preceding a number, it has a higher + // precedence + Token nextTok = buf.peekNext(); + if((nextTok != null) && (nextTok.getType() == TokenType.LITERAL) && + nextTok.getValueType().isNumeric()) { + op = numOp; + } + } + + 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 ParseException( + "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 ParseException( + "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 ParseException( + "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 ParseException("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 ParseException( + "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 ParseException("Malformed In expression " + buf); + } + + List<Expr> exprs = findParenExprs(buf, true); + specOpExpr = new EInOp(specOp, expr, exprs); + break; + + default: + throw new ParseException("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 ParseException( + "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 ParseException("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 isEitherOp(Token t, String opStr1, String opStr2) { + return ((t != null) && (t.getType() == TokenType.OP) && + (opStr1.equalsIgnoreCase(t.getValueStr()) || + opStr2.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 ParseException("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 TokBuf(Type exprType, List<Token> tokens, ParseContext context) { + this(exprType, tokens, null, 0, context); + } + + private TokBuf(List<Token> tokens, TokBuf parent, int parentOff) { + this(parent._exprType, tokens, parent, parentOff, parent._context); + } + + private TokBuf(Type exprType, List<Token> tokens, TokBuf parent, + int parentOff, ParseContext context) { + _exprType = exprType; + _tokens = tokens; + _parent = parent; + _parentOff = parentOff; + _context = context; + } + + public Type getExprType() { + return _exprType; + } + + 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 ParseException( + "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 ParseException( + "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 BuiltinOperators.toValue((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 BuiltinOperators.toValue((Integer)value); + case DOUBLE: + return BuiltinOperators.toValue((Double)value); + case BIG_DEC: + return BuiltinOperators.toValue((BigDecimal)value); + default: + throw new ParseException("unexpected literal type " + valType); + } + } + + private static boolean isLiteralDefaultValue( + TokBuf buf, Value.Type resultType, String exprStr) { + + // if a default value expression does not start with an '=' and is used in + // a string context, then it is taken as a literal value unless it starts + // with a " char + + if(buf.getExprType() != Type.DEFAULT_VALUE) { + return false; + } + + // a leading "=" indicates "full" expression handling for a DEFAULT_VALUE + // (consume this value once we detect it) + if(isOp(buf.peekNext(), "=")) { + buf.next(); + return false; + } + + return((resultType == Value.Type.STRING) && + ((exprStr.length() == 0) || + (exprStr.charAt(0) != ExpressionTokenizer.QUOTED_STR_CHAR))); + } + + 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 boolean isConditionalExpr() { + return false; + } + + 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); + + public abstract void collectIdentifiers(Collection<Identifier> identifiers); + + 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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + // none + } + + @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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + // none + } + @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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + // none + } + + @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 Identifier _identifier; + + private EObjValue(Identifier identifier) { + _identifier = identifier; + } + + @Override + public boolean isConstant() { + return false; + } + + @Override + public Value eval(EvalContext ctx) { + return ctx.getIdentifierValue(_identifier); + } + + @Override + public void collectIdentifiers(Collection<Identifier> identifiers) { + identifiers.add(_identifier); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append(_identifier); + } + } + + private static class EParen extends Expr + { + private final Expr _expr; + + private EParen(Expr expr) { + _expr = expr; + } + + @Override + public boolean isConstant() { + return _expr.isConstant(); + } + + @Override + protected boolean isConditionalExpr() { + return _expr.isConditionalExpr(); + } + + @Override + public Value eval(EvalContext ctx) { + return _expr.eval(ctx); + } + + @Override + public void collectIdentifiers(Collection<Identifier> identifiers) { + _expr.collectIdentifiers(identifiers); + } + + @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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + for(Expr param : _params) { + param.collectIdentifiers(identifiers); + } + } + + @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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + _left.collectIdentifiers(identifiers); + _right.collectIdentifiers(identifiers); + } + + @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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + _expr.collectIdentifiers(identifiers); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + sb.append(_op); + if(isDebug || ((UnaryOp)_op).needsSpace()) { + sb.append(" "); + } + _expr.toString(sb, isDebug); + } + } + + private static class ECompOp extends EBaseBinaryOp + { + private ECompOp(CompOp op, Expr left, Expr right) { + super(op, left, right); + } + + @Override + protected boolean isConditionalExpr() { + return true; + } + + @Override + public Value eval(EvalContext ctx) { + return ((CompOp)_op).eval(_left.eval(ctx), _right.eval(ctx)); + } + } + + private static class EImplicitCompOp extends ECompOp + { + private EImplicitCompOp(Expr right) { + super(CompOp.EQ, THIS_COL_VALUE, right); + } + + @Override + protected void toExprString(StringBuilder sb, boolean isDebug) { + // only output the full "implicit" comparison in debug mode + if(isDebug) { + super.toExprString(sb, isDebug); + } else { + // just output the explicit part of the expression + _right.toString(sb, isDebug); + } + } + } + + 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; + } + + @Override + public void collectIdentifiers(Collection<Identifier> identifiers) { + _expr.collectIdentifiers(identifiers); + } + + @Override + protected boolean isConditionalExpr() { + return true; + } + } + + 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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + for(Expr expr : _exprs) { + expr.collectIdentifiers(identifiers); + } + } + + @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 + public void collectIdentifiers(Collection<Identifier> identifiers) { + super.collectIdentifiers(identifiers); + _startRangeExpr.collectIdentifiers(identifiers); + _endRangeExpr.collectIdentifiers(identifiers); + } + + @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); + } + } + + /** + * Base Expression wrapper for an Expr. + */ + private static abstract class BaseExprWrapper implements Expression + { + private final Expr _expr; + + private BaseExprWrapper(Expr expr) { + _expr = expr; + } + + public String toDebugString() { + return _expr.toDebugString(); + } + + public boolean isConstant() { + return _expr.isConstant(); + } + + public void collectIdentifiers(Collection<Identifier> identifiers) { + _expr.collectIdentifiers(identifiers); + } + + @Override + public String toString() { + return _expr.toString(); + } + + protected Object evalValue(Value.Type resultType, EvalContext ctx) { + Value val = _expr.eval(ctx); + + if(val.isNull()) { + return null; + } + + 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.getAsLongInt(); + case DOUBLE: + return val.getAsDouble(); + case BIG_DEC: + return val.getAsBigDecimal(); + default: + throw new IllegalStateException("unexpected result type " + resultType); + } + } + + protected Boolean evalCondition(EvalContext ctx) { + Value val = _expr.eval(ctx); + + if(val.isNull()) { + // null can't be coerced to a boolean + throw new EvalException("Condition evaluated to Null"); + } + + return val.getAsBoolean(); + } + } + + /** + * Expression wrapper for an Expr which returns a value. + */ + private static class ExprWrapper extends BaseExprWrapper + { + private final Value.Type _resultType; + + private ExprWrapper(Expr expr, Value.Type resultType) { + super(expr); + _resultType = resultType; + } + + public Object eval(EvalContext ctx) { + return evalValue(_resultType, ctx); + } + } + + /** + * Expression wrapper for an Expr which returns a Boolean from a conditional + * expression. + */ + private static class CondExprWrapper extends BaseExprWrapper + { + private CondExprWrapper(Expr expr) { + super(expr); + } + + public Object eval(EvalContext ctx) { + return evalCondition(ctx); + } + } + + /** + * 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(Expr expr, Value.Type resultType) { + super(expr, resultType); + } + + @Override + public Object eval(EvalContext ctx) { + if(_val == null) { + _val = super.eval(ctx); + } + return _val; + } + } + + /** + * Expression wrapper for a <i>pure</i> conditional Expr which caches the + * result of evaluation. + */ + private static final class MemoizedCondExprWrapper extends CondExprWrapper + { + private Object _val; + + private MemoizedCondExprWrapper(Expr expr) { + super(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..3a47a84 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java @@ -0,0 +1,66 @@ +/* +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 Integer _val; + + public LongValue(Integer 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 Integer getAsLongInt() { + return _val; + } + + @Override + public BigDecimal getAsBigDecimal() { + return BigDecimal.valueOf(_val); + } + + @Override + public String getAsString() { + return _val.toString(); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/RandomContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/RandomContext.java new file mode 100644 index 0000000..71a3f6d --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/RandomContext.java @@ -0,0 +1,140 @@ +/* +Copyright (c) 2018 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 java.util.Random; + +/** + * This class effectively encapsulates the stateful logic of the "Rnd" + * function. + * + * @author James Ahlborn + */ +public class RandomContext +{ + private Source _defRnd; + private Map<Integer,Source> _rnds; + // default to the value access uses for "last val" when none has been + // returned yet + private float _lastVal = 1.953125E-02f; + + public RandomContext() + { + } + + public float getRandom(Integer seed) { + + if(seed == null) { + if(_defRnd == null) { + _defRnd = new SimpleSource(createRandom(System.currentTimeMillis())); + } + return _defRnd.get(); + } + + if(_rnds == null) { + // note, we don't use a SimpleCache here because if we discard a Random + // instance, that will cause the values to be reset + _rnds = new HashMap<Integer,Source>(); + } + + Source rnd = _rnds.get(seed); + if(rnd == null) { + + int seedInt = seed; + if(seedInt > 0) { + // normal random with a user specified seed + rnd = new SimpleSource(createRandom(seedInt)); + } else if(seedInt < 0) { + // returns the same value every time and resets all randoms + rnd = new ResetSource(createRandom(seedInt)); + } else { + // returns the last random value returned + rnd = new LastValSource(); + } + + _rnds.put(seed, rnd); + } + return rnd.get(); + } + + private float setLast(float lastVal) { + _lastVal = lastVal; + return lastVal; + } + + private void reset() { + if(_rnds != null) { + _rnds.clear(); + } + } + + private static Random createRandom(long seed) { + // TODO, support SecureRandom? + return new Random(seed); + } + + private abstract class Source + { + public float get() { + return setLast(getImpl()); + } + + protected abstract float getImpl(); + } + + private class SimpleSource extends Source + { + private final Random _rnd; + + private SimpleSource(Random rnd) { + _rnd = rnd; + } + + @Override + protected float getImpl() { + return _rnd.nextFloat(); + } + } + + private class ResetSource extends Source + { + private final float _val; + + private ResetSource(Random rnd) { + _val = rnd.nextFloat(); + } + + @Override + protected float getImpl() { + reset(); + return _val; + } + } + + private class LastValSource extends Source + { + private LastValSource() { + } + + @Override + protected float getImpl() { + return _lastVal; + } + } +} 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..014e371 --- /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 Integer getAsLongInt() { + return roundToLongInt(); + } + + @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 = BuiltinOperators.normalize(new BigDecimal(_val)); + return (BigDecimal)_num; + } catch(NumberFormatException nfe) { + _num = NOT_A_NUMBER; + // fall through to throw... + } + } + throw new NumberFormatException("Invalid number '" + _val + "'"); + } +} 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/PropertiesTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java index 7ca0521..056ca68 100644 --- a/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java @@ -21,9 +21,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.UUID; import static com.healthmarketscience.jackcess.Database.*; +import com.healthmarketscience.jackcess.InvalidValueException; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import com.healthmarketscience.jackcess.impl.PropertyMapImpl; @@ -44,7 +46,7 @@ public class PropertiesTest extends TestCase public void testPropertyMaps() throws Exception { - PropertyMaps maps = new PropertyMaps(10, null, null); + PropertyMaps maps = new PropertyMaps(10, null, null, null); assertTrue(maps.isEmpty()); assertEquals(0, maps.getSize()); assertFalse(maps.iterator().hasNext()); @@ -103,7 +105,7 @@ public class PropertiesTest extends TestCase public void testInferTypes() throws Exception { - PropertyMaps maps = new PropertyMaps(10, null, null); + PropertyMaps maps = new PropertyMaps(10, null, null, null); PropertyMap defMap = maps.getDefault(); assertEquals(DataType.TEXT, @@ -210,7 +212,8 @@ public class PropertiesTest extends TestCase for(Row row : ((DatabaseImpl)db).getSystemCatalog()) { int id = row.getInt("Id"); byte[] propBytes = row.getBytes("LvProp"); - PropertyMaps propMaps = ((DatabaseImpl)db).getPropertiesForObject(id); + PropertyMaps propMaps = ((DatabaseImpl)db).getPropertiesForObject( + id, null); int byteLen = ((propBytes != null) ? propBytes.length : 0); if(byteLen == 0) { assertTrue(propMaps.isEmpty()); @@ -403,9 +406,119 @@ public class PropertiesTest extends TestCase } } + public void testEnforceProperties() throws Exception + { + for(final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + + Table t = new TableBuilder("testReq") + .addColumn(new ColumnBuilder("id", DataType.LONG) + .setAutoNumber(true) + .putProperty(PropertyMap.REQUIRED_PROP, true)) + .addColumn(new ColumnBuilder("value", DataType.TEXT) + .putProperty(PropertyMap.REQUIRED_PROP, true)) + .toTable(db); + + t.addRow(Column.AUTO_NUMBER, "v1"); + + try { + t.addRow(Column.AUTO_NUMBER, null); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException expected) { + // success + } + + t.addRow(Column.AUTO_NUMBER, ""); + + List<? extends Map<String, Object>> expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "value", "v1"), + createExpectedRow( + "id", 2, + "value", "")); + assertTable(expectedRows, t); + + + t = new TableBuilder("testNz") + .addColumn(new ColumnBuilder("id", DataType.LONG) + .setAutoNumber(true) + .putProperty(PropertyMap.REQUIRED_PROP, true)) + .addColumn(new ColumnBuilder("value", DataType.TEXT) + .putProperty(PropertyMap.ALLOW_ZERO_LEN_PROP, false)) + .toTable(db); + + t.addRow(Column.AUTO_NUMBER, "v1"); + + try { + t.addRow(Column.AUTO_NUMBER, ""); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException expected) { + // success + } + + t.addRow(Column.AUTO_NUMBER, null); + + expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "value", "v1"), + createExpectedRow( + "id", 2, + "value", null)); + assertTable(expectedRows, t); + + + t = new TableBuilder("testReqNz") + .addColumn(new ColumnBuilder("id", DataType.LONG) + .setAutoNumber(true) + .putProperty(PropertyMap.REQUIRED_PROP, true)) + .addColumn(new ColumnBuilder("value", DataType.TEXT)) + .toTable(db); + + Column col = t.getColumn("value"); + PropertyMap props = col.getProperties(); + props.put(PropertyMap.REQUIRED_PROP, true); + props.put(PropertyMap.ALLOW_ZERO_LEN_PROP, false); + props.save(); + + t.addRow(Column.AUTO_NUMBER, "v1"); + + try { + t.addRow(Column.AUTO_NUMBER, ""); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException expected) { + // success + } + + try { + t.addRow(Column.AUTO_NUMBER, null); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException expected) { + // success + } + + t.addRow(Column.AUTO_NUMBER, "v2"); + + expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "value", "v1"), + createExpectedRow( + "id", 2, + "value", "v2")); + assertTable(expectedRows, t); + + db.close(); + } + } + public void testEnumValues() throws Exception { - PropertyMaps maps = new PropertyMaps(10, null, null); + PropertyMaps maps = new PropertyMaps(10, null, null, null); PropertyMapImpl colMap = maps.get("testcol"); diff --git a/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java new file mode 100644 index 0000000..5d3fb44 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java @@ -0,0 +1,283 @@ +/* +Copyright (c) 2018 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; + +import java.util.List; + +import junit.framework.TestCase; + +import static com.healthmarketscience.jackcess.Database.*; +import static com.healthmarketscience.jackcess.TestUtil.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; + +/** + * + * @author James Ahlborn + */ +public class PropertyExpressionTest extends TestCase +{ + + public PropertyExpressionTest(String name) { + super(name); + } + + public void testDefaultValue() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + db.setEvaluateExpressions(true); + + Table t = new TableBuilder("test") + .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true)) + .addColumn(new ColumnBuilder("data1", DataType.TEXT) + .putProperty(PropertyMap.DEFAULT_VALUE_PROP, + "=\"FOO \" & \"BAR\"")) + .addColumn(new ColumnBuilder("data2", DataType.LONG) + .putProperty(PropertyMap.DEFAULT_VALUE_PROP, + "37")) + .toTable(db); + + t.addRow(Column.AUTO_NUMBER, null, 13); + t.addRow(Column.AUTO_NUMBER, "blah", null); + + setProp(t, "data1", PropertyMap.DEFAULT_VALUE_PROP, null); + setProp(t, "data2", PropertyMap.DEFAULT_VALUE_PROP, "42"); + + t.addRow(Column.AUTO_NUMBER, null, null); + + List<Row> expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "data1", "FOO BAR", + "data2", 13), + createExpectedRow( + "id", 2, + "data1", "blah", + "data2", 37), + createExpectedRow( + "id", 3, + "data1", null, + "data2", 42)); + + assertTable(expectedRows, t); + + db.close(); + } + } + + public void testCalculatedValue() throws Exception + { + Database db = create(FileFormat.V2016); + db.setEvaluateExpressions(true); + + Table t = new TableBuilder("test") + .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true)) + .addColumn(new ColumnBuilder("c1", DataType.LONG) + .setCalculatedInfo("[c2]+[c3]")) + .addColumn(new ColumnBuilder("c2", DataType.LONG) + .setCalculatedInfo("[c3]*5")) + .addColumn(new ColumnBuilder("c3", DataType.LONG) + .setCalculatedInfo("[c4]-6")) + .addColumn(new ColumnBuilder("c4", DataType.LONG)) + .toTable(db); + + t.addRow(Column.AUTO_NUMBER, null, null, null, 16); + + setProp(t, "c1", PropertyMap.EXPRESSION_PROP, "[c4]+2"); + setProp(t, "c2", PropertyMap.EXPRESSION_PROP, "[c1]+[c3]"); + setProp(t, "c3", PropertyMap.EXPRESSION_PROP, "[c1]*7"); + + t.addRow(Column.AUTO_NUMBER, null, null, null, 7); + + List<Row> expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "c1", 60, + "c2", 50, + "c3", 10, + "c4", 16), + createExpectedRow( + "id", 2, + "c1", 9, + "c2", 72, + "c3", 63, + "c4", 7)); + + assertTable(expectedRows, t); + + db.close(); + } + + public void testColumnValidator() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + db.setEvaluateExpressions(true); + + Table t = new TableBuilder("test") + .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true)) + .addColumn(new ColumnBuilder("data1", DataType.LONG) + .putProperty(PropertyMap.VALIDATION_RULE_PROP, + ">37")) + .addColumn(new ColumnBuilder("data2", DataType.LONG) + .putProperty(PropertyMap.VALIDATION_RULE_PROP, + "between 7 and 10") + .putProperty(PropertyMap.VALIDATION_TEXT_PROP, + "You failed")) + .toTable(db); + + t.addRow(Column.AUTO_NUMBER, 42, 8); + + try { + t.addRow(Column.AUTO_NUMBER, 42, 20); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException ive) { + // success + assertTrue(ive.getMessage().contains("You failed")); + } + + try { + t.addRow(Column.AUTO_NUMBER, 3, 8); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException ive) { + // success + assertFalse(ive.getMessage().contains("You failed")); + } + + t.addRow(Column.AUTO_NUMBER, 54, 9); + + setProp(t, "data1", PropertyMap.VALIDATION_RULE_PROP, null); + setProp(t, "data2", PropertyMap.VALIDATION_RULE_PROP, "<100"); + setProp(t, "data2", PropertyMap.VALIDATION_TEXT_PROP, "Too big"); + + try { + t.addRow(Column.AUTO_NUMBER, 42, 200); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException ive) { + // success + assertTrue(ive.getMessage().contains("Too big")); + } + + t.addRow(Column.AUTO_NUMBER, 1, 9); + + List<Row> expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "data1", 42, + "data2", 8), + createExpectedRow( + "id", 2, + "data1", 54, + "data2", 9), + createExpectedRow( + "id", 3, + "data1", 1, + "data2", 9)); + + assertTable(expectedRows, t); + + db.close(); + } + } + + public void testRowValidator() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + db.setEvaluateExpressions(true); + + Table t = new TableBuilder("test") + .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true)) + .addColumn(new ColumnBuilder("data1", DataType.LONG)) + .addColumn(new ColumnBuilder("data2", DataType.LONG)) + .putProperty(PropertyMap.VALIDATION_RULE_PROP, + "([data1] > 10) and ([data2] < 100)") + .putProperty(PropertyMap.VALIDATION_TEXT_PROP, + "You failed") + .toTable(db); + + t.addRow(Column.AUTO_NUMBER, 42, 8); + + try { + t.addRow(Column.AUTO_NUMBER, 1, 20); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException ive) { + // success + assertTrue(ive.getMessage().contains("You failed")); + } + + t.addRow(Column.AUTO_NUMBER, 54, 9); + + setTableProp(t, PropertyMap.VALIDATION_RULE_PROP, "[data2]<100"); + setTableProp(t, PropertyMap.VALIDATION_TEXT_PROP, "Too big"); + + try { + t.addRow(Column.AUTO_NUMBER, 42, 200); + fail("InvalidValueException should have been thrown"); + } catch(InvalidValueException ive) { + // success + assertTrue(ive.getMessage().contains("Too big")); + } + + t.addRow(Column.AUTO_NUMBER, 1, 9); + + List<Row> expectedRows = + createExpectedTable( + createExpectedRow( + "id", 1, + "data1", 42, + "data2", 8), + createExpectedRow( + "id", 2, + "data1", 54, + "data2", 9), + createExpectedRow( + "id", 3, + "data1", 1, + "data2", 9)); + + assertTable(expectedRows, t); + + db.close(); + } + } + + private static void setProp(Table t, String colName, String propName, + String propVal) throws Exception { + PropertyMap props = t.getColumn(colName).getProperties(); + if(propVal != null) { + props.put(propName, propVal); + } else { + props.remove(propName); + } + props.save(); + } + + private static void setTableProp(Table t, String propName, + String propVal) throws Exception { + PropertyMap props = t.getProperties(); + if(propVal != null) { + props.put(propName, propVal); + } else { + props.remove(propName); + } + props.save(); + } +} diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java index 3317c7f..7680fb3 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java +++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java @@ -53,12 +53,12 @@ import org.junit.Assert; * * @author James Ahlborn */ -public class TestUtil +public class TestUtil { public static final TimeZone TEST_TZ = TimeZone.getTimeZone("America/New_York"); - - private static final ThreadLocal<Boolean> _autoSync = + + private static final ThreadLocal<Boolean> _autoSync = new ThreadLocal<Boolean>(); private TestUtil() {} @@ -76,22 +76,22 @@ public class TestUtil return ((autoSync != null) ? autoSync : Database.DEFAULT_AUTO_SYNC); } - public static Database open(FileFormat fileFormat, File file) - throws Exception + public static Database open(FileFormat fileFormat, File file) + throws Exception { return open(fileFormat, file, false); } - public static Database open(FileFormat fileFormat, File file, boolean inMem) - throws Exception + public static Database open(FileFormat fileFormat, File file, boolean inMem) + throws Exception { FileChannel channel = (inMem ? MemFileChannel.newChannel( - file, DatabaseImpl.RW_CHANNEL_MODE) + file, DatabaseImpl.RW_CHANNEL_MODE) : null); final Database db = new DatabaseBuilder(file).setReadOnly(true) .setAutoSync(getTestAutoSync()).setChannel(channel).open(); - Assert.assertEquals("Wrong JetFormat.", - DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(), + Assert.assertEquals("Wrong JetFormat.", + DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(), ((DatabaseImpl)db).getFormat()); Assert.assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat()); return db; @@ -109,8 +109,8 @@ public class TestUtil return create(fileFormat, false); } - public static Database create(FileFormat fileFormat, boolean keep) - throws Exception + public static Database create(FileFormat fileFormat, boolean keep) + throws Exception { return create(fileFormat, keep, false); } @@ -119,9 +119,9 @@ public class TestUtil return create(fileFormat, false, true); } - private static Database create(FileFormat fileFormat, boolean keep, - boolean inMem) - throws Exception + private static Database create(FileFormat fileFormat, boolean keep, + boolean inMem) + throws Exception { FileChannel channel = (inMem ? MemFileChannel.newChannel() : null); @@ -147,7 +147,7 @@ public class TestUtil ByteUtil.closeQuietly(outStream); } } - + return new DatabaseBuilder(createTempFile(keep)).setFileFormat(fileFormat) .setAutoSync(getTestAutoSync()).setChannel(channel).create(); } @@ -176,7 +176,7 @@ public class TestUtil File tmp = createTempFile(keep); copyFile(file, tmp); Database db = new DatabaseBuilder(tmp).setAutoSync(getTestAutoSync()).open(); - Assert.assertEquals("Wrong JetFormat.", + Assert.assertEquals("Wrong JetFormat.", DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(), ((DatabaseImpl)db).getFormat()); Assert.assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat()); @@ -192,7 +192,7 @@ public class TestUtil public static Object[] createTestRow() { return createTestRow("Tim"); } - + static Map<String,Object> createTestRowMap(String col1Val) { return createExpectedRow("A", col1Val, "B", "R", "C", "McCune", "D", 1234, "E", (byte) 0xad, "F", 555.66d, @@ -220,7 +220,7 @@ public class TestUtil static String createNonAsciiString(int len) { return createString(len, '\u0CC0'); } - + private static String createString(int len, char firstChar) { StringBuilder builder = new StringBuilder(len); for(int i = 0; i < len; ++i) { @@ -235,7 +235,7 @@ public class TestUtil Assert.assertEquals(expectedRowCount, countRows(table)); Assert.assertEquals(expectedRowCount, table.getRowCount()); } - + public static int countRows(Table table) throws Exception { int rtn = 0; for(Map<String, Object> row : CursorBuilder.createCursor(table)) { @@ -245,15 +245,15 @@ public class TestUtil } public static void assertTable( - List<? extends Map<String, Object>> expectedTable, + List<? extends Map<String, Object>> expectedTable, Table table) throws IOException { assertCursor(expectedTable, CursorBuilder.createCursor(table)); } - + public static void assertCursor( - List<? extends Map<String, Object>> expectedTable, + List<? extends Map<String, Object>> expectedTable, Cursor cursor) { List<Map<String, Object>> foundTable = @@ -264,9 +264,9 @@ public class TestUtil Assert.assertEquals(expectedTable.size(), foundTable.size()); for(int i = 0; i < expectedTable.size(); ++i) { Assert.assertEquals(expectedTable.get(i), foundTable.get(i)); - } + } } - + public static RowImpl createExpectedRow(Object... rowElements) { RowImpl row = new RowImpl((RowIdImpl)null); for(int i = 0; i < rowElements.length; i += 2) { @@ -274,12 +274,12 @@ public class TestUtil rowElements[i + 1]); } return row; - } + } public static List<Row> createExpectedTable(Row... rows) { return Arrays.<Row>asList(rows); - } - + } + public static void dumpDatabase(Database mdb) throws Exception { dumpDatabase(mdb, false); } @@ -313,7 +313,7 @@ public class TestUtil for(Index index : table.getIndexes()) { ((IndexImpl)index).initialize(); } - + writer.println("TABLE: " + table.getName()); List<String> colNames = new ArrayList<String>(); for(Column col : table.getColumns()) { @@ -377,25 +377,33 @@ public class TestUtil "), found " + foundTime + " (" + found + ")"); } } - + static void copyFile(File srcFile, File dstFile) throws IOException { // FIXME should really be using commons io FileUtils here, but don't want // to add dep for one simple test method - byte[] buf = new byte[1024]; OutputStream ostream = new FileOutputStream(dstFile); InputStream istream = new FileInputStream(srcFile); try { - int numBytes = 0; - while((numBytes = istream.read(buf)) >= 0) { - ostream.write(buf, 0, numBytes); - } + copyStream(istream, ostream); } finally { ostream.close(); } } + static void copyStream(InputStream istream, OutputStream ostream) + throws IOException + { + // FIXME should really be using commons io FileUtils here, but don't want + // to add dep for one simple test method + byte[] buf = new byte[1024]; + int numBytes = 0; + while((numBytes = istream.read(buf)) >= 0) { + ostream.write(buf, 0, numBytes); + } + } + static File createTempFile(boolean keep) throws Exception { File tmp = File.createTempFile("databaseTest", ".mdb"); if(keep) { @@ -416,7 +424,7 @@ public class TestUtil val = f.get(val); ((Map<?,?>)val).clear(); } - + public static byte[] toByteArray(File file) throws IOException { diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java new file mode 100644 index 0000000..f69dca1 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java @@ -0,0 +1,106 @@ +/* +Copyright (c) 2018 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; + + +import java.math.BigDecimal; + +import junit.framework.TestCase; + + +/** + * + * @author James Ahlborn + */ +public class NumberFormatterTest extends TestCase +{ + + public NumberFormatterTest(String name) { + super(name); + } + + public void testDoubleFormat() throws Exception + { + assertEquals("894984737284944", NumberFormatter.format(894984737284944d)); + assertEquals("-894984737284944", NumberFormatter.format(-894984737284944d)); + assertEquals("8949.84737284944", NumberFormatter.format(8949.84737284944d)); + assertEquals("8949847372844", NumberFormatter.format(8949847372844d)); + assertEquals("8949.847384944", NumberFormatter.format(8949.847384944d)); + assertEquals("8.94985647372849E+16", NumberFormatter.format(89498564737284944d)); + assertEquals("-8.94985647372849E+16", NumberFormatter.format(-89498564737284944d)); + assertEquals("895649.847372849", NumberFormatter.format(895649.84737284944d)); + assertEquals("300", NumberFormatter.format(300d)); + assertEquals("-300", NumberFormatter.format(-300d)); + assertEquals("0.3", NumberFormatter.format(0.3d)); + assertEquals("0.1", NumberFormatter.format(0.1d)); + assertEquals("2.3423421E-12", NumberFormatter.format(0.0000000000023423421d)); + assertEquals("2.3423421E-11", NumberFormatter.format(0.000000000023423421d)); + assertEquals("2.3423421E-10", NumberFormatter.format(0.00000000023423421d)); + assertEquals("-2.3423421E-10", NumberFormatter.format(-0.00000000023423421d)); + assertEquals("2.34234214E-12", NumberFormatter.format(0.00000000000234234214d)); + assertEquals("2.342342156E-12", NumberFormatter.format(0.000000000002342342156d)); + assertEquals("0.000000023423421", NumberFormatter.format(0.000000023423421d)); + assertEquals("2.342342133E-07", NumberFormatter.format(0.0000002342342133d)); + assertEquals("1.#INF", NumberFormatter.format(Double.POSITIVE_INFINITY)); + assertEquals("-1.#INF", NumberFormatter.format(Double.NEGATIVE_INFINITY)); + assertEquals("1.#QNAN", NumberFormatter.format(Double.NaN)); + } + + public void testFloatFormat() throws Exception + { + assertEquals("8949847", NumberFormatter.format(8949847f)); + assertEquals("-8949847", NumberFormatter.format(-8949847f)); + assertEquals("8949.847", NumberFormatter.format(8949.847f)); + assertEquals("894984", NumberFormatter.format(894984f)); + assertEquals("8949.84", NumberFormatter.format(8949.84f)); + assertEquals("8.949856E+16", NumberFormatter.format(89498564737284944f)); + assertEquals("-8.949856E+16", NumberFormatter.format(-89498564737284944f)); + assertEquals("895649.9", NumberFormatter.format(895649.84737284944f)); + assertEquals("300", NumberFormatter.format(300f)); + assertEquals("-300", NumberFormatter.format(-300f)); + assertEquals("0.3", NumberFormatter.format(0.3f)); + assertEquals("0.1", NumberFormatter.format(0.1f)); + assertEquals("2.342342E-12", NumberFormatter.format(0.0000000000023423421f)); + assertEquals("2.342342E-11", NumberFormatter.format(0.000000000023423421f)); + assertEquals("2.342342E-10", NumberFormatter.format(0.00000000023423421f)); + assertEquals("-2.342342E-10", NumberFormatter.format(-0.00000000023423421f)); + assertEquals("2.342342E-12", NumberFormatter.format(0.00000000000234234214f)); + assertEquals("2.342342E-12", NumberFormatter.format(0.000000000002342342156f)); + assertEquals("0.0000234", NumberFormatter.format(0.0000234f)); + assertEquals("2.342E-05", NumberFormatter.format(0.00002342f)); + assertEquals("1.#INF", NumberFormatter.format(Float.POSITIVE_INFINITY)); + assertEquals("-1.#INF", NumberFormatter.format(Float.NEGATIVE_INFINITY)); + assertEquals("1.#QNAN", NumberFormatter.format(Float.NaN)); + } + + public void testDecimalFormat() throws Exception + { + assertEquals("9874539485972.2342342234234", NumberFormatter.format(new BigDecimal("9874539485972.2342342234234"))); + assertEquals("9874539485972.234234223423468", NumberFormatter.format(new BigDecimal("9874539485972.2342342234234678"))); + assertEquals("-9874539485972.234234223423468", NumberFormatter.format(new BigDecimal("-9874539485972.2342342234234678"))); + assertEquals("9.874539485972234234223423468E+31", NumberFormatter.format(new BigDecimal("98745394859722342342234234678000"))); + assertEquals("9.874539485972234234223423468E+31", NumberFormatter.format(new BigDecimal("98745394859722342342234234678000"))); + assertEquals("-9.874539485972234234223423468E+31", NumberFormatter.format(new BigDecimal("-98745394859722342342234234678000"))); + assertEquals("300", NumberFormatter.format(new BigDecimal("300.0"))); + assertEquals("-300", NumberFormatter.format(new BigDecimal("-300.000"))); + assertEquals("0.3", NumberFormatter.format(new BigDecimal("0.3"))); + assertEquals("0.1", NumberFormatter.format(new BigDecimal("0.1000"))); + assertEquals("0.0000000000023423428930458", NumberFormatter.format(new BigDecimal("0.0000000000023423428930458"))); + assertEquals("2.3423428930458389038451E-12", NumberFormatter.format(new BigDecimal("0.0000000000023423428930458389038451"))); + assertEquals("2.342342893045838903845134766E-12", NumberFormatter.format(new BigDecimal("0.0000000000023423428930458389038451347656"))); + } +} diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java new file mode 100644 index 0000000..61acacb --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java @@ -0,0 +1,166 @@ +/* +Copyright (c) 2018 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * + * @author James Ahlborn + */ +public class TopoSorterTest extends TestCase +{ + + public TopoSorterTest(String name) { + super(name); + } + + public void testTopoSort() throws Exception + { + doTopoTest(Arrays.asList("A", "B", "C"), + Arrays.asList("A", "B", "C")); + + doTopoTest(Arrays.asList("B", "A", "C"), + Arrays.asList("A", "B", "C"), + "B", "C", + "A", "B"); + + try { + doTopoTest(Arrays.asList("B", "A", "C"), + Arrays.asList("C", "B", "A"), + "B", "C", + "A", "B", + "C", "A"); + fail("IllegalStateException should have been thrown"); + } catch(IllegalStateException expected) { + // success + assertTrue(expected.getMessage().startsWith("Cycle")); + } + + try { + doTopoTest(Arrays.asList("B", "A", "C"), + Arrays.asList("C", "B", "A"), + "B", "D"); + fail("IllegalStateException should have been thrown"); + } catch(IllegalStateException expected) { + // success + assertTrue(expected.getMessage().startsWith("Unknown descendent")); + } + + doTopoTest(Arrays.asList("B", "D", "A", "C"), + Arrays.asList("D", "A", "B", "C"), + "B", "C", + "A", "B"); + + doTopoTest(Arrays.asList("B", "D", "A", "C"), + Arrays.asList("A", "D", "B", "C"), + "B", "C", + "A", "B", + "A", "D"); + + doTopoTest(Arrays.asList("B", "D", "A", "C"), + Arrays.asList("D", "A", "C", "B"), + "D", "A", + "C", "B"); + + doTopoTest(Arrays.asList("B", "D", "A", "C"), + Arrays.asList("D", "C", "A", "B"), + "D", "A", + "C", "B", + "C", "A"); + + doTopoTest(Arrays.asList("B", "D", "A", "C"), + Arrays.asList("C", "D", "A", "B"), + "D", "A", + "C", "B", + "C", "D"); + + doTopoTest(Arrays.asList("B", "D", "A", "C"), + Arrays.asList("D", "A", "C", "B"), + "D", "A", + "C", "B", + "D", "B"); + } + + private static void doTopoTest(List<String> original, + List<String> expected, + String... descs) { + + List<String> values = new ArrayList<String>(); + values.addAll(original); + + TestTopoSorter tsorter = new TestTopoSorter(values, false); + for(int i = 0; i < descs.length; i+=2) { + tsorter.addDescendents(descs[i], descs[i+1]); + } + + tsorter.sort(); + + assertEquals(expected, values); + + + values = new ArrayList<String>(); + values.addAll(original); + + tsorter = new TestTopoSorter(values, true); + for(int i = 0; i < descs.length; i+=2) { + tsorter.addDescendents(descs[i], descs[i+1]); + } + + tsorter.sort(); + + List<String> expectedReverse = new ArrayList<String>(expected); + Collections.reverse(expectedReverse); + + assertEquals(expectedReverse, values); + } + + private static class TestTopoSorter extends TopoSorter<String> + { + private final Map<String,List<String>> _descMap = + new HashMap<String,List<String>>(); + + protected TestTopoSorter(List<String> values, boolean reverse) { + super(values, reverse); + } + + public void addDescendents(String from, String... tos) { + List<String> descs = _descMap.get(from); + if(descs == null) { + descs = new ArrayList<String>(); + _descMap.put(from, descs); + } + + descs.addAll(Arrays.asList(tos)); + } + + @Override + protected void getDescendents(String from, List<String> descendents) { + List<String> descs = _descMap.get(from); + if(descs != null) { + descendents.addAll(descs); + } + } + } +} diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java new file mode 100644 index 0000000..0b02888 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -0,0 +1,221 @@ +/* +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 com.healthmarketscience.jackcess.expr.EvalException; +import junit.framework.TestCase; +import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.eval; + +/** + * + * @author James Ahlborn + */ +public class DefaultFunctionsTest extends TestCase +{ + + public DefaultFunctionsTest(String name) { + super(name); + } + + public void testFuncs() throws Exception + { + assertEquals("foo", eval("=IIf(10 > 1, \"foo\", \"bar\")")); + assertEquals("bar", eval("=IIf(10 < 1, \"foo\", \"bar\")")); + assertEquals(102, eval("=Asc(\"foo\")")); + assertEquals(9786, eval("=AscW(\"\u263A\")")); + assertEquals("f", eval("=Chr(102)")); + assertEquals("\u263A", eval("=ChrW(9786)")); + assertEquals("263A", eval("=Hex(9786)")); + + assertEquals("blah", eval("=Nz(\"blah\")")); + assertEquals("", eval("=Nz(Null)")); + assertEquals("blah", eval("=Nz(\"blah\",\"FOO\")")); + assertEquals("FOO", eval("=Nz(Null,\"FOO\")")); + + assertEquals("23072", eval("=Oct(9786)")); + assertEquals(" 9786", eval("=Str(9786)")); + assertEquals("-42", eval("=Str(-42)")); + assertEquals("-42", eval("=Str$(-42)")); + assertNull(eval("=Str(Null)")); + + try { + eval("=Str$(Null)"); + fail("EvalException should have been thrown"); + } catch(EvalException expected) { + // success + } + + assertEquals(-1, eval("=CBool(\"1\")")); + assertEquals(13, eval("=CByte(\"13\")")); + assertEquals(14, eval("=CByte(\"13.7\")")); + assertEquals(new BigDecimal("57.1235"), eval("=CCur(\"57.12346\")")); + assertEquals(new Double("57.12345"), eval("=CDbl(\"57.12345\")")); + assertEquals(new BigDecimal("57.123456789"), eval("=CDec(\"57.123456789\")")); + assertEquals(513, eval("=CInt(\"513\")")); + assertEquals(514, eval("=CInt(\"513.7\")")); + assertEquals(345513, eval("=CLng(\"345513\")")); + assertEquals(345514, eval("=CLng(\"345513.7\")")); + assertEquals(new Float("57.12345").doubleValue(), + eval("=CSng(\"57.12345\")")); + assertEquals("9786", eval("=CStr(9786)")); + assertEquals("-42", eval("=CStr(-42)")); + + assertEquals(2, eval("=InStr('AFOOBAR', 'FOO')")); + assertEquals(2, eval("=InStr('AFOOBAR', 'foo')")); + assertEquals(2, eval("=InStr(1, 'AFOOBAR', 'foo')")); + assertEquals(0, eval("=InStr(1, 'AFOOBAR', 'foo', 0)")); + assertEquals(2, eval("=InStr(1, 'AFOOBAR', 'foo', 1)")); + assertEquals(2, eval("=InStr(1, 'AFOOBAR', 'FOO', 0)")); + assertEquals(2, eval("=InStr(2, 'AFOOBAR', 'FOO')")); + assertEquals(0, eval("=InStr(3, 'AFOOBAR', 'FOO')")); + assertEquals(0, eval("=InStr(17, 'AFOOBAR', 'FOO')")); + assertEquals(2, eval("=InStr(1, 'AFOOBARFOOBAR', 'FOO')")); + assertEquals(8, eval("=InStr(3, 'AFOOBARFOOBAR', 'FOO')")); + assertNull(eval("=InStr(3, Null, 'FOO')")); + + assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO')")); + assertEquals(2, eval("=InStrRev('AFOOBAR', 'foo')")); + assertEquals(2, eval("=InStrRev('AFOOBAR', 'foo', -1)")); + assertEquals(0, eval("=InStrRev('AFOOBAR', 'foo', -1, 0)")); + assertEquals(2, eval("=InStrRev('AFOOBAR', 'foo', -1, 1)")); + assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO', -1, 0)")); + assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO', 4)")); + assertEquals(0, eval("=InStrRev('AFOOBAR', 'FOO', 3)")); + assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO', 17)")); + assertEquals(2, eval("=InStrRev('AFOOBARFOOBAR', 'FOO', 9)")); + assertEquals(8, eval("=InStrRev('AFOOBARFOOBAR', 'FOO', 10)")); + assertNull(eval("=InStrRev(Null, 'FOO', 3)")); + + assertEquals("FOOO", eval("=UCase(\"fOoO\")")); + assertEquals("fooo", eval("=LCase(\"fOoO\")")); + + assertEquals("bl", eval("=Left(\"blah\", 2)")); + assertEquals("", eval("=Left(\"blah\", 0)")); + assertEquals("blah", eval("=Left(\"blah\", 17)")); + + assertEquals("ah", eval("=Right(\"blah\", 2)")); + assertEquals("", eval("=Right(\"blah\", 0)")); + assertEquals("blah", eval("=Right(\"blah\", 17)")); + + } + + + public void testFinancialFuncs() throws Exception + { + assertEquals("-9.57859403981317", + eval("=CStr(NPer(0.12/12,-100,-1000))")); + assertEquals("-9.48809500550583", + eval("=CStr(NPer(0.12/12,-100,-1000,0,1))")); + assertEquals("60.0821228537617", + eval("=CStr(NPer(0.12/12,-100,-1000,10000))")); + assertEquals("59.6738656742946", + eval("=CStr(NPer(0.12/12,-100,-1000,10000,1))")); + assertEquals("69.6607168935748", + eval("=CStr(NPer(0.12/12,-100,0,10000))")); + assertEquals("69.1619606798004", + eval("=CStr(NPer(0.12/12,-100,0,10000,1))")); + + assertEquals("8166.96698564091", + eval("=CStr(FV(0.12/12,60,-100))")); + assertEquals("8248.63665549732", + eval("=CStr(FV(0.12/12,60,-100,0,1))")); + assertEquals("6350.27028707682", + eval("=CStr(FV(0.12/12,60,-100,1000))")); + assertEquals("6431.93995693323", + eval("=CStr(FV(0.12/12,60,-100,1000,1))")); + + assertEquals("4495.5038406224", + eval("=CStr(PV(0.12/12,60,-100))")); + assertEquals("4540.45887902863", + eval("=CStr(PV(0.12/12,60,-100,0,1))")); + assertEquals("-1008.99231875519", + eval("=CStr(PV(0.12/12,60,-100,10000))")); + assertEquals("-964.037280348968", + eval("=CStr(PV(0.12/12,60,-100,10000,1))")); + + assertEquals("22.2444476849018", + eval("=CStr(Pmt(0.12/12,60,-1000))")); + assertEquals("22.0242056286156", + eval("=CStr(Pmt(0.12/12,60,-1000,0,1))")); + assertEquals("-100.200029164116", + eval("=CStr(Pmt(0.12/12,60,-1000,10000))")); + assertEquals("-99.2079496674414", + eval("=CStr(Pmt(0.12/12,60,-1000,10000,1))")); + assertEquals("-122.444476849018", + eval("=CStr(Pmt(0.12/12,60,0,10000))")); + assertEquals("-121.232155296057", + eval("=CStr(Pmt(0.12/12,60,0,10000,1))")); + + // FIXME not working for all param combos + // assertEquals("10.0", + // eval("=CStr(IPmt(0.12/12,1,60,-1000))")); + // assertEquals("5.904184782975672", + // eval("=CStr(IPmt(0.12/12,30,60,-1000))")); + // 0 + // assertEquals("", + // eval("=CStr(IPmt(0.12/12,1,60,-1000,0,1))")); + // 5.84572750... + // assertEquals("5.845727507896704", + // eval("=CStr(IPmt(0.12/12,30,60,-1000,0,1))")); + // 0 + // assertEquals("", + // eval("=CStr(IPmt(0.12/12,1,60,0,10000))")); + // 40.9581521702433 + // assertEquals("40.95815217024329", + // eval("=CStr(IPmt(0.12/12,30,60,0,10000))")); + // 0 + // assertEquals("", + // eval("=CStr(IPmt(0.12/12,1,60,0,10000,1))")); + // 40.552625911132 + // assertEquals("40.55262591113197", + // eval("=CStr(IPmt(0.12/12,30,60,0,10000,1))")); + // assertEquals("10.0", + // eval("=CStr(IPmt(0.12/12,1,60,-1000,10000))")); + // assertEquals("46.862336953218964", + // eval("=CStr(IPmt(0.12/12,30,60,-1000,10000))")); + // 0 + // assertEquals("", + // eval("=CStr(IPmt(0.12/12,1,60,-1000,10000,1))")); + // 46.3983534190287 + // assertEquals("46.39835341902867", + // eval("=CStr(IPmt(0.12/12,30,60,-1000,10000,1))")); + + // FIXME, doesn't work for partial days + // assertEquals("1.3150684931506849", + // eval("=CStr(DDB(2400,300,10*365,1))")); + // assertEquals("40.0", + // eval("=CStr(DDB(2400,300,10*12,1))")); + // assertEquals("480.0", + // eval("=CStr(DDB(2400,300,10,1))")); + // assertEquals("22.122547200000042", + // eval("=CStr(DDB(2400,300,10,10))")); + // assertEquals("245.76", + // eval("=CStr(DDB(2400,300,10,4))")); + // assertEquals("307.20000000000005", + // eval("=CStr(DDB(2400,300,10,3))")); + // assertEquals("480.0", + // eval("=CStr(DDB(2400,300,10,0.1))")); + // 274.768033075174 + // assertEquals("", + // eval("=CStr(DDB(2400,300,10,3.5))")); + + + } + +} 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..2f6e738 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java @@ -0,0 +1,468 @@ +/* +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.SimpleDateFormat; +import java.util.Date; +import javax.script.Bindings; +import javax.script.SimpleBindings; + +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.Identifier; +import com.healthmarketscience.jackcess.expr.TemporalConfig; +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.NumberFormatter; +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", "<EUnaryOp>{- <ELiteralValue>{42}}"); + + validateExpr("(+37)", "<EParen>{(<EUnaryOp>{+ <ELiteralValue>{37}})}"); + + 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)}"); + + validateExpr("' \"A\" '", "<ELiteralValue>{\" \"\"A\"\" \"}", + "\" \"\"A\"\" \""); + } + + 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(int i = -10; i <= 10; ++i) { + assertEquals(-i, eval("=-(" + i + ")")); + } + + for(int i = -10; i <= 10; ++i) { + assertEquals(i, eval("=+(" + i + ")")); + } + + for(double i : DBLS) { + assertEquals(toBD(-i), eval("=-(" + i + ")")); + } + + for(double i : DBLS) { + assertEquals(toBD(i), eval("=+(" + i + ")")); + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++j) { + assertEquals((i + j), eval("=" + i + " + " + j)); + } + } + + for(double i : DBLS) { + for(double j : DBLS) { + assertEquals(toBD(toBD(i).add(toBD(j))), eval("=" + i + " + " + j)); + } + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++j) { + assertEquals((i - j), eval("=" + i + " - " + j)); + } + } + + for(double i : DBLS) { + for(double j : DBLS) { + assertEquals(toBD(toBD(i).subtract(toBD(j))), eval("=" + i + " - " + j)); + } + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++j) { + assertEquals((i * j), eval("=" + i + " * " + j)); + } + } + + for(double i : DBLS) { + for(double j : DBLS) { + assertEquals(toBD(toBD(i).multiply(toBD(j))), eval("=" + i + " * " + j)); + } + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++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(roundToLongInt(j) == 0) { + evalFail("=" + i + " \\ " + j, ArithmeticException.class); + } else { + assertEquals((roundToLongInt(i) / roundToLongInt(j)), + eval("=" + i + " \\ " + j)); + } + } + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++j) { + if(j == 0) { + evalFail("=" + i + " Mod " + j, ArithmeticException.class); + } else { + assertEquals((i % j), eval("=" + i + " Mod " + j)); + } + } + } + + for(double i : DBLS) { + for(double j : DBLS) { + if(roundToLongInt(j) == 0) { + evalFail("=" + i + " Mod " + j, ArithmeticException.class); + } else { + assertEquals((roundToLongInt(i) % roundToLongInt(j)), + eval("=" + i + " Mod " + j)); + } + } + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++j) { + if(j == 0) { + evalFail("=" + i + " / " + j, ArithmeticException.class); + } else { + double result = (double)i / (double)j; + if((int)result == result) { + assertEquals((int)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(toBD(BuiltinOperators.divide(toBD(i), toBD(j))), + eval("=" + i + " / " + j)); + } + } + } + + for(int i = -10; i <= 10; ++i) { + for(int j = -10; j <= 10; ++j) { + double result = Math.pow(i, j); + if((int)result == result) { + assertEquals((int)result, eval("=" + i + " ^ " + j)); + } else { + assertEquals(result, eval("=" + i + " ^ " + j)); + } + } + } + } + + public void testTrickyMathExpressions() throws Exception + { + assertEquals(37, eval("=30+7")); + assertEquals(23, eval("=30+-7")); + assertEquals(23, eval("=30-+7")); + assertEquals(37, eval("=30--7")); + assertEquals(23, eval("=30-7")); + + assertEquals(100, eval("=-10^2")); + assertEquals(-100, eval("=-(10)^2")); + assertEquals(-100d, eval("=-\"10\"^2")); + assertEquals(toBD(-98.9d), eval("=1.1+(-\"10\"^2)")); + + assertEquals(toBD(99d), eval("=-10E-1+10e+1")); + assertEquals(toBD(-101d), eval("=-10E-1-10e+1")); + } + + public void testTypeCoercion() throws Exception + { + assertEquals("foobar", eval("=\"foo\" + \"bar\"")); + + assertEquals("12foo", eval("=12 + \"foo\"")); + assertEquals("foo12", eval("=\"foo\" + 12")); + + assertEquals(37d, eval("=\"25\" + 12")); + assertEquals(37d, 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(128208, 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*\"", "")); + } + + public void testLiteralDefaultValue() throws Exception + { + assertEquals("-28 blah ", eval("=CDbl(9)-37 & \" blah \"", + Value.Type.STRING)); + assertEquals("CDbl(9)-37 & \" blah \"", + eval("CDbl(9)-37 & \" blah \"", Value.Type.STRING)); + + assertEquals(-28d, eval("=CDbl(9)-37", Value.Type.DOUBLE)); + assertEquals(-28d, eval("CDbl(9)-37", Value.Type.DOUBLE)); + } + + 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, null); + String foundDebugStr = expr.toDebugString(); + if(foundDebugStr.startsWith("<EImplicitCompOp>")) { + assertEquals("<EImplicitCompOp>{<EThisValue>{<THIS_COL>} = " + + debugStr + "}", foundDebugStr); + } else { + assertEquals(debugStr, foundDebugStr); + } + assertEquals(cleanStr, expr.toString()); + } + + static Object eval(String exprStr) { + return eval(exprStr, null); + } + + static Object eval(String exprStr, Value.Type resultType) { + Expression expr = Expressionator.parse( + Expressionator.Type.DEFAULT_VALUE, exprStr, resultType, + 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, null, + 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, null, new TestParseContext()); + return (Boolean)expr.eval(new TestEvalContext(BuiltinOperators.toValue(thisVal))); + } + + static int roundToLongInt(double d) { + return new BigDecimal(d).setScale(0, NumberFormatter.ROUND_MODE) + .intValueExact(); + } + + static BigDecimal toBD(double d) { + return toBD(BigDecimal.valueOf(d)); + } + + static BigDecimal toBD(BigDecimal bd) { + return BuiltinOperators.normalize(bd); + } + + 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 DefaultFunctions.getFunction(name); + } + } + + private static final class TestEvalContext implements EvalContext + { + private final Value _thisVal; + private final RandomContext _rndCtx = new RandomContext(); + private final Bindings _bindings = new SimpleBindings(); + + 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 getIdentifierValue(Identifier identifier) { + throw new UnsupportedOperationException(); + } + + public float getRandom(Integer seed) { + return _rndCtx.getRandom(seed); + } + + public Bindings getBindings() { + return _bindings; + } + + public Object get(String key) { + return _bindings.get(key); + } + + public void put(String key, Object value) { + _bindings.put(key, value); + } + } +} |