git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1172 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-2.2.0
@@ -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(); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); |
@@ -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); | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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(); | |||
} |
@@ -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); | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
@@ -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()); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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; |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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(); | |||
} |
@@ -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() + "'"; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -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) | |||
// } | |||
// }); | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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 + "'"); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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) { |
@@ -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"); | |||
@@ -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(); | |||
} | |||
} |
@@ -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 | |||
{ |
@@ -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"))); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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))")); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |