Browse Source

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
tags/jackcess-2.2.0
James Ahlborn 5 years ago
parent
commit
77b2229aa3
60 changed files with 10362 additions and 482 deletions
  1. 39
    13
      src/main/java/com/healthmarketscience/jackcess/Database.java
  2. 32
    0
      src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java
  3. 2
    2
      src/main/java/com/healthmarketscience/jackcess/JackcessException.java
  4. 38
    0
      src/main/java/com/healthmarketscience/jackcess/expr/EvalConfig.java
  5. 45
    0
      src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java
  6. 40
    0
      src/main/java/com/healthmarketscience/jackcess/expr/EvalException.java
  7. 34
    0
      src/main/java/com/healthmarketscience/jackcess/expr/Expression.java
  8. 28
    0
      src/main/java/com/healthmarketscience/jackcess/expr/Function.java
  9. 26
    0
      src/main/java/com/healthmarketscience/jackcess/expr/FunctionLookup.java
  10. 84
    0
      src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java
  11. 39
    0
      src/main/java/com/healthmarketscience/jackcess/expr/ParseException.java
  12. 92
    0
      src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java
  13. 81
    0
      src/main/java/com/healthmarketscience/jackcess/expr/Value.java
  14. 211
    0
      src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java
  15. 65
    0
      src/main/java/com/healthmarketscience/jackcess/impl/CalcColEvalContext.java
  16. 78
    16
      src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java
  17. 41
    0
      src/main/java/com/healthmarketscience/jackcess/impl/ColDefaultValueEvalContext.java
  18. 43
    0
      src/main/java/com/healthmarketscience/jackcess/impl/ColEvalContext.java
  19. 95
    0
      src/main/java/com/healthmarketscience/jackcess/impl/ColValidatorEvalContext.java
  20. 418
    157
      src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
  21. 25
    11
      src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java
  22. 99
    0
      src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java
  23. 231
    167
      src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
  24. 81
    0
      src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java
  25. 32
    32
      src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java
  26. 178
    0
      src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java
  27. 47
    24
      src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java
  28. 64
    0
      src/main/java/com/healthmarketscience/jackcess/impl/RowEvalContext.java
  29. 66
    0
      src/main/java/com/healthmarketscience/jackcess/impl/RowValidatorEvalContext.java
  30. 46
    0
      src/main/java/com/healthmarketscience/jackcess/impl/SimpleCache.java
  31. 172
    18
      src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
  32. 114
    0
      src/main/java/com/healthmarketscience/jackcess/impl/TopoSorter.java
  33. 83
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java
  34. 80
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java
  35. 56
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java
  36. 75
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java
  37. 63
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java
  38. 794
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
  39. 37
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java
  40. 36
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java
  41. 276
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java
  42. 440
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFinancialFunctions.java
  43. 554
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java
  44. 195
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java
  45. 380
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java
  46. 68
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java
  47. 658
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java
  48. 2142
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
  49. 66
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java
  50. 140
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/RandomContext.java
  51. 87
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java
  52. 37
    0
      src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java
  53. 4
    2
      src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java
  54. 117
    4
      src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java
  55. 283
    0
      src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java
  56. 44
    36
      src/test/java/com/healthmarketscience/jackcess/TestUtil.java
  57. 106
    0
      src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java
  58. 166
    0
      src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java
  59. 221
    0
      src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java
  60. 468
    0
      src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java

+ 39
- 13
src/main/java/com/healthmarketscience/jackcess/Database.java View File

@@ -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();
@@ -443,6 +451,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();
}

+ 32
- 0
src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java View File

@@ -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);
}
}

+ 2
- 2
src/main/java/com/healthmarketscience/jackcess/JackcessException.java View File

@@ -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);

+ 38
- 0
src/main/java/com/healthmarketscience/jackcess/expr/EvalConfig.java View File

@@ -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);
}

+ 45
- 0
src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java View File

@@ -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);
}

+ 40
- 0
src/main/java/com/healthmarketscience/jackcess/expr/EvalException.java View File

@@ -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);
}
}

+ 34
- 0
src/main/java/com/healthmarketscience/jackcess/expr/Expression.java View File

@@ -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);
}

+ 28
- 0
src/main/java/com/healthmarketscience/jackcess/expr/Function.java View File

@@ -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();
}

+ 26
- 0
src/main/java/com/healthmarketscience/jackcess/expr/FunctionLookup.java View File

@@ -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);
}

+ 84
- 0
src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java View File

@@ -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();
}

}

+ 39
- 0
src/main/java/com/healthmarketscience/jackcess/expr/ParseException.java View File

@@ -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);
}
}

+ 92
- 0
src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java View File

@@ -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;
}
}

+ 81
- 0
src/main/java/com/healthmarketscience/jackcess/expr/Value.java View File

@@ -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();
}

+ 211
- 0
src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java View File

@@ -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;
}
}
}

+ 65
- 0
src/main/java/com/healthmarketscience/jackcess/impl/CalcColEvalContext.java View File

@@ -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);
}
}

+ 78
- 16
src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java View File

@@ -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,17 +128,29 @@ 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);
@@ -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,10 +179,22 @@ 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
@@ -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,10 +225,22 @@ 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
@@ -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,10 +268,22 @@ 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
@@ -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,10 +312,22 @@ 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);


+ 41
- 0
src/main/java/com/healthmarketscience/jackcess/impl/ColDefaultValueEvalContext.java View File

@@ -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());
}
}

+ 43
- 0
src/main/java/com/healthmarketscience/jackcess/impl/ColEvalContext.java View File

@@ -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);
}
}

+ 95
- 0
src/main/java/com/healthmarketscience/jackcess/impl/ColValidatorEvalContext.java View File

@@ -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();
}
}
}

+ 418
- 157
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
File diff suppressed because it is too large
View File


+ 25
- 11
src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java View File

@@ -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) {
@@ -70,6 +71,15 @@ public class CustomToStringStyle extends StandardToStringStyle
return new ToStringBuilder(obj, VALUE_INSTANCE);
}

@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) {
@@ -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);
}
}

+ 99
- 0
src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java View File

@@ -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);
}
}

+ 231
- 167
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
File diff suppressed because it is too large
View File


+ 81
- 0
src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java View File

@@ -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;
}

+ 32
- 32
src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java View File

@@ -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;

+ 178
- 0
src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java View File

@@ -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();
}
}
}

+ 47
- 24
src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java View File

@@ -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;
}
}

+ 64
- 0
src/main/java/com/healthmarketscience/jackcess/impl/RowEvalContext.java View File

@@ -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();
}

+ 66
- 0
src/main/java/com/healthmarketscience/jackcess/impl/RowValidatorEvalContext.java View File

@@ -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);
}
}

+ 46
- 0
src/main/java/com/healthmarketscience/jackcess/impl/SimpleCache.java View File

@@ -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);
}
}

+ 172
- 18
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java View File

@@ -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();
}
}
}

+ 114
- 0
src/main/java/com/healthmarketscience/jackcess/impl/TopoSorter.java View File

@@ -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;
}
}
}

+ 83
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java View File

@@ -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());
}
}

+ 80
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java View File

@@ -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();
}

+ 56
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java View File

@@ -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();
}

+ 75
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java View File

@@ -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() + "'";
}
}

+ 63
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java View File

@@ -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;
}
}

+ 794
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java View File

@@ -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;
}
}

+ 37
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java View File

@@ -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;
}
}

+ 36
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java View File

@@ -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;
}
}

+ 276
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java View File

@@ -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());
}
}

+ 440
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFinancialFunctions.java View File

@@ -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;
}

}


+ 554
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java View File

@@ -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);
}
}
}

+ 195
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java View File

@@ -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)
// }
// });


}

+ 380
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java View File

@@ -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);
}
}


}

+ 68
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java View File

@@ -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);
}
}

+ 658
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java View File

@@ -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();
}
}

}

+ 2142
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java
File diff suppressed because it is too large
View File


+ 66
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java View File

@@ -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();
}
}

+ 140
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/RandomContext.java View File

@@ -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;
}
}
}

+ 87
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java View File

@@ -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 + "'");
}
}

+ 37
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java View File

@@ -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;
}
}

+ 4
- 2
src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java View File

@@ -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) {

+ 117
- 4
src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java View File

@@ -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");


+ 283
- 0
src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java View File

@@ -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();
}
}

+ 44
- 36
src/test/java/com/healthmarketscience/jackcess/TestUtil.java View File

@@ -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
{

+ 106
- 0
src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java View File

@@ -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")));
}
}

+ 166
- 0
src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java View File

@@ -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);
}
}
}
}

+ 221
- 0
src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java View File

@@ -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))"));


}

}

+ 468
- 0
src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java View File

@@ -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);
}
}
}

Loading…
Cancel
Save