aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2018-06-26 03:59:12 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2018-06-26 03:59:12 +0000
commit77b2229aa3f6e96c30731c8f09b01f48a281f8a8 (patch)
treea4d60afccd08e6d9ee4acfa02646606815bc4daf
parentc01cc6e96c9bae9db4acc58f86ef56223c45c040 (diff)
parent175a918ed7c42a531dd8a5f08e2f9e645a02320a (diff)
downloadjackcess-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
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/Database.java52
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java32
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/JackcessException.java4
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/EvalConfig.java38
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java45
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/EvalException.java40
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Expression.java34
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Function.java28
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/FunctionLookup.java26
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java84
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/ParseException.java39
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java92
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Value.java81
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java211
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/CalcColEvalContext.java65
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java94
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColDefaultValueEvalContext.java41
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColEvalContext.java43
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColValidatorEvalContext.java95
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java575
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java36
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java99
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java398
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java81
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java64
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java178
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java71
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/RowEvalContext.java64
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/RowValidatorEvalContext.java66
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/SimpleCache.java46
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java190
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TopoSorter.java114
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java83
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java80
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java56
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java75
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java63
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java794
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java37
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java36
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java276
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFinancialFunctions.java440
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java554
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java195
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java380
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java68
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java658
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java2142
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java66
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/RandomContext.java140
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java87
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java37
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java6
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java121
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java283
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/TestUtil.java80
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java106
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java166
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java221
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java468
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);
+ }
+ }
+}