diff options
Diffstat (limited to 'src/main/java')
98 files changed, 3558 insertions, 1211 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/Cursor.java b/src/main/java/com/healthmarketscience/jackcess/Cursor.java index 7260b79..2d0c9f7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/main/java/com/healthmarketscience/jackcess/Cursor.java @@ -31,7 +31,7 @@ import com.healthmarketscience.jackcess.util.IterableBuilder; * of table modification during traversal (although depending on how the table * is traversed, row updates may or may not be seen). Multiple cursors may * traverse the same table simultaneously. - * <p/> + * <p> * Basic cursors will generally iterate table data in the order it appears in * the database and searches will require scanning the entire table. * Additional features are available when utilizing an {@link Index} backed @@ -135,11 +135,12 @@ public interface Cursor extends Iterable<Row> * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to * {@link #getNextRow}. - * <p/> + * <p> * For more flexible iteration see {@link #newIterable}. * @throws RuntimeIOException if an IOException is thrown by one of the * operations, the actual exception will be contained within */ + @Override public Iterator<Row> iterator(); /** @@ -151,7 +152,7 @@ public interface Cursor extends Iterable<Row> /** * Delete the current row. - * <p/> + * <p> * Note, re-deleting an already deleted row is allowed (it does nothing). * @throws IllegalStateException if the current row is not valid (at * beginning or end of table) @@ -178,7 +179,7 @@ public interface Cursor extends Iterable<Row> /** * Moves to the next row in the table and returns it. - * @return The next row in this table (Column name -> Column value), or + * @return The next row in this table (Column name -> Column value), or * {@code null} if no next row is found */ public Row getNextRow() throws IOException; @@ -186,7 +187,7 @@ public interface Cursor extends Iterable<Row> /** * Moves to the next row in the table and returns it. * @param columnNames Only column names in this collection will be returned - * @return The next row in this table (Column name -> Column value), or + * @return The next row in this table (Column name -> Column value), or * {@code null} if no next row is found */ public Row getNextRow(Collection<String> columnNames) @@ -194,7 +195,7 @@ public interface Cursor extends Iterable<Row> /** * Moves to the previous row in the table and returns it. - * @return The previous row in this table (Column name -> Column value), or + * @return The previous row in this table (Column name -> Column value), or * {@code null} if no previous row is found */ public Row getPreviousRow() throws IOException; @@ -202,7 +203,7 @@ public interface Cursor extends Iterable<Row> /** * Moves to the previous row in the table and returns it. * @param columnNames Only column names in this collection will be returned - * @return The previous row in this table (Column name -> Column value), or + * @return The previous row in this table (Column name -> Column value), or * {@code null} if no previous row is found */ public Row getPreviousRow(Collection<String> columnNames) @@ -325,12 +326,12 @@ public interface Cursor extends Iterable<Row> public int movePreviousRows(int numRows) throws IOException; /** - * Returns the current row in this cursor (Column name -> Column value). + * Returns the current row in this cursor (Column name -> Column value). */ public Row getCurrentRow() throws IOException; /** - * Returns the current row in this cursor (Column name -> Column value). + * Returns the current row in this cursor (Column name -> Column value). * @param columnNames Only column names in this collection will be returned */ public Row getCurrentRow(Collection<String> columnNames) diff --git a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java index 831c78f..d4f063c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -34,14 +34,14 @@ import com.healthmarketscience.jackcess.util.ColumnMatcher; * Builder style class for constructing a {@link Cursor}. By default, a * cursor is created at the beginning of the table, and any start/end rows are * inclusive. - * <p/> + * <p> * Simple example traversal: * <pre> * for(Row row : table.newCursor().toCursor()) { * // ... process each row ... * } * </pre> - * <p/> + * <p> * Simple example search: * <pre> * Row row = CursorBuilder.findRow(table, Collections.singletonMap(col, "foo")); diff --git a/src/main/java/com/healthmarketscience/jackcess/DataType.java b/src/main/java/com/healthmarketscience/jackcess/DataType.java index 380f7f1..6070dbd 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DataType.java +++ b/src/main/java/com/healthmarketscience/jackcess/DataType.java @@ -24,6 +24,7 @@ import java.sql.Types; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.impl.JetFormat; @@ -87,9 +88,10 @@ public enum DataType { */ DOUBLE((byte) 0x07, Types.DOUBLE, 8), /** - * Corresponds to a java {@link Date}. Accepts a Date, any {@link Number} - * (using {@link Number#longValue}), or {@code null}. Equivalent to SQL - * {@link Types#TIMESTAMP}, {@link Types#DATE}, {@link Types#TIME}. + * Corresponds to a java {@link Date} or {@link LocalDateTime}. Accepts a + * Date, LocalDateTime (or related types), any {@link Number} (using {@link + * Number#longValue}), or {@code null}. Equivalent to SQL {@link + * Types#TIMESTAMP}, {@link Types#DATE}, {@link Types#TIME}. */ SHORT_DATE_TIME((byte) 0x08, Types.TIMESTAMP, 8), /** diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index d853fe8..4fa8741 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -21,6 +21,8 @@ import java.io.File; import java.io.Flushable; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.ZoneId; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; @@ -29,8 +31,8 @@ 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.query.Query; import com.healthmarketscience.jackcess.util.ColumnValidatorFactory; import com.healthmarketscience.jackcess.util.ErrorHandler; import com.healthmarketscience.jackcess.util.LinkResolver; @@ -44,11 +46,11 @@ import com.healthmarketscience.jackcess.util.TableIterableBuilder; * Database has been opened, you can interact with the data via the relevant * {@link Table}. When a Database instance is no longer useful, it should * <b>always</b> be closed ({@link #close}) to avoid corruption. - * <p/> + * <p> * Database instances (and all the related objects) are <i>not</i> * thread-safe. However, separate Database instances (and their respective * objects) can be used by separate threads without a problem. - * <p/> + * <p> * Database instances do not implement any "transactional" support, and * therefore concurrent editing of the same database file by multiple Database * instances (or with outside programs such as MS Access) <i>will generally @@ -104,7 +106,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable "com.healthmarketscience.jackcess.brokenNio"; /** system property which can be used to set the default sort order for - * table columns. Value should be one {@link Table.ColumnOrder} enum + * table columns. Value should be one of {@link Table.ColumnOrder} enum * values. * @usage _intermediate_field_ */ @@ -132,6 +134,13 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public static final String ENABLE_EXPRESSION_EVALUATION_PROPERTY = "com.healthmarketscience.jackcess.enableExpressionEvaluation"; + /** system property which can be used to set the default date/Time type. + * Value should be one of {@link DateTimeType} enum values. + * @usage _general_field_ + */ + public static final String DATE_TIME_TYPE_PROPERTY = + "com.healthmarketscience.jackcess.dateTimeType"; + /** * Enum which indicates which version of Access created the database. * @usage _general_class_ @@ -179,6 +188,11 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public File getFile(); /** + * Returns the File underlying this Database + */ + public Path getPath(); + + /** * @return The names of all of the user tables * @usage _general_method_ */ @@ -201,6 +215,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * database while an Iterator is in use. * @usage _general_method_ */ + @Override public Iterator<Table> iterator(); /** @@ -240,7 +255,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable /** * Finds all the relationships in the database in <i>non-system</i> tables. - * </p> + * <p> * Warning, this may load <i>all</i> the Tables (metadata, not data) in the * database which could cause memory issues. * @usage _intermediate_method_ @@ -250,7 +265,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable /** * Finds <i>all</i> the relationships in the database, <i>including system * tables</i>. - * </p> + * <p> * Warning, this may load <i>all</i> the Tables (metadata, not data) in the * database which could cause memory issues. * @usage _intermediate_method_ @@ -319,6 +334,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * databases) to disk. * @usage _general_method_ */ + @Override public void flush() throws IOException; /** @@ -329,6 +345,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * OutputStream or jdbc Connection). * @usage _general_method_ */ + @Override public void close() throws IOException; /** @@ -377,18 +394,34 @@ public interface Database extends Iterable<Table>, Closeable, Flushable public boolean isLinkedTable(Table table) throws IOException; /** - * Gets currently configured TimeZone (always non-{@code null}). + * Gets currently configured TimeZone (always non-{@code null} and aligned + * with the ZoneId). * @usage _intermediate_method_ */ public TimeZone getTimeZone(); /** - * Sets a new TimeZone. If {@code null}, resets to the default value. + * Sets a new TimeZone. If {@code null}, resets to the default value. Note + * that setting the TimeZone will alter the ZoneId as well. * @usage _intermediate_method_ */ public void setTimeZone(TimeZone newTimeZone); /** + * Gets currently configured ZoneId (always non-{@code null} and aligned + * with the TimeZone). + * @usage _intermediate_method_ + */ + public ZoneId getZoneId(); + + /** + * Sets a new ZoneId. If {@code null}, resets to the default value. Note + * that setting the ZoneId will alter the TimeZone as well. + * @usage _intermediate_method_ + */ + public void setZoneId(ZoneId newZoneId); + + /** * Gets currently configured Charset (always non-{@code null}). * @usage _intermediate_method_ */ @@ -492,4 +525,16 @@ public interface Database extends Iterable<Table>, Closeable, Flushable * Returns the EvalConfig for configuring expression evaluation. */ public EvalConfig getEvalConfig(); + + /** + * Gets the currently configured DateTimeType. + * @usage _general_method_ + */ + public DateTimeType getDateTimeType(); + + /** + * Sets the DateTimeType. If {@code null}, resets to the default value. + * @usage _general_method_ + */ + public void setDateTimeType(DateTimeType dateTimeType); } diff --git a/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java b/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java index c5e0252..da9681e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -35,12 +36,12 @@ import com.healthmarketscience.jackcess.util.MemFileChannel; /** * Builder style class for opening/creating a {@link Database}. - * <p/> + * <p> * Simple example usage: * <pre> * Database db = DatabaseBuilder.open(new File("test.mdb")); * </pre> - * <p/> + * <p> * Advanced example usage: * <pre> * Database db = new DatabaseBuilder(new File("test.mdb")) @@ -51,10 +52,10 @@ import com.healthmarketscience.jackcess.util.MemFileChannel; * @author James Ahlborn * @usage _general_class_ */ -public class DatabaseBuilder +public class DatabaseBuilder { /** the file name of the mdb to open/create */ - private File _mdbFile; + private Path _mdbFile; /** whether or not to open existing mdb read-only */ private boolean _readOnly; /** whether or not to auto-sync writes to the filesystem */ @@ -77,12 +78,16 @@ public class DatabaseBuilder /** database user-defined (if any) */ private Map<String,PropertyMap.Property> _userProps; - + public DatabaseBuilder() { - this(null); + this((Path)null); } public DatabaseBuilder(File mdbFile) { + this(toPath(mdbFile)); + } + + public DatabaseBuilder(Path mdbFile) { _mdbFile = mdbFile; } @@ -93,6 +98,16 @@ public class DatabaseBuilder * @usage _general_method_ */ public DatabaseBuilder setFile(File mdbFile) { + return setPath(toPath(mdbFile)); + } + + /** + * File containing an existing database for {@link #open} or target file for + * new database for {@link #create} (in which case, <b>tf this file already + * exists, it will be overwritten.</b>) + * @usage _general_method_ + */ + public DatabaseBuilder setPath(Path mdbFile) { _mdbFile = mdbFile; return this; } @@ -183,7 +198,7 @@ public class DatabaseBuilder public DatabaseBuilder putDatabaseProperty(String name, Object value) { return putDatabaseProperty(name, null, value); } - + /** * Sets the database property with the given name and type to the given * value. @@ -193,7 +208,7 @@ public class DatabaseBuilder _dbProps = putProperty(_dbProps, name, type, value); return this; } - + /** * Sets the summary database property with the given name to the given * value. Attempts to determine the type of the property (see @@ -203,7 +218,7 @@ public class DatabaseBuilder public DatabaseBuilder putSummaryProperty(String name, Object value) { return putSummaryProperty(name, null, value); } - + /** * Sets the summary database property with the given name and type to * the given value. @@ -223,7 +238,7 @@ public class DatabaseBuilder public DatabaseBuilder putUserDefinedProperty(String name, Object value) { return putUserDefinedProperty(name, null, value); } - + /** * Sets the user-defined database property with the given name and type to * the given value. @@ -257,7 +272,7 @@ public class DatabaseBuilder * Creates a new Database using the configured information. */ public Database create() throws IOException { - Database db = DatabaseImpl.create(_fileFormat, _mdbFile, _channel, _autoSync, + Database db = DatabaseImpl.create(_fileFormat, _mdbFile, _channel, _autoSync, _charset, _timeZone); if(_dbProps != null) { PropertyMap props = db.getDatabaseProperties(); @@ -281,19 +296,19 @@ public class DatabaseBuilder * Open an existing Database. If the existing file is not writeable, the * file will be opened read-only. Auto-syncing is enabled for the returned * Database. - * + * * @param mdbFile File containing the database - * + * * @see DatabaseBuilder for more flexible Database opening * @usage _general_method_ */ public static Database open(File mdbFile) throws IOException { return new DatabaseBuilder(mdbFile).open(); } - + /** * Create a new Database for the given fileFormat - * + * * @param fileFormat version of new database. * @param mdbFile Location to write the new database to. <b>If this file * already exists, it will be overwritten.</b> @@ -301,8 +316,8 @@ public class DatabaseBuilder * @see DatabaseBuilder for more flexible Database creation * @usage _general_method_ */ - public static Database create(Database.FileFormat fileFormat, File mdbFile) - throws IOException + public static Database create(Database.FileFormat fileFormat, File mdbFile) + throws IOException { return new DatabaseBuilder(mdbFile).setFileFormat(fileFormat).create(); } @@ -330,4 +345,8 @@ public class DatabaseBuilder } return cal; } + + private static Path toPath(File file) { + return ((file != null) ? file.toPath() : null); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java new file mode 100644 index 0000000..8704350 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java @@ -0,0 +1,33 @@ +/* +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; + +/** + * Enum for selecting how a Database returns date/time types. Prefer using + * {@link DateTimeType#LOCAL_DATE_TIME} as using Date is being phased out and + * will eventually be removed. + * + * @author James Ahlborn + */ +public enum DateTimeType +{ + /** use legacy {@link java.util.Date} objects. To maintain backwards + compatibility, this is the default type. */ + DATE, + /** use jdk8+ {@link java.time.LocalDateTime} objects */ + LOCAL_DATE_TIME; +} diff --git a/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java index 1e4aa34..a48a2ce 100644 --- a/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java +++ b/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java @@ -41,9 +41,9 @@ public interface IndexCursor extends Cursor * @param entryValues the column values for the index's columns. * @return the matching row or {@code null} if a match could not be found. */ - public Row findRowByEntry(Object... entryValues) + public Row findRowByEntry(Object... entryValues) throws IOException; - + /** * Moves to the first row (as defined by the cursor) where the index entries * match the given values. If a match is not found (or an exception is @@ -56,32 +56,32 @@ public interface IndexCursor extends Cursor * @return {@code true} if a valid row was found with the given values, * {@code false} if no row was found */ - public boolean findFirstRowByEntry(Object... entryValues) + public boolean findFirstRowByEntry(Object... entryValues) throws IOException; /** * Moves to the first row (as defined by the cursor) where the index entries - * are >= the given values. If a an exception is thrown, the cursor is + * are >= the given values. If a an exception is thrown, the cursor is * restored to its previous state. * * @param entryValues the column values for the index's columns. */ - public void findClosestRowByEntry(Object... entryValues) + public void findClosestRowByEntry(Object... entryValues) throws IOException; /** * Returns {@code true} if the current row matches the given index entries. - * + * * @param entryValues the column values for the index's columns. */ - public boolean currentRowMatchesEntry(Object... entryValues) + public boolean currentRowMatchesEntry(Object... entryValues) throws IOException; /** * Convenience method for constructing a new EntryIterableBuilder for this * cursor. An EntryIterableBuilder provides a variety of options for more * flexible iteration based on a specific index entry. - * + * * @param entryValues the column values for the index's columns. */ public EntryIterableBuilder newEntryIterable(Object... entryValues); diff --git a/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java b/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java index adffc0f..2e161d2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java +++ b/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java @@ -29,4 +29,8 @@ public class InvalidValueException extends JackcessException public InvalidValueException(String msg) { super(msg); } + + public InvalidValueException(String msg, Throwable cause) { + super(msg, cause); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java b/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java index 516a098..7ff920b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java +++ b/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java @@ -80,7 +80,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> * determine the type of the property based on the name and value (the * property names listed above have their types builtin, otherwise the type * of the value is used). - * <p/> + * <p> * Note, this change will not be persisted until the {@link #save} method * has been called. * @@ -92,7 +92,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> /** * Creates a new (or updates an existing) property in the map. - * <p/> + * <p> * Note, this change will not be persisted until the {@link #save} method * has been called. * @@ -102,7 +102,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> /** * Creates a new (or updates an existing) property in the map. - * <p/> + * <p> * Note, this change will not be persisted until the {@link #save} method * has been called. * @@ -151,7 +151,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> /** * Sets the new value for this property. - * <p/> + * <p> * Note, this change will not be persisted until the {@link * PropertyMap#save} method has been called. */ @@ -204,6 +204,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> _value = (short)value; } + @Override public Short getValue() { return _value; } @@ -228,6 +229,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> _value = (byte)value; } + @Override public Byte getValue() { return _value; } @@ -261,6 +263,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> _value = (byte)value; } + @Override public Byte getValue() { return _value; } @@ -287,6 +290,7 @@ public interface PropertyMap extends Iterable<PropertyMap.Property> _value = (byte)value; } + @Override public Byte getValue() { return _value; } diff --git a/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java b/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java index 3ca3e85..f9ae6e9 100644 --- a/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java @@ -32,7 +32,7 @@ import com.healthmarketscience.jackcess.impl.RelationshipImpl; * if integrity enforcement is enabled, there must already be a unique index * on the "from" Table for the relevant columns (same requirement as MS * Access). - * <p/> + * <p> * Example: * <pre> * Relationship rel = new RelationshipBuilder("FromTable", "ToTable") diff --git a/src/main/java/com/healthmarketscience/jackcess/Row.java b/src/main/java/com/healthmarketscience/jackcess/Row.java index a599c20..430dfd1 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Row.java +++ b/src/main/java/com/healthmarketscience/jackcess/Row.java @@ -20,13 +20,14 @@ import java.io.IOException; import java.util.Date; import java.util.Map; import java.math.BigDecimal; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.util.OleBlob; /** - * A row of data as column name->value pairs. Values are strongly typed, and + * A row of data as column name->value pairs. Values are strongly typed, and * column names are case sensitive. * * @author James Ahlborn @@ -35,7 +36,7 @@ import com.healthmarketscience.jackcess.util.OleBlob; public interface Row extends Map<String,Object> { /** - * @return the id of this row + * @return the id of this row */ public RowId getId(); @@ -90,11 +91,25 @@ public interface Row extends Map<String,Object> /** * Convenience method which gets the value for the row with the given name, * casting it to a Date (DataType SHORT_DATE_TIME). + * @deprecated this is only valid for Database instances configured for the + * legacy {@link DateTimeType#DATE}. Prefer using + * {@link DateTimeType#LOCAL_DATE_TIME} and the corresponding + * {@link #getLocalDateTime} method. Using Date is being phased + * out and will eventually be removed. */ + @Deprecated public Date getDate(String name); /** * Convenience method which gets the value for the row with the given name, + * casting it to a LocalDateTime (DataType SHORT_DATE_TIME). This method + * will only work for Database instances configured for + * {@link DateTimeType#LOCAL_DATE_TIME}. + */ + public LocalDateTime getLocalDateTime(String name); + + /** + * Convenience method which gets the value for the row with the given name, * casting it to a byte[] (DataTypes BINARY, OLE). */ public byte[] getBytes(String name); @@ -108,7 +123,7 @@ public interface Row extends Map<String,Object> /** * Convenience method which gets the value for the row with the given name, * converting it to an {@link OleBlob} (DataTypes OLE). - * </p> + * <p> * Note, <i>the OleBlob should be closed after use</i>. */ public OleBlob getBlob(String name) throws IOException; diff --git a/src/main/java/com/healthmarketscience/jackcess/Table.java b/src/main/java/com/healthmarketscience/jackcess/Table.java index bbb6885..c1d1ddd 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Table.java +++ b/src/main/java/com/healthmarketscience/jackcess/Table.java @@ -33,7 +33,7 @@ import com.healthmarketscience.jackcess.util.ErrorHandler; * {@link TableBuilder}. The {@link com.healthmarketscience.jackcess.util.Joiner} utility can be used to traverse * table relationships (e.g. find rows in another table based on a foreign-key * relationship). - * <p/> + * <p> * A Table instance is not thread-safe (see {@link Database} for more * thread-safety details). * @@ -160,14 +160,14 @@ public interface Table extends Iterable<Row> public Index getForeignKeyIndex(Table otherTable); /** - * Converts a map of columnName -> columnValue to an array of row values + * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #addRow(Object...)}. * @usage _general_method_ */ public Object[] asRow(Map<String,?> rowMap); /** - * Converts a map of columnName -> columnValue to an array of row values + * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link Cursor#updateCurrentRow(Object...)}. * @usage _general_method_ */ @@ -203,7 +203,7 @@ public interface Table extends Iterable<Row> /** * Calls {@link #asRow} on the given row map and passes the result to {@link * #addRow}. - * <p/> + * <p> * Note, if this table has an auto-number column, the value generated will be * put back into the given row map. * @return the given row map, which will contain any autonumbers generated @@ -242,7 +242,7 @@ public interface Table extends Iterable<Row> /** * Calls {@link #asRow} on the given row maps and passes the results to * {@link #addRows}. - * <p/> + * <p> * Note, if this table has an auto-number column, the values generated will * be put back into the appropriate row maps. * <p> @@ -278,13 +278,14 @@ public interface Table extends Iterable<Row> * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to * {@link #getNextRow}. - * <p/> + * <p> * For more advanced iteration, use the {@link #getDefaultCursor default * cursor} directly. * @throws RuntimeIOException if an IOException is thrown by one of the * operations, the actual exception will be contained within * @usage _general_method_ */ + @Override public Iterator<Row> iterator(); /** @@ -296,7 +297,7 @@ public interface Table extends Iterable<Row> public void reset(); /** - * @return The next row in this table (Column name -> Column value) (uses + * @return The next row in this table (Column name -> Column value) (uses * the {@link #getDefaultCursor default cursor}) * @usage _general_method_ */ diff --git a/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java b/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java index 31aa3a0..b26da3e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java @@ -32,7 +32,7 @@ import com.healthmarketscience.jackcess.impl.TableCreator; /** * Builder style class for constructing a {@link Table}. - * <p/> + * <p> * Example: * <pre> * Table table = new TableBuilder("Test") diff --git a/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java b/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java index d35559e..0047719 100644 --- a/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java +++ b/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java @@ -17,14 +17,16 @@ limitations under the License. package com.healthmarketscience.jackcess.complex; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Date; +import com.healthmarketscience.jackcess.DateTimeType; /** * Complex value corresponding to an attachment. * * @author James Ahlborn */ -public interface Attachment extends ComplexValue +public interface Attachment extends ComplexValue { public byte[] getFileData() throws IOException; @@ -37,20 +39,34 @@ public interface Attachment extends ComplexValue public String getFileName(); public void setFileName(String fileName); - + public String getFileUrl(); public void setFileUrl(String fileUrl); - + public String getFileType(); public void setFileType(String fileType); - + + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public Date getFileTimeStamp(); + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public void setFileTimeStamp(Date fileTimeStamp); - + + public LocalDateTime getFileLocalTimeStamp(); + + public void setFileLocalTimeStamp(LocalDateTime fileTimeStamp); + + public Object getFileTimeStampObject(); + public Integer getFileFlags(); - public void setFileFlags(Integer fileFlags); + public void setFileFlags(Integer fileFlags); } diff --git a/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java index 150dd07..0e0bc13 100644 --- a/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java +++ b/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -18,10 +18,13 @@ package com.healthmarketscience.jackcess.complex; import java.io.IOException; import java.io.ObjectStreamException; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; import java.util.Map; + import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.DateTimeType; /** @@ -40,33 +43,33 @@ import com.healthmarketscience.jackcess.Column; */ public abstract class ComplexValueForeignKey extends Number { - private static final long serialVersionUID = 20130319L; + private static final long serialVersionUID = 20130319L; @Override public byte byteValue() { return (byte)get(); } - + @Override public short shortValue() { return (short)get(); } - + @Override public int intValue() { return get(); } - + @Override public long longValue() { return get(); } - + @Override public float floatValue() { return get(); } - + @Override public double doubleValue() { return get(); @@ -78,12 +81,12 @@ public abstract class ComplexValueForeignKey extends Number // of jackcess) return Integer.valueOf(get()); } - + @Override public int hashCode() { return get(); } - + @Override public boolean equals(Object o) { return ((this == o) || @@ -94,7 +97,7 @@ public abstract class ComplexValueForeignKey extends Number @Override public String toString() { return String.valueOf(get()); - } + } public abstract int get(); @@ -122,25 +125,50 @@ public abstract class ComplexValueForeignKey extends Number public abstract Version addVersion(String value) throws IOException; + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public abstract Version addVersion(String value, Date modifiedDate) throws IOException; + public abstract Version addVersion(String value, LocalDateTime modifiedDate) + throws IOException; + public abstract Attachment addAttachment(byte[] data) throws IOException; + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public abstract Attachment addAttachment( String url, String name, String type, byte[] data, Date timeStamp, Integer flags) throws IOException; + public abstract Attachment addAttachment( + String url, String name, String type, byte[] data, + LocalDateTime timeStamp, Integer flags) + throws IOException; + public abstract Attachment addEncodedAttachment(byte[] encodedData) throws IOException; + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public abstract Attachment addEncodedAttachment( String url, String name, String type, byte[] encodedData, Date timeStamp, Integer flags) throws IOException; + public abstract Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + LocalDateTime timeStamp, Integer flags) + throws IOException; + public abstract Attachment updateAttachment(Attachment attachment) throws IOException; diff --git a/src/main/java/com/healthmarketscience/jackcess/complex/Version.java b/src/main/java/com/healthmarketscience/jackcess/complex/Version.java index a1ace1b..374e047 100644 --- a/src/main/java/com/healthmarketscience/jackcess/complex/Version.java +++ b/src/main/java/com/healthmarketscience/jackcess/complex/Version.java @@ -16,7 +16,9 @@ limitations under the License. package com.healthmarketscience.jackcess.complex; +import java.time.LocalDateTime; import java.util.Date; +import com.healthmarketscience.jackcess.DateTimeType; /** * Complex value corresponding to a version of a memo column. @@ -27,5 +29,13 @@ public interface Version extends ComplexValue, Comparable<Version> { public String getValue(); + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public Date getModifiedDate(); + + public LocalDateTime getModifiedLocalDate(); + + public Object getModifiedDateObject(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java b/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java index 45db1ad..709a7cd 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java @@ -16,7 +16,8 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; -import org.apache.commons.lang.ObjectUtils; +import java.util.Objects; + /** * identifies a database entity (e.g. the name of a database field). An @@ -71,9 +72,9 @@ public class Identifier Identifier oi = (Identifier)o; - return (ObjectUtils.equals(_objectName, oi._objectName) && - ObjectUtils.equals(_collectionName, oi._collectionName) && - ObjectUtils.equals(_propertyName, oi._propertyName)); + return (Objects.equals(_objectName, oi._objectName) && + Objects.equals(_collectionName, oi._collectionName) && + Objects.equals(_propertyName, oi._propertyName)); } @Override diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java b/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java index a90a80b..7b7a306 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java @@ -17,8 +17,8 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; /** * LocaleContext encapsulates all shared localization state for expression @@ -35,16 +35,15 @@ public interface LocaleContext public TemporalConfig getTemporalConfig(); /** - * @return an appropriately configured (i.e. TimeZone and other date/time - * flags) SimpleDateFormat for the given format. + * @return an appropriately configured (i.e. locale) DateTimeFormatter for + * the given format. */ - public SimpleDateFormat createDateFormat(String formatStr); + public DateTimeFormatter createDateFormatter(String formatStr); /** - * @return an appropriately configured (i.e. TimeZone and other date/time - * flags) Calendar. + * @return the currently configured ZoneId */ - public Calendar getCalendar(); + public ZoneId getZoneId(); /** * @return the currently configured NumericConfig (from the diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java index 74dd06e..d231522 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java @@ -35,7 +35,7 @@ public class NumericConfig 2, true, false, true, 3, Locale.US); public enum Type { - CURRENCY, FIXED, STANDARD, PERCENT, SCIENTIFIC; + CURRENCY, FIXED, STANDARD, PERCENT, SCIENTIFIC, EURO; } private final int _numDecDigits; @@ -49,6 +49,7 @@ public class NumericConfig private final String _standardFormat; private final String _percentFormat; private final String _scientificFormat; + private final String _euroFormat; public NumericConfig(int numDecDigits, boolean incLeadingDigit, boolean useNegParens, boolean useNegCurrencyParens, @@ -75,6 +76,9 @@ public class NumericConfig _scientificFormat = FormatUtil.createNumberFormatPattern( FormatUtil.NumPatternType.SCIENTIFIC, _numDecDigits, true, false, 0); + _euroFormat = FormatUtil.createNumberFormatPattern( + FormatUtil.NumPatternType.EURO, _numDecDigits, _incLeadingDigit, + _useNegCurrencyParens, _numGroupDigits); } public int getNumDecimalDigits() { @@ -109,6 +113,8 @@ public class NumericConfig return _percentFormat; case SCIENTIFIC: return _scientificFormat; + case EURO: + return _euroFormat; default: throw new IllegalArgumentException("unknown number type " + type); } diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java index db7806f..cfe08e1 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java @@ -30,19 +30,18 @@ import java.util.Locale; */ public class TemporalConfig { - public static final String US_DATE_FORMAT = "M/d/yyyy"; - public static final String US_DATE_IMPLICIT_YEAR_FORMAT = "M/d"; + public static final String US_DATE_FORMAT = "M/d[/uuuu]"; public static final String US_TIME_FORMAT_12_FORMAT = "h:mm:ss a"; public static final String US_TIME_FORMAT_24_FORMAT = "H:mm:ss"; - public static final String US_LONG_DATE_FORMAT = "EEEE, MMMM dd, yyyy"; + public static final String US_LONG_DATE_FORMAT = "EEEE, MMMM dd, uuuu"; - public static final String MEDIUM_DATE_FORMAT = "dd-MMM-yy"; + public static final String MEDIUM_DATE_FORMAT = "dd-MMM-uu"; public static final String MEDIUM_TIME_FORMAT = "hh:mm a"; public static final String SHORT_TIME_FORMAT = "HH:mm"; /** default implementation which is configured for the US locale */ public static final TemporalConfig US_TEMPORAL_CONFIG = new TemporalConfig( - US_DATE_FORMAT, US_DATE_IMPLICIT_YEAR_FORMAT, US_LONG_DATE_FORMAT, + US_DATE_FORMAT, US_LONG_DATE_FORMAT, US_TIME_FORMAT_12_FORMAT, US_TIME_FORMAT_24_FORMAT, '/', ':', Locale.US); public enum Type { @@ -133,8 +132,8 @@ public class TemporalConfig } } + private final Locale _locale; private final String _dateFormat; - private final String _dateImplicitYearFormat; private final String _longDateFormat; private final String _timeFormat12; private final String _timeFormat24; @@ -142,16 +141,15 @@ public class TemporalConfig private final char _timeSeparator; private final String _dateTimeFormat12; private final String _dateTimeFormat24; - private final DateFormatSymbols _symbols; + private final String[] _amPmStrings; /** * Instantiates a new TemporalConfig with the given configuration. Note * that the date/time format variants will be created by concatenating the - * relevant date and time formats, separated by a single space, e.g. "<date> - * <time>". + * relevant date and time formats, separated by a single space, + * e.g. "<date> <time>". * * @param dateFormat the date (no time) format - * @param dateImplicitYearFormat the date (no time) with no year format * @param timeFormat12 the 12 hour time format * @param timeFormat24 the 24 hour time format * @param dateSeparator the primary separator used to separate elements in @@ -163,21 +161,26 @@ public class TemporalConfig * string. This value should differ from the * dateSeparator. */ - public TemporalConfig(String dateFormat, String dateImplicitYearFormat, - String longDateFormat, + public TemporalConfig(String dateFormat, String longDateFormat, String timeFormat12, String timeFormat24, char dateSeparator, char timeSeparator, Locale locale) { + _locale = locale; _dateFormat = dateFormat; - _dateImplicitYearFormat = dateImplicitYearFormat; _longDateFormat = longDateFormat; _timeFormat12 = timeFormat12; _timeFormat24 = timeFormat24; _dateSeparator = dateSeparator; _timeSeparator = timeSeparator; - _dateTimeFormat12 = _dateFormat + " " + _timeFormat12; - _dateTimeFormat24 = _dateFormat + " " + _timeFormat24; - _symbols = DateFormatSymbols.getInstance(locale); + _dateTimeFormat12 = toDateTimeFormat(_dateFormat, _timeFormat12); + _dateTimeFormat24 = toDateTimeFormat(_dateFormat, _timeFormat24); + // there doesn't seem to be a good/easy way to get this in new jave.time + // api, so just use old api + _amPmStrings = DateFormatSymbols.getInstance(locale).getAmPmStrings(); + } + + public Locale getLocale() { + return _locale; } public String getDateFormat() { @@ -252,24 +255,8 @@ public class TemporalConfig } } - public String getImplicitYearDateTimeFormat(Type type) { - switch(type) { - case DATE: - return _dateImplicitYearFormat; - case DATE_TIME: - return toDateTimeFormat(_dateImplicitYearFormat, getDefaultTimeFormat()); - case DATE_TIME_12: - return toDateTimeFormat(_dateImplicitYearFormat, getTimeFormat12()); - case DATE_TIME_24: - return toDateTimeFormat(_dateImplicitYearFormat, getTimeFormat24()); - default: - throw new IllegalArgumentException( - "the given format does not include a date " + type); - } - } - - public DateFormatSymbols getDateFormatSymbols() { - return _symbols; + public String[] getAmPmStrings() { + return _amPmStrings; } private static String toDateTimeFormat(String dateFormat, String timeFormat) { diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java index 118215e..ded758b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java @@ -17,7 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; /** * Wrapper for a typed primitive value used within the expression evaluation @@ -97,9 +97,9 @@ public interface Value public String getAsString(LocaleContext ctx); /** - * @return this primitive value converted to a Date + * @return this primitive value converted to a LocalDateTime */ - public Date getAsDateTime(LocaleContext ctx); + public LocalDateTime getAsLocalDateTime(LocaleContext ctx); /** * Since date/time values have different types, it may be more convenient to diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/package-info.java b/src/main/java/com/healthmarketscience/jackcess/expr/package-info.java index 6e4d5ab..d5d380a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/package-info.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/package-info.java @@ -20,13 +20,13 @@ limitations under the License. * but can be globally enabled via the system property * "com.healthmarketscience.jackcess.enableExpressionEvaluation" or * selectively enabled on a per database basis using {@link com.healthmarketscience.jackcess.Database#setEvaluateExpressions(Boolean)}. - * <p/> + * <p> * The expression evaluation engine implementation does its best to follow all * the warts and idiosyncracies of Access expression evaluation (both those * that are documented as well as those discovered through experimentation). * These include such things as value conversions, "Null" handling, rounding * rules, and implicit interpretations of expression in certain contexts. - * <p/> + * <p> * Expressions can be used in a number of different places within an Access * database. When enabled, Jackcess supports the following usage: * <ul> @@ -50,14 +50,14 @@ limitations under the License. * record validation rules will be run for the entire record before * update. Failures are handled in a similar manner.</li> * </ul> - * <p/> + * * <h2>Supporting Classes</h2> - * <p/> + * <p> * The classes in this package make up the public api for expression handling * in Jackcess. They generally fall into two categories: - * <p/> + * * <h3>General Use Classes</h3> - * <p/> + * * <ul> * <li>{@link com.healthmarketscience.jackcess.expr.EvalConfig} allows for customization of the expression * evaluation context for a given {@link com.healthmarketscience.jackcess.Database} instance.</li> @@ -72,9 +72,9 @@ limitations under the License. * <li>{@link com.healthmarketscience.jackcess.expr.ParseException} wrapper exception thrown for failures which * occur during expression parsing.</li> * </ul> - * <p/> + * * <h3>Advanced Use Classes</h3> - * <p/> + * * <ul> * <li>{@link com.healthmarketscience.jackcess.expr.EvalContext} encapsulates all shared state for expression * parsing and evaluation.</li> @@ -86,9 +86,9 @@ limitations under the License. * database field).</li> * <li>{@link com.healthmarketscience.jackcess.expr.Value} represents a typed primitive value.</li> * </ul> - * <p/> + * * <h2>Function Support</h2> - * <p/> + * <p> * Jackcess supports many of the standard Access functions. The following * tables list the (hopefully) current status of support built into Jackcess. * diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java index 0e52fa4..36b32d8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java @@ -19,8 +19,9 @@ package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.math.BigDecimal; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Date; import java.util.EnumMap; @@ -28,6 +29,7 @@ import java.util.Map; import javax.script.Bindings; import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.EvalException; @@ -78,50 +80,62 @@ public abstract class BaseEvalContext implements EvalContext return _dbCtx.getDatabase(); } + @Override public TemporalConfig getTemporalConfig() { return _dbCtx.getTemporalConfig(); } - public SimpleDateFormat createDateFormat(String formatStr) { - return _dbCtx.createDateFormat(formatStr); + @Override + public DateTimeFormatter createDateFormatter(String formatStr) { + return _dbCtx.createDateFormatter(formatStr); } - public Calendar getCalendar() { - return _dbCtx.getCalendar(); + @Override + public ZoneId getZoneId() { + return _dbCtx.getZoneId(); } + @Override public NumericConfig getNumericConfig() { return _dbCtx.getNumericConfig(); } + @Override public DecimalFormat createDecimalFormat(String formatStr) { return _dbCtx.createDecimalFormat(formatStr); } + @Override public float getRandom(Integer seed) { return _dbCtx.getRandom(seed); } + @Override public Value.Type getResultType() { return null; } + @Override public Value getThisColumnValue() { throw new UnsupportedOperationException(); } + @Override public Value getIdentifierValue(Identifier identifier) { throw new UnsupportedOperationException(); } + @Override public Bindings getBindings() { return _dbCtx.getBindings(); } + @Override public Object get(String key) { return _dbCtx.getBindings().get(key); } + @Override public void put(String key, Object value) { _dbCtx.getBindings().put(key, value); } @@ -146,7 +160,10 @@ public abstract class BaseEvalContext implements EvalContext protected Value toValue(Object val, DataType dType) { try { - val = ColumnImpl.toInternalValue(dType, val, getDatabase()); + // expression engine always uses LocalDateTime, so force that date/time + // type + val = ColumnImpl.toInternalValue(dType, val, getDatabase(), + ColumnImpl.LDT_DATE_TIME_FACTORY); if(val == null) { return ValueSupport.NULL_VAL; } @@ -158,7 +175,7 @@ public abstract class BaseEvalContext implements EvalContext case DATE: case TIME: case DATE_TIME: - return ValueSupport.toValue(vType, (Date)val); + return ValueSupport.toValue(vType, (LocalDateTime)val); case LONG: Integer i = ((val instanceof Integer) ? (Integer)val : ((Number)val).intValue()); @@ -203,26 +220,32 @@ public abstract class BaseEvalContext implements EvalContext return expr; } + @Override public Object eval(EvalContext ctx) { return getExpr().eval(ctx); } + @Override public String toDebugString(LocaleContext ctx) { return getExpr().toDebugString(ctx); } + @Override public String toRawString() { return _exprStr; } + @Override public String toCleanString(LocaleContext ctx) { return getExpr().toCleanString(ctx); } + @Override public boolean isConstant() { return getExpr().isConstant(); } + @Override public void collectIdentifiers(Collection<Identifier> identifiers) { getExpr().collectIdentifiers(identifiers); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java index 9aa7d17..8e3802b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CalculatedColumnUtil.java @@ -26,7 +26,7 @@ import com.healthmarketscience.jackcess.InvalidValueException; /** * Utility code for dealing with calculated columns. - * <p/> + * <p> * These are the currently possible calculated types: FLOAT, DOUBLE, INT, * LONG, BIG_INT, GUID, SHORT_DATE_TIME, MONEY, BOOLEAN, NUMERIC, TEXT, MEMO. * diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 3445ba3..212b912 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -32,11 +32,22 @@ import java.nio.charset.Charset; import java.sql.Blob; import java.sql.Clob; import java.sql.SQLException; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.TimeZone; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,6 +55,7 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.InvalidValueException; import com.healthmarketscience.jackcess.PropertyMap; import com.healthmarketscience.jackcess.Table; @@ -52,9 +64,10 @@ import com.healthmarketscience.jackcess.complex.ComplexValue; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl; +import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; import com.healthmarketscience.jackcess.util.ColumnValidator; import com.healthmarketscience.jackcess.util.SimpleColumnValidator; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -63,7 +76,8 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class ColumnImpl implements Column, Comparable<ColumnImpl> { +public class ColumnImpl implements Column, Comparable<ColumnImpl>, DateTimeContext +{ protected static final Log LOG = LogFactory.getLog(ColumnImpl.class); @@ -79,8 +93,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { /** * Access stores numeric dates in days. Java stores them in milliseconds. */ - private static final long MILLISECONDS_PER_DAY = - (24L * 60L * 60L * 1000L); + private static final long MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L); + private static final long SECONDS_PER_DAY = (24L * 60L * 60L); + private static final long NANOS_PER_SECOND = 1_000_000_000L; + private static final long NANOS_PER_MILLI = 1_000_000L; + private static final long MILLIS_PER_SECOND = 1000L; /** * Access starts counting dates at Dec 30, 1899 (note, this strange date @@ -91,6 +108,16 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { static final long MILLIS_BETWEEN_EPOCH_AND_1900 = 25569L * MILLISECONDS_PER_DAY; + public static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30); + public static final LocalTime BASE_LT = LocalTime.of(0, 0); + public static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT); + + private static final DateTimeFactory DEF_DATE_TIME_FACTORY = + new DefaultDateTimeFactory(); + + static final DateTimeFactory LDT_DATE_TIME_FACTORY = + new LDTDateTimeFactory(); + /** * mask for the fixed len bit * @usage _advanced_field_ @@ -336,10 +363,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // base does nothing } + @Override public TableImpl getTable() { return _table; } + @Override public DatabaseImpl getDatabase() { return getTable().getDatabase(); } @@ -358,14 +387,17 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return getDatabase().getPageChannel(); } + @Override public String getName() { return _name; } + @Override public boolean isVariableLength() { return _variableLength; } + @Override public boolean isAutoNumber() { return _autoNumber; } @@ -377,6 +409,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _columnNumber; } + @Override public int getColumnIndex() { return _columnIndex; } @@ -395,22 +428,27 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _displayIndex; } + @Override public DataType getType() { return _type; } + @Override public int getSQLType() throws SQLException { return _type.getSQLType(); } + @Override public boolean isCompressedUnicode() { return false; } + @Override public byte getPrecision() { return (byte)getType().getDefaultPrecision(); } + @Override public byte getScale() { return (byte)getType().getDefaultScale(); } @@ -429,10 +467,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return 0; } + @Override public short getLength() { return _columnLength; } + @Override public final short getLengthInUnits() { if(_lengthInUnits == INVALID_LENGTH) { _lengthInUnits = calcLengthInUnits(); @@ -444,6 +484,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return getType().toUnitSize(getLength(), getFormat()); } + @Override public boolean isCalculated() { return _calculated; } @@ -466,14 +507,27 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return getDatabase().getCharset(); } - protected Calendar getCalendar() { - return getDatabase().getCalendar(); + @Override + public TimeZone getTimeZone() { + return getDatabase().getTimeZone(); + } + + @Override + public ZoneId getZoneId() { + return getDatabase().getZoneId(); + } + + @Override + public DateTimeFactory getDateTimeFactory() { + return getDatabase().getDateTimeFactory(); } + @Override public boolean isAppendOnly() { return (getVersionHistoryColumn() != null); } + @Override public ColumnImpl getVersionHistoryColumn() { return null; } @@ -493,10 +547,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { throw new UnsupportedOperationException(); } + @Override public boolean isHyperlink() { return false; } + @Override public ComplexColumnInfo<? extends ComplexValue> getComplexInfo() { return null; } @@ -593,12 +649,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { reloadPropertiesValidators(); } + @Override public ColumnValidator getColumnValidator() { // unwrap any "internal" validator return ((_validator instanceof InternalColumnValidator) ? ((InternalColumnValidator)_validator).getExternal() : _validator); } + @Override public void setColumnValidator(ColumnValidator newValidator) { if(isAutoNumber()) { @@ -659,6 +717,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _autoNumberGenerator; } + @Override public PropertyMap getProperties() throws IOException { if(_props == null) { _props = getTable().getPropertyMaps().get(getName()); @@ -666,20 +725,24 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _props; } + @Override public Object setRowValue(Object[] rowArray, Object value) { rowArray[_columnIndex] = value; return value; } + @Override public Object setRowValue(Map<String,Object> rowMap, Object value) { rowMap.put(_name, value); return value; } + @Override public Object getRowValue(Object[] rowArray) { return rowArray[_columnIndex]; } + @Override public Object getRowValue(Map<String,?> rowMap) { return rowMap.get(_name); } @@ -898,45 +961,25 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { /** * Decodes a date value. */ - private Date readDateValue(ByteBuffer buffer) - { - // seems access stores dates in the local timezone. guess you just hope - // you read it in the same timezone in which it was written! + private Object readDateValue(ByteBuffer buffer) { long dateBits = buffer.getLong(); - long time = fromDateDouble(Double.longBitsToDouble(dateBits)); - return new DateExt(time, dateBits); + return getDateTimeFactory().fromDateBits(this, dateBits); } /** * Returns a java long time value converted from an access date double. * @usage _advanced_method_ */ - public long fromDateDouble(double value) - { - return fromDateDouble(value, getCalendar()); + public long fromDateDouble(double value) { + return fromDateDouble(value, getTimeZone()); } - /** - * Returns a java long time value converted from an access date double. - * @usage _advanced_method_ - */ - public static long fromDateDouble(double value, DatabaseImpl db) - { - return fromDateDouble(value, db.getCalendar()); - } - - /** - * Returns a java long time value converted from an access date double. - * @usage _advanced_method_ - */ - public static long fromDateDouble(double value, Calendar c) - { + private static long fromDateDouble(double value, TimeZone tz) { long localTime = fromLocalDateDouble(value); - return localTime - getFromLocalTimeZoneOffset(localTime, c); + return localTime - getFromLocalTimeZoneOffset(localTime, tz); } - static long fromLocalDateDouble(double value) - { + static long fromLocalDateDouble(double value) { long datePart = ((long)value) * MILLISECONDS_PER_DAY; // the fractional part of the double represents the time. it is always @@ -944,29 +987,49 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { // _not_ the time distance from zero (as one would expect with "normal" // numbers). therefore, we need to do a little number logic to convert // the absolute time fraction into a normal distance from zero number. - long timePart = Math.round((Math.abs(value) % 1.0) * - (double)MILLISECONDS_PER_DAY); + long timePart = Math.round((Math.abs(value) % 1.0d) * + MILLISECONDS_PER_DAY); long time = datePart + timePart; - time -= MILLIS_BETWEEN_EPOCH_AND_1900; - return time; + return time - MILLIS_BETWEEN_EPOCH_AND_1900; + } + + public static LocalDateTime ldtFromLocalDateDouble(double value) { + Duration dateTimeOffset = durationFromLocalDateDouble(value); + return BASE_LDT.plus(dateTimeOffset); + } + + private static Duration durationFromLocalDateDouble(double value) { + long dateSeconds = ((long)value) * SECONDS_PER_DAY; + + // the fractional part of the double represents the time. it is always + // a positive fraction of the day (even if the double is negative), + // _not_ the time distance from zero (as one would expect with "normal" + // numbers). therefore, we need to do a little number logic to convert + // the absolute time fraction into a normal distance from zero number. + + double secondsDouble = (Math.abs(value) % 1.0d) * SECONDS_PER_DAY; + long timeSeconds = (long)secondsDouble; + long timeMillis = (long)(roundToMillis(secondsDouble % 1.0d) * + MILLIS_PER_SECOND); + + return Duration.ofSeconds(dateSeconds + timeSeconds, + timeMillis * NANOS_PER_MILLI); } /** * Writes a date value. */ private void writeDateValue(ByteBuffer buffer, Object value) + throws InvalidValueException { if(value == null) { buffer.putDouble(0d); } else if(value instanceof DateExt) { - // this is a Date value previously read from readDateValue(). use the // original bits to store the value so we don't lose any precision buffer.putLong(((DateExt)value).getDateBits()); - } else { - buffer.putDouble(toDateDouble(value)); } } @@ -977,36 +1040,87 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * @usage _advanced_method_ */ public double toDateDouble(Object value) + throws InvalidValueException { - return toDateDouble(value, getCalendar()); + try { + return toDateDouble(value, this); + } catch(IllegalArgumentException iae) { + throw new InvalidValueException(withErrorContext(iae.getMessage()), iae); + } } /** - * Returns an access date double converted from a java Date/Calendar/Number - * time value. + * Returns an access date double converted from a java + * Date/Calendar/Number/Temporal time value. * @usage _advanced_method_ */ - public static double toDateDouble(Object value, DatabaseImpl db) - { - return toDateDouble(value, db.getCalendar()); + private static double toDateDouble(Object value, DateTimeContext dtc) { + return dtc.getDateTimeFactory().toDateDouble(value, dtc); } - /** - * Returns an access date double converted from a java Date/Calendar/Number - * time value. - * @usage _advanced_method_ - */ - public static double toDateDouble(Object value, Calendar c) - { - // seems access stores dates in the local timezone. guess you just - // hope you read it in the same timezone in which it was written! - long time = toDateLong(value); - time += getToLocalTimeZoneOffset(time, c); - return toLocalDateDouble(time); + private static LocalDateTime toLocalDateTime( + TemporalAccessor value, DateTimeContext dtc) { + + // handle some common Temporal types + if(value instanceof LocalDateTime) { + return (LocalDateTime)value; + } + if(value instanceof ZonedDateTime) { + // if the temporal value has a timezone, convert it to this db's timezone + return ((ZonedDateTime)value).withZoneSameInstant( + dtc.getZoneId()).toLocalDateTime(); + } + if(value instanceof Instant) { + return LocalDateTime.ofInstant((Instant)value, dtc.getZoneId()); + } + if(value instanceof LocalDate) { + return ((LocalDate)value).atTime(BASE_LT); + } + if(value instanceof LocalTime) { + return ((LocalTime)value).atDate(BASE_LD); + } + + // generic handling for many other Temporal types + try { + + LocalDate ld = value.query(TemporalQueries.localDate()); + if(ld == null) { + ld = BASE_LD; + } + LocalTime lt = value.query(TemporalQueries.localTime()); + if(lt == null) { + lt = BASE_LT; + } + ZoneId zone = value.query(TemporalQueries.zone()); + if(zone != null) { + // the Temporal has a zone, see if it is the right zone. if not, + // adjust it + ZoneId zoneId = dtc.getZoneId(); + if(!zoneId.equals(zone)) { + return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId) + .toLocalDateTime(); + } + } + + return LocalDateTime.of(ld, lt); + + } catch(DateTimeException | ArithmeticException e) { + throw new IllegalArgumentException( + "Unsupported temporal type " + value.getClass(), e); + } } - static double toLocalDateDouble(long time) - { + private static Instant toInstant(TemporalAccessor value, DateTimeContext dtc) { + if(value instanceof ZonedDateTime) { + return ((ZonedDateTime)value).toInstant(); + } + if(value instanceof Instant) { + return (Instant)value; + } + return toLocalDateTime(value, dtc).atZone(dtc.getZoneId()).toInstant(); + } + + static double toLocalDateDouble(long time) { time += MILLIS_BETWEEN_EPOCH_AND_1900; if(time < 0L) { @@ -1020,11 +1134,63 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return time / (double)MILLISECONDS_PER_DAY; } + public static double toDateDouble(LocalDateTime ldt) { + Duration dateTimeOffset = Duration.between(BASE_LDT, ldt); + return toLocalDateDouble(dateTimeOffset); + } + + private static double toLocalDateDouble(Duration time) { + long dateTimeSeconds = time.getSeconds(); + long timeSeconds = dateTimeSeconds % SECONDS_PER_DAY; + if(timeSeconds < 0) { + timeSeconds += SECONDS_PER_DAY; + } + long dateSeconds = dateTimeSeconds - timeSeconds; + long timeNanos = time.getNano(); + + // we have a difficult choice to make here between keeping a value which + // most accurately represents the bits saved and rounding to a value that + // would match what the user would expect too see. since we do a double + // to long conversion, we end up in a situation where the value might be + // 19.9999 seconds. access will display this as 20 seconds (access seems + // to only record times to second precision). if we return 19.9999, then + // when the value is written back out it will be exactly the same double + // (good), but will display as 19 seconds (bad because it looks wrong to + // the user). on the flip side, if we round, the value will display + // "correctly" to the user, but if the value is written back out, it will + // be a slightly different double value. this may not be a problem for + // most situations, but may result in incorrect index based lookups. in + // the old date time handling we use DateExt to store the original bits. + // in jdk8, we cannot extend LocalDateTime. for now, we will try + // returning the value rounded to milliseconds (technically still more + // precision than access uses but more likely to round trip to the same + // value). + double timeDouble = ((roundToMillis((double)timeNanos / NANOS_PER_SECOND) + + timeSeconds) / SECONDS_PER_DAY); + + double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY); + + if(dateSeconds < 0) { + timeDouble = -timeDouble; + } + + return dateDouble + timeDouble; + } + + /** + * Rounds the given decimal to milliseconds (3 decimal places) using the + * standard access rounding mode. + */ + private static double roundToMillis(double dbl) { + return ((dbl == 0d) ? dbl : + new BigDecimal(dbl).setScale(3, NumberFormatter.ROUND_MODE) + .doubleValue()); + } + /** * @return an appropriate Date long value for the given object */ - private static long toDateLong(Object value) - { + private static long toDateLong(Object value) { return ((value instanceof Date) ? ((Date)value).getTime() : ((value instanceof Calendar) ? @@ -1036,24 +1202,19 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * Gets the timezone offset from UTC to local time for the given time * (including DST). */ - private static long getToLocalTimeZoneOffset(long time, Calendar c) - { - c.setTimeInMillis(time); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + private static long getToLocalTimeZoneOffset(long time, TimeZone tz) { + return tz.getOffset(time); } /** * Gets the timezone offset from local time to UTC for the given time * (including DST). */ - private static long getFromLocalTimeZoneOffset(long time, Calendar c) - { + private static long getFromLocalTimeZoneOffset(long time, TimeZone tz) { // getting from local time back to UTC is a little wonky (and not - // guaranteed to get you back to where you started) - c.setTimeInMillis(time); - // apply the zone offset first to get us closer to the original time - c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + // guaranteed to get you back to where you started). apply the zone + // offset first to get us closer to the original time + return tz.getOffset(time - tz.getRawOffset()); } /** @@ -1563,6 +1724,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { * Orders Columns by column number. * @usage _general_method_ */ + @Override public int compareTo(ColumnImpl other) { if (_columnNumber > other.getColumnNumber()) { return 1; @@ -1618,6 +1780,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return ((Boolean)value) ? BigDecimal.valueOf(-1) : BigDecimal.ZERO; } else if(value instanceof Date) { return new BigDecimal(toDateDouble(value, db)); + } else if(value instanceof LocalDateTime) { + return new BigDecimal(toDateDouble((LocalDateTime)value)); } return new BigDecimal(value.toString()); } @@ -1648,6 +1812,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return ((Boolean)value) ? -1 : 0; } else if(value instanceof Date) { return toDateDouble(value, db); + } else if(value instanceof LocalDateTime) { + return toDateDouble((LocalDateTime)value); } return Double.valueOf(value.toString()); } @@ -1983,6 +2149,14 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { DatabaseImpl db) throws IOException { + return toInternalValue(dataType, value, db, null); + } + + static Object toInternalValue(DataType dataType, Object value, + DatabaseImpl db, + ColumnImpl.DateTimeFactory factory) + throws IOException + { if(value == null) { return null; } @@ -2007,8 +2181,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return ((value instanceof Double) ? value : toNumber(value, db).doubleValue()); case SHORT_DATE_TIME: - return ((value instanceof Date) ? value : - new Date(toDateLong(value))); + if(factory == null) { + factory = db.getDateTimeFactory(); + } + return factory.toInternalValue(db, value); case TEXT: case MEMO: case GUID: @@ -2028,6 +2204,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } } + protected static DateTimeFactory getDateTimeFactory(DateTimeType type) { + return ((type == DateTimeType.LOCAL_DATE_TIME) ? + LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY); + } + String withErrorContext(String msg) { return withErrorContext(msg, getDatabase(), getTable().getName(), getName()); } @@ -2045,8 +2226,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { /** * Date subclass which stashes the original date bits, in case we attempt to - * re-write the value (will not lose precision). + * re-write the value (will not lose precision). Also, this implementation + * is immutable. */ + @SuppressWarnings("deprecation") private static final class DateExt extends Date { private static final long serialVersionUID = 0L; @@ -2063,6 +2246,41 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return _dateBits; } + @Override + public void setDate(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHours(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMinutes(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMonth(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setSeconds(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setYear(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTime(long time) { + throw new UnsupportedOperationException(); + } + private Object writeReplace() throws ObjectStreamException { // if we are going to serialize this Date, convert it back to a normal // Date (in case it is restored outside of the context of jackcess) @@ -2467,4 +2685,96 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { sb.append("allowZeroLength=false"); } } + + /** + * Factory which handles date/time values appropriately for a DateTimeType. + */ + protected static abstract class DateTimeFactory + { + public abstract DateTimeType getType(); + + public abstract Object fromDateBits(ColumnImpl col, long dateBits); + + public abstract double toDateDouble(Object value, DateTimeContext dtc); + + public abstract Object toInternalValue(DatabaseImpl db, Object value); + } + + /** + * Factory impl for legacy Date handling. + */ + private static final class DefaultDateTimeFactory extends DateTimeFactory + { + @Override + public DateTimeType getType() { + return DateTimeType.DATE; + } + + @Override + public Object fromDateBits(ColumnImpl col, long dateBits) { + long time = col.fromDateDouble( + Double.longBitsToDouble(dateBits)); + return new DateExt(time, dateBits); + } + + @Override + public double toDateDouble(Object value, DateTimeContext dtc) { + // ZoneId and TimeZone have different rules for older timezones, so we + // need to consistently use one or the other depending on the date/time + // type + long time = 0L; + if(value instanceof TemporalAccessor) { + time = toInstant((TemporalAccessor)value, dtc).toEpochMilli(); + } else { + time = toDateLong(value); + } + // seems access stores dates in the local timezone. guess you just + // hope you read it in the same timezone in which it was written! + time += getToLocalTimeZoneOffset(time, dtc.getTimeZone()); + return toLocalDateDouble(time); + } + + @Override + public Object toInternalValue(DatabaseImpl db, Object value) { + return ((value instanceof Date) ? value : + new Date(toDateLong(value))); + } + } + + /** + * Factory impl for LocalDateTime handling. + */ + private static final class LDTDateTimeFactory extends DateTimeFactory + { + @Override + public DateTimeType getType() { + return DateTimeType.LOCAL_DATE_TIME; + } + + @Override + public Object fromDateBits(ColumnImpl col, long dateBits) { + return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits)); + } + + @Override + public double toDateDouble(Object value, DateTimeContext dtc) { + // ZoneId and TimeZone have different rules for older timezones, so we + // need to consistently use one or the other depending on the date/time + // type + if(!(value instanceof TemporalAccessor)) { + value = Instant.ofEpochMilli(toDateLong(value)); + } + return ColumnImpl.toDateDouble( + toLocalDateTime((TemporalAccessor)value, dtc)); + } + + @Override + public Object toInternalValue(DatabaseImpl db, Object value) { + if(value instanceof TemporalAccessor) { + return toLocalDateTime((TemporalAccessor)value, db); + } + Instant inst = Instant.ofEpochMilli(toDateLong(value)); + return LocalDateTime.ofInstant(inst, db.getZoneId()); + } + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java index 8440f81..897a195 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java @@ -32,16 +32,16 @@ import com.healthmarketscience.jackcess.RuntimeIOException; import static com.healthmarketscience.jackcess.impl.OleUtil.*; import com.healthmarketscience.jackcess.util.MemFileChannel; import static com.healthmarketscience.jackcess.util.OleBlob.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.poi.poifs.filesystem.DirectoryEntry; import org.apache.poi.poifs.filesystem.DocumentEntry; import org.apache.poi.poifs.filesystem.DocumentInputStream; -import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; /** * Utility code for working with OLE data which is in the compound storage * format. This functionality relies on the optional POI library. - * <p/> + * <p> * Note that all POI usage is restricted to this file so that the basic ole * support in OleUtil can be utilized without requiring POI. * @@ -57,7 +57,7 @@ public class CompoundOleUtil implements CompoundPackageFactory static { // force a poi class to be loaded to ensure that when this class is // loaded, we know that the poi classes are available - NPOIFSFileSystem.class.getName(); + POIFSFileSystem.class.getName(); } public CompoundOleUtil() @@ -67,6 +67,7 @@ public class CompoundOleUtil implements CompoundPackageFactory /** * Creates a nes CompoundContent for the given blob information. */ + @Override public ContentImpl createCompoundPackageContent( OleBlobImpl blob, String prettyName, String className, String typeName, ByteBuffer blobBb, int dataBlockLen) @@ -139,7 +140,7 @@ public class CompoundOleUtil implements CompoundPackageFactory extends EmbeddedPackageContentImpl implements CompoundContent { - private NPOIFSFileSystem _fs; + private POIFSFileSystem _fs; private CompoundContentImpl( OleBlobImpl blob, String prettyName, String className, @@ -148,17 +149,19 @@ public class CompoundOleUtil implements CompoundPackageFactory super(blob, prettyName, className, typeName, position, length); } + @Override public ContentType getType() { return ContentType.COMPOUND_STORAGE; } - private NPOIFSFileSystem getFileSystem() throws IOException { + private POIFSFileSystem getFileSystem() throws IOException { if(_fs == null) { - _fs = new NPOIFSFileSystem(MemFileChannel.newChannel(getStream(), "r")); + _fs = new POIFSFileSystem(MemFileChannel.newChannel(getStream(), "r")); } return _fs; } + @Override public Iterator<Entry> iterator() { try { return getEntries(new ArrayList<Entry>(), getFileSystem().getRoot(), @@ -168,15 +171,18 @@ public class CompoundOleUtil implements CompoundPackageFactory } } + @Override public EntryImpl getEntry(String entryName) throws IOException { return new EntryImpl(entryName, getDocumentEntry(entryName, getFileSystem().getRoot())); } + @Override public boolean hasContentsEntry() throws IOException { return getFileSystem().getRoot().hasEntry(CONTENTS_ENTRY); } + @Override public EntryImpl getContentsEntry() throws IOException { return getEntry(CONTENTS_ENTRY); } @@ -230,30 +236,37 @@ public class CompoundOleUtil implements CompoundPackageFactory _docEntry = docEntry; } + @Override public ContentType getType() { return ContentType.UNKNOWN; } + @Override public String getName() { return _name; } + @Override public CompoundContentImpl getParent() { return CompoundContentImpl.this; } + @Override public OleBlobImpl getBlob() { return getParent().getBlob(); } + @Override public long length() { return _docEntry.getSize(); } + @Override public InputStream getStream() throws IOException { return new DocumentInputStream(_docEntry); } + @Override public void writeTo(OutputStream out) throws IOException { InputStream in = null; try { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java index 85a53dd..7b973a4 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CursorImpl.java @@ -102,10 +102,12 @@ public abstract class CursorImpl implements Cursor return _rowState; } + @Override public IdImpl getId() { return _id; } + @Override public TableImpl getTable() { return _table; } @@ -118,18 +120,22 @@ public abstract class CursorImpl implements Cursor return getTable().getPageChannel(); } + @Override public ErrorHandler getErrorHandler() { return _rowState.getErrorHandler(); } + @Override public void setErrorHandler(ErrorHandler newErrorHandler) { _rowState.setErrorHandler(newErrorHandler); } + @Override public ColumnMatcher getColumnMatcher() { return _columnMatcher; } + @Override public void setColumnMatcher(ColumnMatcher columnMatcher) { if(columnMatcher == null) { columnMatcher = getDefaultColumnMatcher(); @@ -144,10 +150,12 @@ public abstract class CursorImpl implements Cursor return SimpleColumnMatcher.INSTANCE; } + @Override public SavepointImpl getSavepoint() { return new SavepointImpl(_id, _curPos, _prevPos); } + @Override public void restoreSavepoint(Savepoint savepoint) throws IOException { @@ -180,22 +188,27 @@ public abstract class CursorImpl implements Cursor return _lastPos; } + @Override public void reset() { beforeFirst(); } + @Override public void beforeFirst() { reset(MOVE_FORWARD); } + @Override public void afterLast() { reset(MOVE_REVERSE); } + @Override public boolean isBeforeFirst() throws IOException { return isAtBeginning(MOVE_FORWARD); } + @Override public boolean isAfterLast() throws IOException { return isAtBeginning(MOVE_REVERSE); } @@ -207,6 +220,7 @@ public abstract class CursorImpl implements Cursor return false; } + @Override public boolean isCurrentRowDeleted() throws IOException { // we need to ensure that the "deleted" flag has been read for this row @@ -224,10 +238,12 @@ public abstract class CursorImpl implements Cursor _rowState.reset(); } + @Override public Iterator<Row> iterator() { return new RowIterator(null, true, MOVE_FORWARD); } + @Override public IterableBuilder newIterable() { return new IterableBuilder(this); } @@ -260,34 +276,41 @@ public abstract class CursorImpl implements Cursor } } + @Override public void deleteCurrentRow() throws IOException { _table.deleteRow(_rowState, _curPos.getRowId()); } + @Override public Object[] updateCurrentRow(Object... row) throws IOException { return _table.updateRow(_rowState, _curPos.getRowId(), row); } + @Override public <M extends Map<String,Object>> M updateCurrentRowFromMap(M row) throws IOException { return _table.updateRowFromMap(_rowState, _curPos.getRowId(), row); } + @Override public Row getNextRow() throws IOException { return getNextRow(null); } + @Override public Row getNextRow(Collection<String> columnNames) throws IOException { return getAnotherRow(columnNames, MOVE_FORWARD); } + @Override public Row getPreviousRow() throws IOException { return getPreviousRow(null); } + @Override public Row getPreviousRow(Collection<String> columnNames) throws IOException { @@ -299,7 +322,7 @@ public abstract class CursorImpl implements Cursor * Moves to another row in the table based on the given direction and * returns it. * @param columnNames Only column names in this collection will be returned - * @return another row in this table (Column name -> Column value), where + * @return another row in this table (Column name -> Column value), where * "next" may be backwards if moveForward is {@code false}, or * {@code null} if there is not another row in the given direction. */ @@ -313,11 +336,13 @@ public abstract class CursorImpl implements Cursor return null; } + @Override public boolean moveToNextRow() throws IOException { return moveToAnotherRow(MOVE_FORWARD); } + @Override public boolean moveToPreviousRow() throws IOException { return moveToAnotherRow(MOVE_REVERSE); @@ -407,6 +432,7 @@ public abstract class CursorImpl implements Cursor return(!_curPos.equals(getDirHandler(moveForward).getEndPosition())); } + @Override public boolean findRow(RowId rowId) throws IOException { RowIdImpl rowIdImpl = (RowIdImpl)rowId; @@ -435,6 +461,7 @@ public abstract class CursorImpl implements Cursor } } + @Override public boolean findFirstRow(Column columnPattern, Object valuePattern) throws IOException { @@ -449,6 +476,7 @@ public abstract class CursorImpl implements Cursor prepareSearchInfo(columnPattern, valuePattern)); } + @Override public boolean findNextRow(Column columnPattern, Object valuePattern) throws IOException { @@ -489,12 +517,14 @@ public abstract class CursorImpl implements Cursor } } + @Override public boolean findFirstRow(Map<String,?> rowPattern) throws IOException { return findAnotherRow(rowPattern, true, MOVE_FORWARD, _columnMatcher, prepareSearchInfo(rowPattern)); } + @Override public boolean findNextRow(Map<String,?> rowPattern) throws IOException { @@ -528,6 +558,7 @@ public abstract class CursorImpl implements Cursor } } + @Override public boolean currentRowMatches(Column columnPattern, Object valuePattern) throws IOException { @@ -550,6 +581,7 @@ public abstract class CursorImpl implements Cursor getCurrentRowValue(columnPattern)); } + @Override public boolean currentRowMatches(Map<String,?> rowPattern) throws IOException { @@ -663,11 +695,13 @@ public abstract class CursorImpl implements Cursor return true; } + @Override public int moveNextRows(int numRows) throws IOException { return moveSomeRows(numRows, MOVE_FORWARD); } + @Override public int movePreviousRows(int numRows) throws IOException { return moveSomeRows(numRows, MOVE_REVERSE); @@ -688,17 +722,20 @@ public abstract class CursorImpl implements Cursor return numMovedRows; } + @Override public Row getCurrentRow() throws IOException { return getCurrentRow(null); } + @Override public Row getCurrentRow(Collection<String> columnNames) throws IOException { return _table.getRow(_rowState, _curPos.getRowId(), columnNames); } + @Override public Object getCurrentRowValue(Column column) throws IOException { @@ -711,6 +748,7 @@ public abstract class CursorImpl implements Cursor return _table.getRowValue(_rowState, _curPos.getRowId(), column); } + @Override public void setCurrentRowValue(Column column, Object value) throws IOException { @@ -802,6 +840,7 @@ public abstract class CursorImpl implements Cursor } } + @Override public boolean hasNext() { if(_hasNext == null) { try { @@ -814,6 +853,7 @@ public abstract class CursorImpl implements Cursor return _hasNext; } + @Override public Row next() { if(!hasNext()) { throw new NoSuchElementException(); @@ -827,6 +867,7 @@ public abstract class CursorImpl implements Cursor } } + @Override public void remove() { if(_validRow) { try { @@ -983,6 +1024,7 @@ public abstract class CursorImpl implements Cursor /** * Returns the unique RowId of the position of the cursor. */ + @Override public abstract RowIdImpl getRowId(); /** @@ -1010,10 +1052,12 @@ public abstract class CursorImpl implements Cursor _prevPos = prevPos; } + @Override public IdImpl getCursorId() { return _cursorId; } + @Override public PositionImpl getCurrentPosition() { return _curPos; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java b/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java index 707e163..89ef061 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java @@ -21,9 +21,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.Map; -import org.apache.commons.lang.SystemUtils; -import org.apache.commons.lang.builder.StandardToStringStyle; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.StandardToStringStyle; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Custom ToStringStyle for use with ToStringBuilder. @@ -34,7 +33,7 @@ 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 ML_FIELD_SEP = System.lineSeparator() + " "; private static final String IMPL_SUFFIX = "Impl"; private static final int MAX_BYTE_DETAIL_LEN = 20; private static final Object IGNORE_ME = new Object(); @@ -47,7 +46,7 @@ public class CustomToStringStyle extends StandardToStringStyle setFieldSeparatorAtStart(true); setFieldNameValueSeparator(": "); setArraySeparator("," + ML_FIELD_SEP); - setContentEnd(SystemUtils.LINE_SEPARATOR + "]"); + setContentEnd(System.lineSeparator() + "]"); setUseShortClassName(true); } }; @@ -91,7 +90,7 @@ public class CustomToStringStyle extends StandardToStringStyle } @Override - protected String getShortClassName(Class clss) { + protected String getShortClassName(Class<?> clss) { String shortName = super.getShortClassName(clss); if(shortName.endsWith(IMPL_SUFFIX)) { shortName = shortName.substring(0, @@ -116,7 +115,7 @@ public class CustomToStringStyle extends StandardToStringStyle @Override protected void appendDetail(StringBuffer buffer, String fieldName, - Collection value) { + Collection<?> value) { buffer.append("["); // gather contents of list in a new StringBuffer @@ -145,13 +144,12 @@ public class CustomToStringStyle extends StandardToStringStyle @Override protected void appendDetail(StringBuffer buffer, String fieldName, - Map value) { + Map<?,?> value) { buffer.append("{"); // gather contents of map in a new StringBuffer StringBuffer sb = new StringBuffer(); - @SuppressWarnings("unchecked") - Iterator<Map.Entry<?,?>> iter = value.entrySet().iterator(); + Iterator<? extends Map.Entry<?,?>> iter = value.entrySet().iterator(); if(iter.hasNext()) { if(isFieldSeparatorAtStart()) { appendFieldSeparator(sb); @@ -203,7 +201,7 @@ public class CustomToStringStyle extends StandardToStringStyle private static String indent(Object obj) { return ((obj != null) ? obj.toString().replaceAll( - SystemUtils.LINE_SEPARATOR, ML_FIELD_SEP) : null); + System.lineSeparator(), ML_FIELD_SEP) : null); } public static Object ignoreNull(Object obj) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java index 7f50f68..227cdd8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java @@ -17,8 +17,8 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Map; import javax.script.Bindings; import javax.script.SimpleBindings; @@ -42,7 +42,7 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig private final DatabaseImpl _db; private FunctionLookup _funcs = DefaultFunctions.LOOKUP; - private Map<String,SimpleDateFormat> _sdfs; + private Map<String,DateTimeFormatter> _sdfs; private Map<String,DecimalFormat> _dfs; private TemporalConfig _temporal = TemporalConfig.US_TEMPORAL_CONFIG; private NumericConfig _numeric = NumericConfig.US_NUMERIC_CONFIG; @@ -57,10 +57,12 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig return _db; } + @Override public TemporalConfig getTemporalConfig() { return _temporal; } + @Override public void setTemporalConfig(TemporalConfig temporal) { if(_temporal != temporal) { _temporal = temporal; @@ -68,14 +70,17 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig } } - public Calendar getCalendar() { - return _db.getCalendar(); + @Override + public ZoneId getZoneId() { + return _db.getZoneId(); } + @Override public NumericConfig getNumericConfig() { return _numeric; } + @Override public void setNumericConfig(NumericConfig numeric) { if(_numeric != numeric) { _numeric = numeric; @@ -83,35 +88,40 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig } } + @Override public FunctionLookup getFunctionLookup() { return _funcs; } + @Override public void setFunctionLookup(FunctionLookup lookup) { _funcs = lookup; } + @Override public Bindings getBindings() { return _bindings; } + @Override public void setBindings(Bindings bindings) { _bindings = bindings; } - public SimpleDateFormat createDateFormat(String formatStr) { + @Override + public DateTimeFormatter createDateFormatter(String formatStr) { if(_sdfs == null) { - _sdfs = new SimpleCache<String,SimpleDateFormat>(MAX_CACHE_SIZE); + _sdfs = new SimpleCache<String,DateTimeFormatter>(MAX_CACHE_SIZE); } - SimpleDateFormat sdf = _sdfs.get(formatStr); + DateTimeFormatter sdf = _sdfs.get(formatStr); if(sdf == null) { - sdf = _db.createDateFormat(formatStr); - sdf.setDateFormatSymbols(_temporal.getDateFormatSymbols()); + sdf = DateTimeFormatter.ofPattern(formatStr, _temporal.getLocale()); _sdfs.put(formatStr, sdf); } return sdf; } + @Override public DecimalFormat createDecimalFormat(String formatStr) { if(_dfs == null) { _dfs = new SimpleCache<String,DecimalFormat>(MAX_CACHE_SIZE); @@ -128,8 +138,4 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig public float getRandom(Integer seed) { return _rndCtx.getRandom(seed); } - - void resetDateTimeConfig() { - _sdfs = null; - } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index bb74bba..1db1c3b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -20,7 +20,6 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; @@ -28,10 +27,14 @@ import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -53,6 +56,7 @@ import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.IndexCursor; @@ -73,8 +77,8 @@ import com.healthmarketscience.jackcess.util.LinkResolver; import com.healthmarketscience.jackcess.util.ReadOnlyFileChannel; import com.healthmarketscience.jackcess.util.SimpleColumnValidatorFactory; import com.healthmarketscience.jackcess.util.TableIterableBuilder; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -84,7 +88,7 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class DatabaseImpl implements Database +public class DatabaseImpl implements Database, DateTimeContext { private static final Log LOG = LogFactory.getLog(DatabaseImpl.class); @@ -198,9 +202,15 @@ public class DatabaseImpl implements Database SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG; /** read-only channel access mode */ - public static final String RO_CHANNEL_MODE = "r"; - /** read/write channel access mode */ - public static final String RW_CHANNEL_MODE = "rw"; + public static final OpenOption[] RO_CHANNEL_OPTS = + {StandardOpenOption.READ}; + /** read/write channel access mode for existing files */ + public static final OpenOption[] RW_CHANNEL_OPTS = + {StandardOpenOption.READ, StandardOpenOption.WRITE}; + /** read/write/create channel access mode for new files */ + public static final OpenOption[] RWC_CHANNEL_OPTS = + {StandardOpenOption.READ, StandardOpenOption.WRITE, + StandardOpenOption.CREATE}; /** Name of the system object that is the parent of all tables */ private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; @@ -252,7 +262,7 @@ public class DatabaseImpl implements Database Pattern.compile("[\\p{Cntrl}.!`\\]\\[]"); /** the File of the database */ - private final File _file; + private final Path _file; /** the simple name of the database */ private final String _name; /** whether or not this db is read-only */ @@ -300,6 +310,8 @@ public class DatabaseImpl implements Database private Charset _charset; /** timezone to use when handling dates */ private TimeZone _timeZone; + /** zoneId to use when handling dates */ + private ZoneId _zoneId; /** language sort order to be used for textual columns */ private ColumnImpl.SortOrder _defaultSortOrder; /** default code page to be used for textual columns (in some dbs) */ @@ -335,10 +347,10 @@ public class DatabaseImpl implements Database /** shared state used when enforcing foreign keys */ private final FKEnforcer.SharedState _fkEnforcerSharedState = FKEnforcer.initSharedState(); - /** Calendar for use interpreting dates/times in Columns */ - private Calendar _calendar; /** shared context for evaluating expressions */ private DBEvalContext _evalCtx; + /** factory for the appropriate date/time type */ + private ColumnImpl.DateTimeFactory _dtf; /** * Open an existing Database. If the existing file is not writeable or the @@ -364,23 +376,23 @@ public class DatabaseImpl implements Database * @usage _advanced_method_ */ public static DatabaseImpl open( - File mdbFile, boolean readOnly, FileChannel channel, + Path mdbFile, boolean readOnly, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone, CodecProvider provider) throws IOException { boolean closeChannel = false; if(channel == null) { - if(!mdbFile.exists() || !mdbFile.canRead()) { + if(!Files.isReadable(mdbFile)) { throw new FileNotFoundException("given file does not exist: " + mdbFile); } // force read-only for non-writable files - readOnly |= !mdbFile.canWrite(); + readOnly |= !Files.isWritable(mdbFile); // open file channel - channel = openChannel(mdbFile, readOnly); + channel = openChannel(mdbFile, readOnly, false); closeChannel = true; } @@ -434,7 +446,7 @@ public class DatabaseImpl implements Database * @param timeZone TimeZone to use, if {@code null}, uses default * @usage _advanced_method_ */ - public static DatabaseImpl create(FileFormat fileFormat, File mdbFile, + public static DatabaseImpl create(FileFormat fileFormat, Path mdbFile, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone) throws IOException @@ -451,7 +463,7 @@ public class DatabaseImpl implements Database boolean closeChannel = false; if(channel == null) { - channel = openChannel(mdbFile, false); + channel = openChannel(mdbFile, false, true); closeChannel = true; } @@ -486,11 +498,13 @@ public class DatabaseImpl implements Database * that name cannot be created, or if some other error occurs * while opening or creating the file */ - static FileChannel openChannel(final File mdbFile, final boolean readOnly) - throws FileNotFoundException + static FileChannel openChannel( + Path mdbFile, boolean readOnly, boolean create) + throws IOException { - final String mode = (readOnly ? RO_CHANNEL_MODE : RW_CHANNEL_MODE); - return new RandomAccessFile(mdbFile, mode).getChannel(); + OpenOption[] opts = (readOnly ? RO_CHANNEL_OPTS : + (create ? RWC_CHANNEL_OPTS : RW_CHANNEL_OPTS)); + return FileChannel.open(mdbFile, opts); } /** @@ -512,7 +526,7 @@ public class DatabaseImpl implements Database * @param charset Charset to use, if {@code null}, uses default * @param timeZone TimeZone to use, if {@code null}, uses default */ - protected DatabaseImpl(File file, FileChannel channel, boolean closeChannel, + protected DatabaseImpl(Path file, FileChannel channel, boolean closeChannel, boolean autoSync, FileFormat fileFormat, Charset charset, TimeZone timeZone, CodecProvider provider, boolean readOnly) @@ -528,8 +542,9 @@ public class DatabaseImpl implements Database _allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); _evaluateExpressions = getDefaultEvaluateExpressions(); _fileFormat = fileFormat; + setZoneInfo(timeZone, null); + _dtf = ColumnImpl.getDateTimeFactory(getDefaultDateTimeType()); _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); - _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); if(provider == null) { provider = DefaultCodecProvider.INSTANCE; } @@ -541,7 +556,13 @@ public class DatabaseImpl implements Database readSystemCatalog(); } + @Override public File getFile() { + return ((_file != null) ? _file.toFile() : null); + } + + @Override + public Path getPath() { return _file; } @@ -597,27 +618,33 @@ public class DatabaseImpl implements Database return _complexCols; } + @Override public ErrorHandler getErrorHandler() { return((_dbErrorHandler != null) ? _dbErrorHandler : ErrorHandler.DEFAULT); } + @Override public void setErrorHandler(ErrorHandler newErrorHandler) { _dbErrorHandler = newErrorHandler; } + @Override public LinkResolver getLinkResolver() { return((_linkResolver != null) ? _linkResolver : LinkResolver.DEFAULT); } + @Override public void setLinkResolver(LinkResolver newLinkResolver) { _linkResolver = newLinkResolver; } + @Override public Map<String,Database> getLinkedDatabases() { return ((_linkedDbs == null) ? Collections.<String,Database>emptyMap() : Collections.unmodifiableMap(_linkedDbs)); } + @Override public boolean isLinkedTable(Table table) throws IOException { if((table == null) || (this == table.getDatabase())) { @@ -645,27 +672,62 @@ public class DatabaseImpl implements Database (_linkedDbs.get(linkedDbName) == table.getDatabase())); } + @Override public TimeZone getTimeZone() { return _timeZone; } + @Override public void setTimeZone(TimeZone newTimeZone) { - if(newTimeZone == null) { + setZoneInfo(newTimeZone, null); + } + + @Override + public ZoneId getZoneId() { + return _zoneId; + } + + @Override + public void setZoneId(ZoneId newZoneId) { + setZoneInfo(null, newZoneId); + } + + private void setZoneInfo(TimeZone newTimeZone, ZoneId newZoneId) { + if(newTimeZone != null) { + newZoneId = newTimeZone.toZoneId(); + } else if(newZoneId != null) { + newTimeZone = TimeZone.getTimeZone(newZoneId); + } else { newTimeZone = getDefaultTimeZone(); + newZoneId = newTimeZone.toZoneId(); } + _timeZone = newTimeZone; - // clear cached calendar(s) when timezone is changed - _calendar = null; - if(_evalCtx != null) { - _evalCtx.resetDateTimeConfig(); - } + _zoneId = newZoneId; } + @Override + public DateTimeType getDateTimeType() { + return _dtf.getType(); + } + + @Override + public void setDateTimeType(DateTimeType dateTimeType) { + _dtf = ColumnImpl.getDateTimeFactory(dateTimeType); + } + + @Override + public ColumnImpl.DateTimeFactory getDateTimeFactory() { + return _dtf; + } + + @Override public Charset getCharset() { return _charset; } + @Override public void setCharset(Charset newCharset) { if(newCharset == null) { newCharset = getDefaultCharset(getFormat()); @@ -673,10 +735,12 @@ public class DatabaseImpl implements Database _charset = newCharset; } + @Override public Table.ColumnOrder getColumnOrder() { return _columnOrder; } + @Override public void setColumnOrder(Table.ColumnOrder newColumnOrder) { if(newColumnOrder == null) { newColumnOrder = getDefaultColumnOrder(); @@ -684,10 +748,12 @@ public class DatabaseImpl implements Database _columnOrder = newColumnOrder; } + @Override public boolean isEnforceForeignKeys() { return _enforceForeignKeys; } + @Override public void setEnforceForeignKeys(Boolean newEnforceForeignKeys) { if(newEnforceForeignKeys == null) { newEnforceForeignKeys = getDefaultEnforceForeignKeys(); @@ -695,10 +761,12 @@ public class DatabaseImpl implements Database _enforceForeignKeys = newEnforceForeignKeys; } + @Override public boolean isAllowAutoNumberInsert() { return _allowAutoNumInsert; } + @Override public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) { if(allowAutoNumInsert == null) { allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); @@ -706,10 +774,12 @@ public class DatabaseImpl implements Database _allowAutoNumInsert = allowAutoNumInsert; } + @Override public boolean isEvaluateExpressions() { return _evaluateExpressions; } + @Override public void setEvaluateExpressions(Boolean evaluateExpressions) { if(evaluateExpressions == null) { evaluateExpressions = getDefaultEvaluateExpressions(); @@ -717,10 +787,12 @@ public class DatabaseImpl implements Database _evaluateExpressions = evaluateExpressions; } + @Override public ColumnValidatorFactory getColumnValidatorFactory() { return _validatorFactory; } + @Override public void setColumnValidatorFactory(ColumnValidatorFactory newFactory) { if(newFactory == null) { newFactory = SimpleColumnValidatorFactory.INSTANCE; @@ -735,17 +807,7 @@ public class DatabaseImpl implements Database return _fkEnforcerSharedState; } - /** - * @usage _advanced_method_ - */ - Calendar getCalendar() { - if(_calendar == null) { - _calendar = DatabaseBuilder.toCompatibleCalendar( - Calendar.getInstance(_timeZone)); - } - return _calendar; - } - + @Override public EvalConfig getEvalConfig() { return getEvalContext(); } @@ -783,6 +845,7 @@ public class DatabaseImpl implements Database return _propsHandler; } + @Override public FileFormat getFileFormat() throws IOException { if(_fileFormat == null) { @@ -940,6 +1003,7 @@ public class DatabaseImpl implements Database } } + @Override public Set<String> getTableNames() throws IOException { if(_tableNames == null) { _tableNames = getTableNames(true, false, true); @@ -947,6 +1011,7 @@ public class DatabaseImpl implements Database return _tableNames; } + @Override public Set<String> getSystemTableNames() throws IOException { return getTableNames(false, true, false); } @@ -961,6 +1026,7 @@ public class DatabaseImpl implements Database return tableNames; } + @Override public Iterator<Table> iterator() { try { return new TableIterator(getTableNames()); @@ -979,14 +1045,17 @@ public class DatabaseImpl implements Database } } + @Override public TableIterableBuilder newIterable() { return new TableIterableBuilder(this); } + @Override public TableImpl getTable(String name) throws IOException { return getTable(name, false); } + @Override public TableMetaData getTableMetaData(String name) throws IOException { return getTableInfo(name, true); } @@ -1101,6 +1170,7 @@ public class DatabaseImpl implements Database .toTable(this); } + @Override public void createLinkedTable(String name, String linkedDbName, String linkedTableName) throws IOException @@ -1147,6 +1217,7 @@ public class DatabaseImpl implements Database addToAccessControlEntries(tdefPageNumber, _tableParentId, _newTableSIDs); } + @Override public List<Relationship> getRelationships(Table table1, Table table2) throws IOException { @@ -1174,6 +1245,7 @@ public class DatabaseImpl implements Database return getRelationshipsImpl(table1, table2, true); } + @Override public List<Relationship> getRelationships(Table table) throws IOException { @@ -1185,12 +1257,14 @@ public class DatabaseImpl implements Database return getRelationshipsImpl((TableImpl)table, null, true); } + @Override public List<Relationship> getRelationships() throws IOException { return getRelationshipsImpl(null, null, false); } + @Override public List<Relationship> getSystemRelationships() throws IOException { @@ -1333,6 +1407,7 @@ public class DatabaseImpl implements Database return ((i == 0) ? origName : (origName + i)); } + @Override public List<Query> getQueries() throws IOException { // the queries table does not get loaded until first accessed @@ -1382,6 +1457,7 @@ public class DatabaseImpl implements Database return queries; } + @Override public TableImpl getSystemTable(String tableName) throws IOException { return getTable(tableName, true); @@ -1397,6 +1473,7 @@ public class DatabaseImpl implements Database return table; } + @Override public PropertyMap getDatabaseProperties() throws IOException { if(_dbPropMaps == null) { _dbPropMaps = getPropertiesForDbObject(OBJECT_NAME_DB_PROPS); @@ -1404,6 +1481,7 @@ public class DatabaseImpl implements Database return _dbPropMaps.getDefault(); } + @Override public PropertyMap getSummaryProperties() throws IOException { if(_summaryPropMaps == null) { _summaryPropMaps = getPropertiesForDbObject(OBJECT_NAME_SUMMARY_PROPS); @@ -1411,6 +1489,7 @@ public class DatabaseImpl implements Database return _summaryPropMaps.getDefault(); } + @Override public PropertyMap getUserDefinedProperties() throws IOException { if(_userDefPropMaps == null) { _userDefPropMaps = getPropertiesForDbObject(OBJECT_NAME_USERDEF_PROPS); @@ -1488,6 +1567,7 @@ public class DatabaseImpl implements Database return getPropsHandler().read(propsBytes, objectId, rowId, owner); } + @Override public String getDatabasePassword() throws IOException { ByteBuffer buffer = takeSharedBuffer(); @@ -1754,6 +1834,7 @@ public class DatabaseImpl implements Database return CursorImpl.createCursor(table); } + @Override public void flush() throws IOException { if(_linkedDbs != null) { for(Database linkedDb : _linkedDbs.values()) { @@ -1763,6 +1844,7 @@ public class DatabaseImpl implements Database _pageChannel.flush(); } + @Override public void close() throws IOException { if(_linkedDbs != null) { for(Database linkedDb : _linkedDbs.values()) { @@ -1968,16 +2050,8 @@ public class DatabaseImpl implements Database */ public static Table.ColumnOrder getDefaultColumnOrder() { - String coProp = System.getProperty(COLUMN_ORDER_PROPERTY); - if(coProp != null) { - coProp = coProp.trim(); - if(coProp.length() > 0) { - return Table.ColumnOrder.valueOf(coProp); - } - } - - // use default order - return DEFAULT_COLUMN_ORDER; + return getEnumSystemProperty(Table.ColumnOrder.class, COLUMN_ORDER_PROPERTY, + DEFAULT_COLUMN_ORDER); } /** @@ -2026,6 +2100,17 @@ public class DatabaseImpl implements Database } /** + * Returns the default DateTimeType. This defaults to + * {@link DateTimeType#DATE}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#DATE_TIME_TYPE_PROPERTY}. + * @usage _advanced_method_ + */ + public static DateTimeType getDefaultDateTimeType() { + return getEnumSystemProperty(DateTimeType.class, DATE_TIME_TYPE_PROPERTY, + DateTimeType.DATE); + } + + /** * Copies the given db InputStream to the given channel using the most * efficient means possible. */ @@ -2106,11 +2191,11 @@ public class DatabaseImpl implements Database FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format)); } - private static String getName(File file) { + private static String getName(Path file) { if(file == null) { return "<UNKNOWN.DB>"; } - return file.getName(); + return file.getFileName().toString(); } private String withErrorContext(String msg) { @@ -2121,6 +2206,19 @@ public class DatabaseImpl implements Database return msg + " (Db=" + dbName + ")"; } + private static <E extends Enum<E>> E getEnumSystemProperty( + Class<E> enumClass, String propName, E defaultValue) + { + String prop = System.getProperty(propName); + if(prop != null) { + prop = prop.trim().toUpperCase(); + if(!prop.isEmpty()) { + return Enum.valueOf(enumClass, prop); + } + } + return defaultValue; + } + /** * Utility class for storing table page number and actual name. */ @@ -2136,26 +2234,32 @@ public class DatabaseImpl implements Database flags = newFlags; } + @Override public String getName() { return tableName; } + @Override public boolean isLinked() { return false; } + @Override public boolean isSystem() { return isSystemObject(flags); } + @Override public String getLinkedTableName() { return null; } + @Override public String getLinkedDbName() { return null; } + @Override public Table open(Database db) throws IOException { return ((DatabaseImpl)db).getTable(this, true); } @@ -2219,14 +2323,17 @@ public class DatabaseImpl implements Database _tableNameIter = tableNames.iterator(); } + @Override public boolean hasNext() { return _tableNameIter.hasNext(); } + @Override public void remove() { throw new UnsupportedOperationException(); } + @Override public Table next() { if(!hasNext()) { throw new NoSuchElementException(); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java new file mode 100644 index 0000000..8045755 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java @@ -0,0 +1,34 @@ +/* +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.time.ZoneId; +import java.util.TimeZone; + +/** + * Provider of zone related info for date/time conversions. + * + * @author James Ahlborn + */ +interface DateTimeContext +{ + public ZoneId getZoneId(); + + public TimeZone getTimeZone(); + + public ColumnImpl.DateTimeFactory getDateTimeFactory(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java b/src/main/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java index 811f5b4..11854e7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DefaultCodecProvider.java @@ -48,6 +48,7 @@ public class DefaultCodecProvider implements CodecProvider * This implementation returns DUMMY_HANDLER for databases with no encoding * and UNSUPPORTED_HANDLER for databases with any encoding. */ + @Override public CodecHandler createHandler(PageChannel channel, Charset charset) throws IOException { @@ -80,14 +81,17 @@ public class DefaultCodecProvider implements CodecProvider */ public static class DummyHandler implements CodecHandler { + @Override public boolean canEncodePartialPage() { return true; } + @Override public boolean canDecodeInline() { return true; } + @Override public void decodePage(ByteBuffer inPage, ByteBuffer outPage, int pageNumber) throws IOException @@ -95,6 +99,7 @@ public class DefaultCodecProvider implements CodecProvider // does nothing } + @Override public ByteBuffer encodePage(ByteBuffer page, int pageNumber, int pageOffset) throws IOException @@ -111,14 +116,17 @@ public class DefaultCodecProvider implements CodecProvider */ public static class UnsupportedHandler implements CodecHandler { + @Override public boolean canEncodePartialPage() { return true; } + @Override public boolean canDecodeInline() { return true; } + @Override public void decodePage(ByteBuffer inPage, ByteBuffer outPage, int pageNumber) throws IOException @@ -126,6 +134,7 @@ public class DefaultCodecProvider implements CodecProvider throw new UnsupportedCodecException("Decoding not supported. Please choose a CodecProvider which supports reading the current database encoding."); } + @Override public ByteBuffer encodePage(ByteBuffer page, int pageNumber, int pageOffset) throws IOException diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java index 21024e2..6c58182 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexCursorImpl.java @@ -129,10 +129,12 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor return _indexEntryPattern; } + @Override public IndexImpl getIndex() { return _index; } + @Override public Row findRowByEntry(Object... entryValues) throws IOException { @@ -142,6 +144,7 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor return null; } + @Override public boolean findFirstRowByEntry(Object... entryValues) throws IOException { @@ -163,6 +166,7 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor } } + @Override public void findClosestRowByEntry(Object... entryValues) throws IOException { @@ -184,12 +188,14 @@ public class IndexCursorImpl extends CursorImpl implements IndexCursor } } + @Override public boolean currentRowMatchesEntry(Object... entryValues) throws IOException { return currentRowMatchesEntryImpl(toRowValues(entryValues), _columnMatcher); } + @Override public EntryIterableBuilder newEntryIterable(Object... entryValues) { return new EntryIterableBuilder(this, entryValues); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java index e2b2dd5..ca91fef 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -33,7 +33,7 @@ import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.RuntimeIOException; import static com.healthmarketscience.jackcess.impl.ByteUtil.ByteStream; import static com.healthmarketscience.jackcess.impl.IndexCodes.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -41,17 +41,17 @@ import org.apache.commons.logging.LogFactory; * Access table index data. This is the actual data which backs a logical * Index, where one or more logical indexes can be backed by the same index * data. - * + * * @author Tim McCune */ public class IndexData { - + protected static final Log LOG = LogFactory.getLog(Index.class); /** special entry which is less than any other entry */ public static final Entry FIRST_ENTRY = createSpecialEntry(RowIdImpl.FIRST_ROW_ID); - + /** special entry which is greater than any other entry */ public static final Entry LAST_ENTRY = createSpecialEntry(RowIdImpl.LAST_ROW_ID); @@ -65,12 +65,12 @@ public class IndexData { public static final Object MIN_VALUE = new Object(); private static final DataPage NEW_ROOT_DATA_PAGE = new RootDataPage(); - - protected static final int INVALID_INDEX_PAGE_NUMBER = 0; - + + protected static final int INVALID_INDEX_PAGE_NUMBER = 0; + /** Max number of columns in an index */ public static final int MAX_COLUMNS = 10; - + protected static final byte[] EMPTY_PREFIX = new byte[0]; static final short COLUMN_UNUSED = -1; @@ -85,7 +85,7 @@ public class IndexData { private static final int MAGIC_INDEX_NUMBER = 1923; private static final ByteOrder ENTRY_BYTE_ORDER = ByteOrder.BIG_ENDIAN; - + /** type attributes for Entries which simplify comparisons */ public enum EntryType { /** comparable type indicating this Entry should always compare less than @@ -104,9 +104,10 @@ public class IndexData { than valid RowIds */ ALWAYS_LAST; } - + public static final Comparator<byte[]> BYTE_CODE_COMPARATOR = new Comparator<byte[]>() { + @Override public int compare(byte[] left, byte[] right) { if(left == right) { return 0; @@ -131,8 +132,8 @@ public class IndexData { ((left.length > right.length) ? 1 : 0)); } }; - - + + /** name, generated on demand */ private String _name; /** owning table */ @@ -175,7 +176,7 @@ public class IndexData { private String _unsupportedReason; /** Cache which manages the index pages */ private final IndexPageCache _pageCache; - + protected IndexData(TableImpl table, int number, int uniqueEntryCount, int uniqueEntryCountOffset) { @@ -216,14 +217,14 @@ public class IndexData { } else { _name = String.valueOf(_number); } - } + } return _name; } public TableImpl getTable() { return _table; } - + public JetFormat getFormat() { return getTable().getFormat(); } @@ -280,7 +281,7 @@ public class IndexData { public int getIndexDataNumber() { return _number; } - + public int getUniqueEntryCount() { return _uniqueEntryCount; } @@ -315,7 +316,7 @@ public class IndexData { public boolean isUnique() { return(isBackingPrimaryKey() || ((_indexFlags & UNIQUE_INDEX_FLAG) != 0)); } - + /** * Whether or not values are required in the columns. */ @@ -345,7 +346,7 @@ public class IndexData { return _rootPageNumber; } - private void setUnsupportedReason(String reason, ColumnImpl col) { + private void setUnsupportedReason(String reason, ColumnImpl col) { _unsupportedReason = withErrorContext(reason); if(!col.getTable().isSystem()) { LOG.warn(_unsupportedReason + ", making read-only"); @@ -371,7 +372,7 @@ public class IndexData { public int getOwnedPageCount() { return _ownedPages.getPageCount(); } - + void addOwnedPage(int pageNumber) throws IOException { _ownedPages.addPageNumber(pageNumber); } @@ -379,7 +380,7 @@ public class IndexData { void collectUsageMapPages(Collection<Integer> pages) { pages.add(_ownedPages.getTablePageNumber()); } - + /** * Used by unit tests to validate the internal status of the index. * @usage _advanced_method_ @@ -407,7 +408,7 @@ public class IndexData { } return count; } - + /** * Forces initialization of this index (actual parsing of index pages). * normally, the index will not be initialized until the entries are @@ -429,7 +430,7 @@ public class IndexData { { // make sure we've parsed the entries initialize(); - + if(_unsupportedReason != null) { throw new UnsupportedOperationException( "Cannot write indexes of this type due to " + _unsupportedReason); @@ -470,7 +471,7 @@ public class IndexData { } _ownedPages = UsageMap.read(getTable().getDatabase(), tableBuffer); - + _rootPageNumber = tableBuffer.getInt(); ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_FLAGS); //Forward past Unknown @@ -525,7 +526,7 @@ public class IndexData { * @param buffer Buffer to write to */ protected static void writeDefinition( - TableMutator creator, ByteBuffer buffer, + TableMutator creator, ByteBuffer buffer, TableMutator.IndexDataState idxDataState, ByteBuffer rootPageBuffer) throws IOException { @@ -559,7 +560,7 @@ public class IndexData { creator.getDatabase(), creator.getTableName(), idx.getName())); } } - + buffer.putShort(columnNumber); // table column number buffer.put(flags); // column flags (e.g. ordering) } @@ -568,7 +569,7 @@ public class IndexData { ByteUtil.put3ByteInt(buffer, idxDataState.getUmapPageNumber()); // umap page // write empty root index page - creator.getPageChannel().writePage(rootPageBuffer, + creator.getPageChannel().writePage(rootPageBuffer, idxDataState.getRootPageNumber()); buffer.putInt(idxDataState.getRootPageNumber()); @@ -577,11 +578,11 @@ public class IndexData { ByteUtil.forward(buffer, 5); // unknown } - private static ByteBuffer createRootPageBuffer(TableMutator creator) + private static ByteBuffer createRootPageBuffer(TableMutator creator) throws IOException { ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer(); - writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE, + writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE, creator.getTdefPageNumber(), creator.getFormat()); return rootPageBuffer; } @@ -591,7 +592,7 @@ public class IndexData { * this method returns. * <p> * Forces index initialization. - * + * * @param row Row to add * @param rowId rowId of the row to be added * @@ -603,7 +604,7 @@ public class IndexData { { return prepareAddRow(row, rowId, new AddRowPendingChange(nextChange)); } - + private PendingChange prepareAddRow(Object[] row, RowIdImpl rowId, AddRowPendingChange change) throws IOException @@ -619,7 +620,7 @@ public class IndexData { "Null value found in row " + Arrays.asList(row) + " for primary key or required index")); } - + // make sure we've parsed the entries initialize(); @@ -637,14 +638,14 @@ public class IndexData { DataPage dataPage = findDataPage(newEntry); int idx = dataPage.findEntry(newEntry); if(idx < 0) { - + // this is a new entry idx = missingIndexToInsertionPoint(idx); Position newPos = new Position(dataPage, idx, newEntry, true); Position nextPos = getNextPosition(newPos); Position prevPos = getPreviousPosition(newPos); - + // determine if the addition of this entry would break the uniqueness // constraint. See isUnique() for some notes about uniqueness as // defined by Access. @@ -691,7 +692,7 @@ public class IndexData { * before this method returns. * <p> * Forces index initialization. - * + * * @param oldRow Row to be removed * @param newRow Row to be added * @param rowId rowId of the row to be updated @@ -699,7 +700,7 @@ public class IndexData { * @return a PendingChange which can complete the update or roll it back */ public PendingChange prepareUpdateRow(Object[] oldRow, RowIdImpl rowId, - Object[] newRow, + Object[] newRow, PendingChange nextChange) throws IOException { @@ -715,12 +716,12 @@ public class IndexData { throw e; } } - + /** * Removes a row from this index * <p> * Forces index initialization. - * + * * @param row Row to remove * @param rowId rowId of the row to be removed */ @@ -729,7 +730,7 @@ public class IndexData { { deleteRowImpl(row, rowId); } - + private Entry deleteRowImpl(Object[] row, RowIdImpl rowId) throws IOException { @@ -738,7 +739,7 @@ public class IndexData { // nothing to do return null; } - + // make sure we've parsed the entries initialize(); @@ -748,7 +749,7 @@ public class IndexData { ++_modCount; } else { LOG.warn(withErrorContext( - "Failed removing index entry " + oldEntry + " for row: " + + "Failed removing index entry " + oldEntry + " for row: " + Arrays.asList(row))); } return removedEntry; @@ -773,7 +774,7 @@ public class IndexData { dataPage.addEntry(missingIndexToInsertionPoint(idx), removedEntry); } } - + /** * Removes an entry from the relevant index dataPage, maintaining the order. * Will search by RowId if entry is not found (in case a partial entry was @@ -810,10 +811,10 @@ public class IndexData { // found it! removedEntry = dataPage.removeEntry(idx); } - + return removedEntry; } - + public static void commitAll(PendingChange change) throws IOException { while(change != null) { change.commit(); @@ -827,7 +828,7 @@ public class IndexData { change = change.getNext(); } } - + /** * Gets a new cursor for this index. * <p> @@ -838,13 +839,13 @@ public class IndexData { { return cursor(null, true, null, true); } - + /** * Gets a new cursor for this index, narrowed to the range defined by the * given startRow and endRow. * <p> * Forces index initialization. - * + * * @param startRow the first row of data for the cursor, or {@code null} for * the first entry * @param startInclusive whether or not startRow is inclusive or exclusive @@ -1008,13 +1009,13 @@ public class IndexData { int valIdx = 0; Object[] idxRow = new Object[getTable().getColumnCount()]; for(ColumnDescriptor col : _columns) { - idxRow[col.getColumnIndex()] = + idxRow[col.getColumnIndex()] = ((valIdx < values.length) ? values[valIdx] : filler); ++valIdx; } return idxRow; } - + /** * Constructs an array of values appropriate for this index from the given * column value. @@ -1057,7 +1058,7 @@ public class IndexData { idxRow[col.getColumnIndex()] = row.get(col.getName()); } return idxRow; - } + } /** * Constructs an array of values appropriate for this index from the given @@ -1088,7 +1089,7 @@ public class IndexData { Object[] idxRow = new Object[getTable().getColumnCount()]; int valIdx = 0; for(ColumnDescriptor col : _columns) { - idxRow[col.getColumnIndex()] = + idxRow[col.getColumnIndex()] = ((valIdx < numCols) ? row.get(col.getName()) : filler); ++valIdx; } @@ -1116,7 +1117,7 @@ public class IndexData { sb.append("pageCache", _pageCache); return sb.toString(); } - + /** * Write the given index page out to a buffer */ @@ -1126,7 +1127,7 @@ public class IndexData { if(dataPage.getCompressedEntrySize() > _maxPageEntrySize) { throw new IllegalStateException(withErrorContext("data page is too large")); } - + ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel()); writeDataPage(buffer, dataPage, getTable().getTableDefPageNumber(), @@ -1170,7 +1171,7 @@ public class IndexData { // first entry includes the prefix buffer.put(entryPrefix); - + for(Entry entry : dataPage.getEntries()) { entry.write(buffer, entryPrefix); } @@ -1232,7 +1233,7 @@ public class IndexData { "Unexpected order in index entries, " + prevEntry + " >= " + entry)); } - + entries.add(entry); if((entries.size() == 1) && (entryPrefixLength > 0)) { @@ -1251,7 +1252,7 @@ public class IndexData { dataPage.setEntryPrefix(entryPrefix != null ? entryPrefix : EMPTY_PREFIX); dataPage.setEntries(entries); dataPage.setTotalEntrySize(totalEntrySize); - + int prevPageNumber = buffer.getInt(getFormat().OFFSET_PREV_INDEX_PAGE); int nextPageNumber = buffer.getInt(getFormat().OFFSET_NEXT_INDEX_PAGE); int childTailPageNumber = @@ -1265,7 +1266,7 @@ public class IndexData { /** * Returns a new Entry of the correct type for the given data and page type. */ - private static Entry newEntry(ByteBuffer buffer, int entryLength, + private static Entry newEntry(ByteBuffer buffer, int entryLength, boolean isLeaf) throws IOException { @@ -1291,10 +1292,10 @@ public class IndexData { tmpEntryBuffer.put(valuePrefix); tmpEntryBuffer.put(indexPage.array(), indexPage.position(), entryLen); tmpEntryBuffer.flip(); - + return tmpEntryBuffer; } - + /** * Determines if the given index page is a leaf or node page. */ @@ -1309,7 +1310,7 @@ public class IndexData { } throw new IOException(withErrorContext("Unexpected page type " + pageType)); } - + /** * Determines the number of {@code null} values for this index from the * given row. @@ -1319,7 +1320,7 @@ public class IndexData { if(values == null) { return _columns.size(); } - + // annoyingly, the values array could come from different sources, one // of which will make it a different size than the other. we need to // handle both situations. @@ -1330,7 +1331,7 @@ public class IndexData { ++nullCount; } } - + return nullCount; } @@ -1342,12 +1343,12 @@ public class IndexData { if(values == null) { return null; } - + if(_entryBuffer == null) { _entryBuffer = new ByteStream(); } _entryBuffer.reset(); - + for(ColumnDescriptor col : _columns) { Object value = values[col.getColumnIndex()]; @@ -1373,9 +1374,9 @@ public class IndexData { col.writeValue(value, _entryBuffer); } - + return _entryBuffer.toByteArray(); - } + } /** * Finds the data page for the given entry. @@ -1385,7 +1386,7 @@ public class IndexData { { return _pageCache.findCacheDataPage(entry); } - + /** * Gets the data page for the pageNumber. */ @@ -1394,7 +1395,7 @@ public class IndexData { { return _pageCache.getCacheDataPage(pageNumber); } - + /** * Flips the first bit in the byte at the given index. */ @@ -1418,7 +1419,7 @@ public class IndexData { static byte[] flipBytes(byte[] value, int offset, int length) { for(int i = offset; i < (offset + length); ++i) { value[i] = (byte)(~value[i]); - } + } return value; } @@ -1430,7 +1431,7 @@ public class IndexData { { // always write in big endian order return column.write(value, 0, ENTRY_BYTE_ORDER).array(); - } + } /** * Writes a binary value using the general binary entry encoding rules. @@ -1452,7 +1453,7 @@ public class IndexData { // bit twiddling rules: // - isAsc => nothing - // - !isAsc => flipBytes, _but keep intermediate 09 unflipped_! + // - !isAsc => flipBytes, _but keep intermediate 09 unflipped_! // first, write any intermediate segements int segmentLen = dataLen; @@ -1549,7 +1550,7 @@ public class IndexData { default: // we can't modify this index at this point in time - setUnsupportedReason("unsupported data type " + col.getType() + + setUnsupportedReason("unsupported data type " + col.getType() + " for index", col); return new ReadOnlyColumnDescriptor(col, flags); } @@ -1598,7 +1599,7 @@ public class IndexData { return msg + " (Db=" + db.getName() + ";Table=" + tableName + ";Index=" + idxName + ")"; } - + /** * Information about the columns in an index. Also encodes new index * values. @@ -1615,6 +1616,7 @@ public class IndexData { _flags = flags; } + @Override public ColumnImpl getColumn() { return _column; } @@ -1623,14 +1625,17 @@ public class IndexData { return _flags; } + @Override public boolean isAscending() { return((getFlags() & ASCENDING_COLUMN_FLAG) != 0); } - + + @Override public int getColumnIndex() { return getColumn().getColumnIndex(); } - + + @Override public String getName() { return getColumn().getName(); } @@ -1638,7 +1643,7 @@ public class IndexData { protected boolean isNullValue(Object value) { return (value == null); } - + protected final void writeValue(Object value, ByteStream bout) throws IOException { @@ -1647,7 +1652,7 @@ public class IndexData { bout.write(getNullEntryFlag(isAscending())); return; } - + // write the start flag bout.write(getStartEntryFlag(isAscending())); // write the rest of the value @@ -1655,8 +1660,8 @@ public class IndexData { } protected abstract void writeNonNullValue(Object value, ByteStream bout) - throws IOException; - + throws IOException; + @Override public String toString() { return CustomToStringStyle.builder(this) @@ -1676,26 +1681,26 @@ public class IndexData { { super(column, flags); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { byte[] valueBytes = encodeNumberColumnValue(value, getColumn()); - + // bit twiddling rules: // - isAsc => flipFirstBit // - !isAsc => flipFirstBit, flipBytes - + flipFirstBitInByte(valueBytes, 0); if(!isAscending()) { flipBytes(valueBytes); } - + bout.write(valueBytes); - } + } } - + /** * ColumnDescriptor for floating point based columns. */ @@ -1707,13 +1712,13 @@ public class IndexData { { super(column, flags); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { byte[] valueBytes = encodeNumberColumnValue(value, getColumn()); - + // determine if the number is negative by testing if the first bit is // set boolean isNegative = ((valueBytes[0] & 0x80) != 0); @@ -1723,18 +1728,18 @@ public class IndexData { // isAsc && isNeg => flipBytes // !isAsc && !isNeg => flipFirstBit, flipBytes // !isAsc && isNeg => nothing - + if(!isNegative) { flipFirstBitInByte(valueBytes, 0); } if(isNegative == isAscending()) { flipBytes(valueBytes); } - + bout.write(valueBytes); - } + } } - + /** * ColumnDescriptor for fixed point based columns (legacy sort order). */ @@ -1755,15 +1760,15 @@ public class IndexData { } // reverse the sign byte (after any previous byte flipping) - valueBytes[0] = (isNegative ? (byte)0x00 : (byte)0xFF); + valueBytes[0] = (isNegative ? (byte)0x00 : (byte)0xFF); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { byte[] valueBytes = encodeNumberColumnValue(value, getColumn()); - + // determine if the number is negative by testing if the first bit is // set boolean isNegative = ((valueBytes[0] & 0x80) != 0); @@ -1773,7 +1778,7 @@ public class IndexData { // isAsc && isNeg => flipBytes, setReverseSignByte => 00 FF FF ... // !isAsc && !isNeg => flipBytes, setReverseSignByte => FF FF FF ... // !isAsc && isNeg => setReverseSignByte => 00 00 00 ... - + // v2007 bit twiddling rules (old ordering was a bug, MS kb 837148): // isAsc && !isNeg => setSignByte 0xFF => FF 00 00 ... // isAsc && isNeg => setSignByte 0xFF, flipBytes => 00 FF FF ... @@ -1782,9 +1787,9 @@ public class IndexData { handleNegationAndOrder(isNegative, valueBytes); bout.write(valueBytes); - } + } } - + /** * ColumnDescriptor for new-style fixed point based columns. */ @@ -1796,7 +1801,7 @@ public class IndexData { { super(column, flags); } - + @Override protected void handleNegationAndOrder(boolean isNegative, byte[] valueBytes) @@ -1809,9 +1814,9 @@ public class IndexData { if(isNegative == isAscending()) { flipBytes(valueBytes); } - } + } } - + /** * ColumnDescriptor for byte based columns. */ @@ -1822,24 +1827,24 @@ public class IndexData { { super(column, flags); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { byte[] valueBytes = encodeNumberColumnValue(value, getColumn()); - + // bit twiddling rules: // - isAsc => nothing // - !isAsc => flipBytes if(!isAscending()) { flipBytes(valueBytes); } - + bout.write(valueBytes); - } + } } - + /** * ColumnDescriptor for boolean columns. */ @@ -1856,7 +1861,7 @@ public class IndexData { // null values are handled as booleans return false; } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException @@ -1867,11 +1872,11 @@ public class IndexData { (isAscending() ? ASC_BOOLEAN_FALSE : DESC_BOOLEAN_FALSE)); } } - + /** * ColumnDescriptor for "general legacy" sort order text based columns. */ - private static final class GenLegTextColumnDescriptor + private static final class GenLegTextColumnDescriptor extends ColumnDescriptor { private GenLegTextColumnDescriptor(ColumnImpl column, byte flags) @@ -1879,14 +1884,14 @@ public class IndexData { { super(column, flags); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { GeneralLegacyIndexCodes.GEN_LEG_INSTANCE.writeNonNullIndexTextValue( value, bout, isAscending()); - } + } } /** @@ -1899,14 +1904,14 @@ public class IndexData { { super(column, flags); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException { GeneralIndexCodes.GEN_INSTANCE.writeNonNullIndexTextValue( value, bout, isAscending()); - } + } } /** @@ -1919,7 +1924,7 @@ public class IndexData { { super(column, flags); } - + @Override protected void writeNonNullValue(Object value, ByteStream bout) throws IOException @@ -1929,7 +1934,7 @@ public class IndexData { bout); } } - + /** * ColumnDescriptor for BINARY columns. @@ -1950,8 +1955,8 @@ public class IndexData { ColumnImpl.toByteArray(value), isAscending(), bout); } } - - + + /** * ColumnDescriptor for columns which we cannot currently write. */ @@ -1971,7 +1976,7 @@ public class IndexData { "Cannot write indexes of this type due to " + _unsupportedReason); } } - + /** * A single leaf entry in an index (points to a single row) */ @@ -1983,7 +1988,7 @@ public class IndexData { private final byte[] _entryBytes; /** comparable type for the entry */ private final EntryType _type; - + /** * Create a new entry * @param entryBytes encoded bytes for this index entry @@ -1995,7 +2000,7 @@ public class IndexData { _entryBytes = entryBytes; _type = type; } - + /** * Create a new entry * @param entryBytes encoded bytes for this index entry @@ -2014,7 +2019,7 @@ public class IndexData { { this(buffer, entryLen, 0); } - + /** * Read an existing entry in from a buffer */ @@ -2031,11 +2036,11 @@ public class IndexData { // read the rowId int page = ByteUtil.get3ByteInt(buffer, ENTRY_BYTE_ORDER); int row = ByteUtil.getUnsignedByte(buffer); - + _rowId = new RowIdImpl(page, row); _type = EntryType.NORMAL; } - + public RowIdImpl getRowId() { return _rowId; } @@ -2047,11 +2052,11 @@ public class IndexData { public Integer getSubPageNumber() { throw new UnsupportedOperationException(); } - + public boolean isLeafEntry() { return true; } - + public boolean isValid() { return(_entryBytes != null); } @@ -2059,7 +2064,7 @@ public class IndexData { protected final byte[] getEntryBytes() { return _entryBytes; } - + /** * Size of this entry in the db. */ @@ -2067,7 +2072,7 @@ public class IndexData { // need 4 trailing bytes for the rowId return _entryBytes.length + 4; } - + /** * Write this entry into a buffer */ @@ -2076,15 +2081,15 @@ public class IndexData { throws IOException { if(prefix.length <= _entryBytes.length) { - + // write entry bytes, not including prefix buffer.put(_entryBytes, prefix.length, (_entryBytes.length - prefix.length)); ByteUtil.put3ByteInt(buffer, getRowId().getPageNumber(), ENTRY_BYTE_ORDER); - + } else if(prefix.length <= (_entryBytes.length + 3)) { - + // the prefix includes part of the page number, write to temp buffer // and copy last bytes to output buffer ByteBuffer tmp = ByteBuffer.allocate(3); @@ -2093,16 +2098,16 @@ public class IndexData { tmp.flip(); tmp.position(prefix.length - _entryBytes.length); buffer.put(tmp); - + } else { - + // since the row number would never be the same if the page number is // the same, nothing past the page number should ever be included in // the prefix. // FIXME, this could happen if page has only one row... throw new IllegalStateException("prefix should never be this long"); } - + buffer.put((byte)getRowId().getRowNumber()); } @@ -2113,7 +2118,7 @@ public class IndexData { } return sb; } - + @Override public String toString() { return entryBytesToStringBuilder( @@ -2141,7 +2146,8 @@ public class IndexData { public boolean equalsEntryBytes(Entry o) { return(BYTE_CODE_COMPARATOR.compare(_entryBytes, o._entryBytes) == 0); } - + + @Override public int compareTo(Entry other) { if (this == other) { return 0; @@ -2165,7 +2171,7 @@ public class IndexData { return typeCmp; } } - + // at this point we let the RowId decide the final result return _rowId.compareTo(other.getRowId()); } @@ -2177,7 +2183,7 @@ public class IndexData { protected Entry asNodeEntry(Integer subPageNumber) { return new NodeEntry(_entryBytes, _rowId, _type, subPageNumber); } - + } /** @@ -2200,7 +2206,7 @@ public class IndexData { super(entryBytes, rowId, type); _subPageNumber = subPageNumber; } - + /** * Read an existing node entry in from a buffer */ @@ -2222,19 +2228,19 @@ public class IndexData { public boolean isLeafEntry() { return false; } - + @Override protected int size() { // need 4 trailing bytes for the sub-page number return super.size() + 4; } - + @Override protected void write(ByteBuffer buffer, byte[] prefix) throws IOException { super.write(buffer, prefix); ByteUtil.putInt(buffer, _subPageNumber, ENTRY_BYTE_ORDER); } - + @Override public boolean equals(Object o) { return((this == o) || @@ -2250,7 +2256,7 @@ public class IndexData { .append("rowId", getRowId()) .append("subPage", _subPageNumber)) .toString(); - } + } } /** @@ -2298,14 +2304,14 @@ public class IndexData { private int getIndexModCount() { return IndexData.this._modCount; } - + /** * Returns the first entry (exclusive) as defined by this cursor. */ public Entry getFirstEntry() { return _firstPos.getEntry(); } - + /** * Returns the last entry (exclusive) as defined by this cursor. */ @@ -2320,7 +2326,7 @@ public class IndexData { public boolean isUpToDate() { return(getIndexModCount() == _lastModCount); } - + public void reset() { beforeFirst(); } @@ -2341,26 +2347,26 @@ public class IndexData { /** * Repositions the cursor so that the next row will be the first entry - * >= the given row. + * >= the given row. */ public void beforeEntry(Object[] row) throws IOException { - restorePosition(new Entry(IndexData.this.createEntryBytes(row), + restorePosition(new Entry(IndexData.this.createEntryBytes(row), RowIdImpl.FIRST_ROW_ID)); } - + /** * Repositions the cursor so that the previous row will be the first - * entry <= the given row. + * entry <= the given row. */ public void afterEntry(Object[] row) throws IOException { - restorePosition(new Entry(IndexData.this.createEntryBytes(row), + restorePosition(new Entry(IndexData.this.createEntryBytes(row), RowIdImpl.LAST_ROW_ID)); } - + /** * @return valid entry if there was a next entry, * {@code #getLastEntry} otherwise @@ -2386,7 +2392,7 @@ public class IndexData { { restorePosition(curEntry, _curPos.getEntry()); } - + /** * Restores a current and previous position for the cursor. */ @@ -2406,7 +2412,7 @@ public class IndexData { checkForModification(); } } - + /** * Gets another entry in the given direction, returning the new entry. */ @@ -2462,7 +2468,7 @@ public class IndexData { withErrorContext("Invalid entry given " + entry)); } } - + Position pos = findEntryPosition(entry); if(pos.compareTo(_lastPos) >= 0) { return _lastPos; @@ -2471,7 +2477,7 @@ public class IndexData { } return pos; } - + /** * Updates any the boundary info (_firstPos/_lastPos). */ @@ -2481,7 +2487,7 @@ public class IndexData { _firstPos = findEntryPosition(_firstPos.getEntry()); _lastPos = findEntryPosition(_lastPos.getEntry()); } - + @Override public String toString() { return CustomToStringStyle.valueBuilder(this) @@ -2489,7 +2495,7 @@ public class IndexData { .append("prevPosition", _prevPos) .toString(); } - + /** * Handles moving the cursor in a given direction. Separates cursor * logic from value storage. @@ -2500,7 +2506,7 @@ public class IndexData { public abstract Position getBeginningPosition(); public abstract Position getEndPosition(); } - + /** * Handles moving the cursor forward. */ @@ -2524,7 +2530,7 @@ public class IndexData { return _lastPos; } } - + /** * Handles moving the cursor backward. */ @@ -2569,7 +2575,7 @@ public class IndexData { { this(dataPage, idx, dataPage.getEntries().get(idx), false); } - + private Position(DataPage dataPage, int idx, Entry entry, boolean between) { _dataPage = dataPage; @@ -2581,7 +2587,7 @@ public class IndexData { public DataPage getDataPage() { return _dataPage; } - + public int getIndex() { return _idx; } @@ -2598,7 +2604,7 @@ public class IndexData { // non-between case return(_idx - 1); } - + public Entry getEntry() { return _entry; } @@ -2610,7 +2616,8 @@ public class IndexData { public boolean equalsEntry(Entry entry) { return _entry.equals(entry); } - + + @Override public int compareTo(Position other) { if(this == other) { @@ -2627,16 +2634,16 @@ public class IndexData { return idxCmp; } } - + // compare the entries. return _entry.compareTo(other._entry); } - + @Override public int hashCode() { return _entry.hashCode(); } - + @Override public boolean equals(Object o) { return((this == o) || @@ -2661,7 +2668,7 @@ public class IndexData { protected static abstract class DataPage { public abstract int getPageNumber(); - + public abstract boolean isLeaf(); public abstract void setLeaf(boolean isLeaf); @@ -2671,7 +2678,7 @@ public class IndexData { public abstract void setNextPageNumber(int pageNumber); public abstract int getChildTailPageNumber(); public abstract void setChildTailPageNumber(int pageNumber); - + public abstract int getTotalEntrySize(); public abstract void setTotalEntrySize(int totalSize); public abstract byte[] getEntryPrefix(); @@ -2688,7 +2695,7 @@ public class IndexData { public final boolean isEmpty() { return getEntries().isEmpty(); } - + public final int getCompressedEntrySize() { // when written to the index page, the entryPrefix bytes will only be // written for the first entry, so we subtract the entry prefix size @@ -2717,7 +2724,7 @@ public class IndexData { public final String toString() { List<Entry> entries = getEntries(); - String objName = + String objName = (isLeaf() ? "Leaf" : "Node") + "DataPage[" + getPageNumber() + "] " + getPrevPageNumber() + ", " + getNextPageNumber() + ", (" + getChildTailPageNumber() + ")"; @@ -2740,7 +2747,7 @@ public class IndexData { @Override public int getPageNumber() { return 0; } - + @Override public boolean isLeaf() { return true; } @Override @@ -2760,7 +2767,7 @@ public class IndexData { public int getChildTailPageNumber() { return 0; } @Override public void setChildTailPageNumber(int pageNumber) { } - + @Override public int getTotalEntrySize() { return 0; } @Override @@ -2772,7 +2779,7 @@ public class IndexData { public void setEntryPrefix(byte[] entryPrefix) { } @Override - public List<Entry> getEntries() { return Collections.emptyList(); } + public List<Entry> getEntries() { return Collections.emptyList(); } @Override public void setEntries(List<Entry> entries) { } @Override @@ -2800,7 +2807,7 @@ public class IndexData { public PendingChange getNext() { return _next; } - + /** * Completes the pending change. */ @@ -2827,7 +2834,7 @@ public class IndexData { super(next); } - public void setAddRow(Entry addEntry, DataPage dataPage, int idx, + public void setAddRow(Entry addEntry, DataPage dataPage, int idx, boolean isDupe) { _addEntry = addEntry; _addDataPage = dataPage; @@ -2843,7 +2850,7 @@ public class IndexData { public void commit() throws IOException { commitAddRow(_addEntry, _addDataPage, _addIdx, _isDupe, _oldEntry); } - + @Override public void rollback() throws IOException { _addEntry = null; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java index 0fbd231..60d80d5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java @@ -25,7 +25,7 @@ import java.util.Map; import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -112,6 +112,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl> return _data; } + @Override public TableImpl getTable() { return getIndexData().getTable(); } @@ -140,6 +141,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl> return getIndexData().getUniqueEntryCountOffset(); } + @Override public String getName() { return _name; } @@ -148,10 +150,12 @@ public class IndexImpl implements Index, Comparable<IndexImpl> _name = name; } + @Override public boolean isPrimaryKey() { return _indexType == PRIMARY_KEY_INDEX_TYPE; } + @Override public boolean isForeignKey() { return _indexType == FOREIGN_KEY_INDEX_TYPE; } @@ -160,6 +164,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl> return _reference; } + @Override public IndexImpl getReferencedIndex() throws IOException { if(_reference == null) { @@ -204,26 +209,32 @@ public class IndexImpl implements Index, Comparable<IndexImpl> return refIndex; } + @Override public boolean shouldIgnoreNulls() { return getIndexData().shouldIgnoreNulls(); } + @Override public boolean isUnique() { return getIndexData().isUnique(); } + @Override public boolean isRequired() { return getIndexData().isRequired(); } + @Override public List<IndexData.ColumnDescriptor> getColumns() { return getIndexData().getColumns(); } + @Override public int getColumnCount() { return getIndexData().getColumnCount(); } + @Override public CursorBuilder newCursor() { return getTable().newCursor().setIndex(this); } @@ -367,6 +378,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl> return sb.toString(); } + @Override public int compareTo(IndexImpl other) { if (_indexNumber > other.getIndexNumber()) { return 1; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java index 4ee1882..d594c1c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java @@ -29,7 +29,7 @@ import java.util.Map; import java.util.RandomAccess; import static com.healthmarketscience.jackcess.impl.IndexData.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Manager of the index pages for a IndexData. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java b/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java index 3d4dab9..1755949 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/InternalColumnValidator.java @@ -53,6 +53,7 @@ abstract class InternalColumnValidator implements ColumnValidator intValidator._delegate = extValidator; } + @Override public final Object validate(Column col, Object val) throws IOException { val = _delegate.validate(col, val); return internalValidate(col, val); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java index d8a2336..0d61923 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java @@ -35,7 +35,7 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.util.OleBlob; import static com.healthmarketscience.jackcess.util.OleBlob.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Utility code for working with OLE data. @@ -471,10 +471,12 @@ public class OleUtil _bytes = bytes; } + @Override public void writeTo(OutputStream out) throws IOException { out.write(_bytes); } + @Override public Content getContent() throws IOException { if(_content == null) { _content = parseContent(this); @@ -482,16 +484,19 @@ public class OleUtil return _content; } + @Override public InputStream getBinaryStream() throws SQLException { return new ByteArrayInputStream(_bytes); } + @Override public InputStream getBinaryStream(long pos, long len) throws SQLException { return new ByteArrayInputStream(_bytes, fromJdbcOffset(pos), (int)len); } + @Override public long length() throws SQLException { return _bytes.length; } @@ -503,41 +508,50 @@ public class OleUtil return _bytes; } + @Override public byte[] getBytes(long pos, int len) throws SQLException { return ByteUtil.copyOf(_bytes, fromJdbcOffset(pos), len); } + @Override public long position(byte[] pattern, long start) throws SQLException { int pos = ByteUtil.findRange(PageChannel.wrap(_bytes), fromJdbcOffset(start), pattern); return((pos >= 0) ? toJdbcOffset(pos) : pos); } + @Override public long position(Blob pattern, long start) throws SQLException { return position(pattern.getBytes(1L, (int)pattern.length()), start); } + @Override public OutputStream setBinaryStream(long position) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override public void truncate(long len) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override public int setBytes(long pos, byte[] bytes) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override public int setBytes(long pos, byte[] bytes, int offset, int lesn) throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override public void free() { close(); } + @Override public void close() { _bytes = null; ByteUtil.closeQuietly(_content); @@ -573,6 +587,7 @@ public class OleUtil _blob = blob; } + @Override public OleBlobImpl getBlob() { return _blob; } @@ -581,6 +596,7 @@ public class OleUtil return getBlob().getBytes(); } + @Override public void close() { // base does nothing } @@ -604,14 +620,17 @@ public class OleUtil _length = length; } + @Override public long length() { return _length; } + @Override public InputStream getStream() throws IOException { return new ByteArrayInputStream(getBytes(), _position, _length); } + @Override public void writeTo(OutputStream out) throws IOException { out.write(getBytes(), _position, _length); } @@ -644,14 +663,17 @@ public class OleUtil _typeName = typeName; } + @Override public String getPrettyName() { return _prettyName; } + @Override public String getClassName() { return _className; } + @Override public String getTypeName() { return _typeName; } @@ -685,22 +707,27 @@ public class OleUtil _filePath = filePath; } + @Override public ContentType getType() { return ContentType.LINK; } + @Override public String getFileName() { return _fileName; } + @Override public String getLinkPath() { return _linkPath; } + @Override public String getFilePath() { return _filePath; } + @Override public InputStream getLinkStream() throws IOException { return new FileInputStream(getLinkPath()); } @@ -735,18 +762,22 @@ public class OleUtil _localFilePath = localFilePath; } + @Override public ContentType getType() { return ContentType.SIMPLE_PACKAGE; } + @Override public String getFileName() { return _fileName; } + @Override public String getFilePath() { return _filePath; } + @Override public String getLocalFilePath() { return _localFilePath; } @@ -772,6 +803,7 @@ public class OleUtil super(blob, prettyName, className, typeName, position, length); } + @Override public ContentType getType() { return ContentType.OTHER; } @@ -789,6 +821,7 @@ public class OleUtil super(blob); } + @Override public ContentType getType() { return ContentType.UNKNOWN; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java b/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java index 19d15fd..41c164c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java @@ -389,10 +389,12 @@ public class PageChannel implements Channel, Flushable { return ByteBuffer.allocate(size).order(order); } + @Override public void flush() throws IOException { _channel.force(true); } + @Override public void close() throws IOException { flush(); if(_closeChannel) { @@ -400,6 +402,7 @@ public class PageChannel implements Channel, Flushable { } } + @Override public boolean isOpen() { return _channel.isOpen(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java index be3a249..a092e82 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java @@ -17,6 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -73,6 +74,7 @@ public class PropertyMapImpl implements PropertyMap _owner = owner; } + @Override public String getName() { return _mapName; } @@ -85,22 +87,27 @@ public class PropertyMapImpl implements PropertyMap return _owner; } + @Override public int getSize() { return _props.size(); } + @Override public boolean isEmpty() { return _props.isEmpty(); } + @Override public Property get(String name) { return _props.get(DatabaseImpl.toLookupName(name)); } + @Override public Object getValue(String name) { return getValue(name, null); } + @Override public Object getValue(String name, Object defaultValue) { Property prop = get(name); Object value = defaultValue; @@ -110,14 +117,17 @@ public class PropertyMapImpl implements PropertyMap return value; } + @Override public PropertyImpl put(String name, Object value) { return put(name, null, value, false); } + @Override public PropertyImpl put(String name, DataType type, Object value) { return put(name, type, value, false); } + @Override public void putAll(Iterable<? extends Property> props) { if(props == null) { return; @@ -135,6 +145,7 @@ public class PropertyMapImpl implements PropertyMap /** * Puts a property into this map with the given information. */ + @Override public PropertyImpl put(String name, DataType type, Object value, boolean isDdl) { PropertyImpl prop = (PropertyImpl)createProperty(name, type, value, isDdl); @@ -142,14 +153,17 @@ public class PropertyMapImpl implements PropertyMap return prop; } + @Override public PropertyImpl remove(String name) { return (PropertyImpl)_props.remove(DatabaseImpl.toLookupName(name)); } + @Override public Iterator<Property> iterator() { return _props.values().iterator(); } + @Override public void save() throws IOException { getOwner().save(); } @@ -208,7 +222,7 @@ public class PropertyMapImpl implements PropertyMap type = DataType.FLOAT; } else if(value instanceof Double) { type = DataType.DOUBLE; - } else if(value instanceof Date) { + } else if((value instanceof Date) || (value instanceof LocalDateTime)) { type = DataType.SHORT_DATE_TIME; } else if(value instanceof byte[]) { type = DataType.OLE; @@ -242,22 +256,27 @@ public class PropertyMapImpl implements PropertyMap _value = value; } + @Override public String getName() { return _name; } + @Override public DataType getType() { return _type; } + @Override public Object getValue() { return _value; } + @Override public void setValue(Object newValue) { _value = newValue; } + @Override public boolean isDdl() { return _ddl; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java index 61e1e07..e545574 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMaps.java @@ -103,6 +103,7 @@ public class PropertyMaps implements Iterable<PropertyMapImpl> return map; } + @Override public Iterator<PropertyMapImpl> iterator() { return _maps.values().iterator(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java index 0cc2b90..a9342f8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java @@ -83,22 +83,27 @@ public class RelationshipImpl implements Relationship _flags = flags; } + @Override public String getName() { return _name; } + @Override public Table getFromTable() { return _fromTable; } + @Override public List<Column> getFromColumns() { return _fromColumns; } + @Override public Table getToTable() { return _toTable; } + @Override public List<Column> getToColumns() { return _toColumns; } @@ -107,34 +112,42 @@ public class RelationshipImpl implements Relationship return _flags; } + @Override public boolean isOneToOne() { return hasFlag(ONE_TO_ONE_FLAG); } + @Override public boolean hasReferentialIntegrity() { return !hasFlag(NO_REFERENTIAL_INTEGRITY_FLAG); } + @Override public boolean cascadeUpdates() { return hasFlag(CASCADE_UPDATES_FLAG); } + @Override public boolean cascadeDeletes() { return hasFlag(CASCADE_DELETES_FLAG); } + @Override public boolean cascadeNullOnDelete() { return hasFlag(CASCADE_NULL_FLAG); } + @Override public boolean isLeftOuterJoin() { return hasFlag(LEFT_OUTER_JOIN_FLAG); } + @Override public boolean isRightOuterJoin() { return hasFlag(RIGHT_OUTER_JOIN_FLAG); } + @Override public JoinType getJoinType() { if(isLeftOuterJoin()) { return JoinType.LEFT_OUTER; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java index 7bfab4a..097823a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java @@ -19,7 +19,7 @@ package com.healthmarketscience.jackcess.impl; import java.io.Serializable; import com.healthmarketscience.jackcess.RowId; -import org.apache.commons.lang.builder.CompareToBuilder; +import org.apache.commons.lang3.builder.CompareToBuilder; /** @@ -98,6 +98,7 @@ public class RowIdImpl implements RowId, Serializable return _type; } + @Override public int compareTo(RowId other) { return compareTo((RowIdImpl)other); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java index ee088f4..e89b070 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.LinkedHashMap; import java.util.Date; import java.math.BigDecimal; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; @@ -27,8 +28,8 @@ import com.healthmarketscience.jackcess.util.OleBlob; /** - * A row of data as column->value pairs. - * </p> + * A row of data as column->value pairs. + * <p> * Note that the {@link #equals} and {@link #hashCode} methods work on the row * contents <i>only</i> (i.e. they ignore the id). * @@ -36,7 +37,7 @@ import com.healthmarketscience.jackcess.util.OleBlob; */ public class RowImpl extends LinkedHashMap<String,Object> implements Row { - private static final long serialVersionUID = 20130314L; + private static final long serialVersionUID = 20130314L; private final RowIdImpl _id; @@ -54,54 +55,73 @@ public class RowImpl extends LinkedHashMap<String,Object> implements Row _id = (RowIdImpl)row.getId(); } + @Override public RowIdImpl getId() { return _id; } + @Override public String getString(String name) { return (String)get(name); } + @Override public Boolean getBoolean(String name) { return (Boolean)get(name); } + @Override public Byte getByte(String name) { return (Byte)get(name); } + @Override public Short getShort(String name) { return (Short)get(name); } + @Override public Integer getInt(String name) { return (Integer)get(name); } + @Override public BigDecimal getBigDecimal(String name) { return (BigDecimal)get(name); } + @Override public Float getFloat(String name) { return (Float)get(name); } + @Override public Double getDouble(String name) { return (Double)get(name); } + @Override + @SuppressWarnings("deprecation") public Date getDate(String name) { return (Date)get(name); } + @Override + public LocalDateTime getLocalDateTime(String name) { + return (LocalDateTime)get(name); + } + + @Override public byte[] getBytes(String name) { return (byte[])get(name); } + @Override public ComplexValueForeignKey getForeignKey(String name) { return (ComplexValueForeignKey)get(name); } + @Override public OleBlob getBlob(String name) throws IOException { byte[] bytes = getBytes(name); return ((bytes != null) ? OleBlob.Builder.fromInternalData(bytes) : null); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 15a0c8c..eafa376 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -52,7 +52,7 @@ 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.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -97,6 +97,7 @@ public class TableImpl implements Table, PropertyMaps.Owner the variable length offset table */ private static final Comparator<ColumnImpl> VAR_LEN_COLUMN_COMPARATOR = new Comparator<ColumnImpl>() { + @Override public int compare(ColumnImpl c1, ColumnImpl c2) { return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1 : ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : @@ -107,6 +108,7 @@ public class TableImpl implements Table, PropertyMaps.Owner /** comparator which sorts columns based on their display index */ private static final Comparator<ColumnImpl> DISPLAY_ORDER_COMPARATOR = new Comparator<ColumnImpl>() { + @Override public int compare(ColumnImpl c1, ColumnImpl c2) { return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1 : ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : @@ -320,14 +322,17 @@ public class TableImpl implements Table, PropertyMaps.Owner } } + @Override public String getName() { return _name; } + @Override public boolean isHidden() { return((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0); } + @Override public boolean isSystem() { return(_tableType != TYPE_USER); } @@ -339,10 +344,12 @@ public class TableImpl implements Table, PropertyMaps.Owner return _maxColumnCount; } + @Override public int getColumnCount() { return _columns.size(); } + @Override public DatabaseImpl getDatabase() { return _database; } @@ -361,11 +368,13 @@ public class TableImpl implements Table, PropertyMaps.Owner return getDatabase().getPageChannel(); } + @Override public ErrorHandler getErrorHandler() { return((_tableErrorHandler != null) ? _tableErrorHandler : getDatabase().getErrorHandler()); } + @Override public void setErrorHandler(ErrorHandler newErrorHandler) { _tableErrorHandler = newErrorHandler; } @@ -374,11 +383,13 @@ public class TableImpl implements Table, PropertyMaps.Owner return _tableDefPageNumber; } + @Override public boolean isAllowAutoNumberInsert() { return ((_allowAutoNumInsert != null) ? (boolean)_allowAutoNumInsert : getDatabase().isAllowAutoNumberInsert()); } + @Override public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) { _allowAutoNumInsert = allowAutoNumInsert; } @@ -432,10 +443,12 @@ public class TableImpl implements Table, PropertyMaps.Owner return _longValueBufferH; } + @Override public List<ColumnImpl> getColumns() { return Collections.unmodifiableList(_columns); } + @Override public ColumnImpl getColumn(String name) { for(ColumnImpl column : _columns) { if(column.getName().equalsIgnoreCase(name)) { @@ -455,6 +468,7 @@ public class TableImpl implements Table, PropertyMaps.Owner return false; } + @Override public PropertyMap getProperties() throws IOException { if(_props == null) { _props = getPropertyMaps().getDefault(); @@ -474,6 +488,7 @@ public class TableImpl implements Table, PropertyMaps.Owner return _propertyMaps; } + @Override public void propertiesUpdated() throws IOException { // propagate update to columns for(ColumnImpl col : _columns) { @@ -487,10 +502,12 @@ public class TableImpl implements Table, PropertyMaps.Owner _calcColEval.reSort(); } + @Override public List<IndexImpl> getIndexes() { return Collections.unmodifiableList(_indexes); } + @Override public IndexImpl getIndex(String name) { for(IndexImpl index : _indexes) { if(index.getName().equalsIgnoreCase(name)) { @@ -501,6 +518,7 @@ public class TableImpl implements Table, PropertyMaps.Owner "Index with name " + name + " does not exist on this table")); } + @Override public IndexImpl getPrimaryKeyIndex() { for(IndexImpl index : _indexes) { if(index.isPrimaryKey()) { @@ -511,6 +529,7 @@ public class TableImpl implements Table, PropertyMaps.Owner "No primary key index found")); } + @Override public IndexImpl getForeignKeyIndex(Table otherTable) { for(IndexImpl index : _indexes) { if(index.isForeignKey() && (index.getReference() != null) && @@ -592,6 +611,7 @@ public class TableImpl implements Table, PropertyMaps.Owner return _autoNumColumns; } + @Override public CursorImpl getDefaultCursor() { if(_defaultCursor == null) { _defaultCursor = CursorImpl.createCursor(this); @@ -599,14 +619,17 @@ public class TableImpl implements Table, PropertyMaps.Owner return _defaultCursor; } + @Override public CursorBuilder newCursor() { return new CursorBuilder(this); } + @Override public void reset() { getDefaultCursor().reset(); } + @Override public Row deleteRow(Row row) throws IOException { deleteRow(row.getId()); return row; @@ -690,6 +713,7 @@ public class TableImpl implements Table, PropertyMaps.Owner } } + @Override public Row getNextRow() throws IOException { return getDefaultCursor().getNextRow(); } @@ -1034,6 +1058,7 @@ public class TableImpl implements Table, PropertyMaps.Owner } } + @Override public Iterator<Row> iterator() { return getDefaultCursor().iterator(); } @@ -2081,12 +2106,13 @@ public class TableImpl implements Table, PropertyMaps.Owner return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH); } + @Override public Object[] asRow(Map<String,?> rowMap) { return asRow(rowMap, null, false); } /** - * Converts a map of columnName -> columnValue to an array of row values + * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #addRow(Object...)}, where the generated * RowId will be an extra value at the end of the array. * @see ColumnImpl#RETURN_ROW_ID @@ -2096,6 +2122,7 @@ public class TableImpl implements Table, PropertyMaps.Owner return asRow(rowMap, null, true); } + @Override public Object[] asUpdateRow(Map<String,?> rowMap) { return asRow(rowMap, Column.KEEP_VALUE, false); } @@ -2110,7 +2137,7 @@ public class TableImpl implements Table, PropertyMaps.Owner } /** - * Converts a map of columnName -> columnValue to an array of row values. + * Converts a map of columnName -> columnValue to an array of row values. */ private Object[] asRow(Map<String,?> rowMap, Object defaultValue, boolean returnRowId) @@ -2137,10 +2164,12 @@ public class TableImpl implements Table, PropertyMaps.Owner return row; } + @Override public Object[] addRow(Object... row) throws IOException { return addRows(Collections.singletonList(row), false).get(0); } + @Override public <M extends Map<String,Object>> M addRowFromMap(M row) throws IOException { @@ -2152,12 +2181,14 @@ public class TableImpl implements Table, PropertyMaps.Owner return row; } + @Override public List<? extends Object[]> addRows(List<? extends Object[]> rows) throws IOException { return addRows(rows, true); } + @Override public <M extends Map<String,Object>> List<M> addRowsFromMaps(List<M> rows) throws IOException { @@ -2387,6 +2418,7 @@ public class TableImpl implements Table, PropertyMaps.Owner return false; } + @Override public Row updateRow(Row row) throws IOException { return updateRowFromMap( getDefaultCursor().getRowState(), (RowIdImpl)row.getId(), row); @@ -2998,6 +3030,7 @@ public class TableImpl implements Table, PropertyMaps.Owner } } + @Override public int getRowCount() { return _rowCount; } @@ -3360,6 +3393,7 @@ public class TableImpl implements Table, PropertyMaps.Owner _lastModCount = TableImpl.this._modCount; } + @Override public TableImpl getTable() { return TableImpl.this; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java index 6642a69..3aff524 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.Date; import java.util.HashSet; @@ -166,7 +167,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> String name = (String)getFileNameColumn().getRowValue(rawValue); String type = (String)getFileTypeColumn().getRowValue(rawValue); Integer flags = (Integer)getFileFlagsColumn().getRowValue(rawValue); - Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue); + Object ts = getFileTimeStampColumn().getRowValue(rawValue); byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue); return new AttachmentImpl(id, complexValueFk, url, name, type, null, @@ -182,7 +183,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> getFileNameColumn().setRowValue(row, attachment.getFileName()); getFileTypeColumn().setRowValue(row, attachment.getFileType()); getFileFlagsColumn().setRowValue(row, attachment.getFileFlags()); - getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStamp()); + getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStampObject()); getFileDataColumn().setRowValue(row, attachment.getEncodedFileData()); return row; } @@ -198,7 +199,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> public static Attachment newAttachment( String url, String name, String type, byte[] data, - Date timeStamp, Integer flags) + Object timeStamp, Integer flags) { return newAttachment(INVALID_FK, url, name, type, data, timeStamp, flags); @@ -206,7 +207,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> public static Attachment newAttachment( ComplexValueForeignKey complexValueFk, String url, String name, - String type, byte[] data, Date timeStamp, Integer flags) + String type, byte[] data, Object timeStamp, Integer flags) { return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, data, timeStamp, flags, null); @@ -224,7 +225,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> public static Attachment newEncodedAttachment( String url, String name, String type, byte[] encodedData, - Date timeStamp, Integer flags) + Object timeStamp, Integer flags) { return newEncodedAttachment(INVALID_FK, url, name, type, encodedData, timeStamp, flags); @@ -232,13 +233,14 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> public static Attachment newEncodedAttachment( ComplexValueForeignKey complexValueFk, String url, String name, - String type, byte[] encodedData, Date timeStamp, Integer flags) + String type, byte[] encodedData, Object timeStamp, Integer flags) { return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, null, timeStamp, flags, encodedData); } + @SuppressWarnings("deprecation") private static class AttachmentImpl extends ComplexValueImpl implements Attachment { @@ -246,13 +248,13 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> private String _name; private String _type; private byte[] _data; - private Date _timeStamp; + private Object _timeStamp; private Integer _flags; private byte[] _encodedData; private AttachmentImpl(Id id, ComplexValueForeignKey complexValueFk, String url, String name, String type, byte[] data, - Date timeStamp, Integer flags, byte[] encodedData) + Object timeStamp, Integer flags, byte[] encodedData) { super(id, complexValueFk); _url = url; @@ -264,6 +266,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> _encodedData = encodedData; } + @Override public byte[] getFileData() throws IOException { if((_data == null) && (_encodedData != null)) { _data = decodeData(); @@ -271,11 +274,13 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> return _data; } + @Override public void setFileData(byte[] data) { _data = data; _encodedData = null; } + @Override public byte[] getEncodedFileData() throws IOException { if((_encodedData == null) && (_data != null)) { _encodedData = encodeData(); @@ -283,55 +288,83 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> return _encodedData; } + @Override public void setEncodedFileData(byte[] data) { _encodedData = data; _data = null; } + @Override public String getFileName() { return _name; } + @Override public void setFileName(String fileName) { _name = fileName; } + @Override public String getFileUrl() { return _url; } + @Override public void setFileUrl(String fileUrl) { _url = fileUrl; } + @Override public String getFileType() { return _type; } + @Override public void setFileType(String fileType) { _type = fileType; } + @Override public Date getFileTimeStamp() { - return _timeStamp; + return (Date)_timeStamp; } + @Override public void setFileTimeStamp(Date fileTimeStamp) { _timeStamp = fileTimeStamp; } + @Override + public LocalDateTime getFileLocalTimeStamp() { + return (LocalDateTime)_timeStamp; + } + + @Override + public void setFileLocalTimeStamp(LocalDateTime fileTimeStamp) { + _timeStamp = fileTimeStamp; + } + + @Override + public Object getFileTimeStampObject() { + return _timeStamp; + } + + @Override public Integer getFileFlags() { return _flags; } + @Override public void setFileFlags(Integer fileFlags) { _flags = fileFlags; } + @Override public void update() throws IOException { getComplexValueForeignKey().updateAttachment(this); } + @Override public void delete() throws IOException { getComplexValueForeignKey().deleteAttachment(this); } @@ -348,7 +381,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl<Attachment> return "Attachment(" + getComplexValueForeignKey() + "," + getId() + ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType() - + ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " + + + ", " + getFileTimeStampObject() + ", " + getFileFlags() + ", " + dataStr; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java index 566af72..31c0e04 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexColumnInfoImpl.java @@ -122,12 +122,14 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> return _typeCols; } + @Override public int countValues(int complexValueFk) throws IOException { return getRawValues(complexValueFk, Collections.singleton(_complexValFkCol.getName())) .size(); } + @Override public List<Row> getRawValues(int complexValueFk) throws IOException { @@ -148,6 +150,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> .setColumnNames(columnNames).iterator(); } + @Override public List<Row> getRawValues(int complexValueFk, Collection<String> columnNames) throws IOException @@ -166,6 +169,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> return values; } + @Override public List<V> getValues(ComplexValueForeignKey complexValueFk) throws IOException { @@ -189,6 +193,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> return values; } + @Override public ComplexValue.Id addRawValue(Map<String,?> rawValue) throws IOException { @@ -197,6 +202,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> return getValueId(row); } + @Override public ComplexValue.Id addValue(V value) throws IOException { Object[] row = asRow(newRowArray(), value); _flatTable.addRow(row); @@ -205,43 +211,51 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> return id; } + @Override public void addValues(Collection<? extends V> values) throws IOException { for(V value : values) { addValue(value); } } + @Override public ComplexValue.Id updateRawValue(Row rawValue) throws IOException { _flatTable.updateRow(rawValue); return getValueId(rawValue); } + @Override public ComplexValue.Id updateValue(V value) throws IOException { ComplexValue.Id id = value.getId(); updateRow(id, asRow(newRowArray(), value)); return id; } + @Override public void updateValues(Collection<? extends V> values) throws IOException { for(V value : values) { updateValue(value); } } + @Override public void deleteRawValue(Row rawValue) throws IOException { deleteRow(rawValue.getId()); } + @Override public void deleteValue(V value) throws IOException { deleteRow(value.getId().getRowId()); } + @Override public void deleteValues(Collection<? extends V> values) throws IOException { for(V value : values) { deleteValue(value); } } + @Override public void deleteAllValues(int complexValueFk) throws IOException { Iterator<Row> entryIter = getComplexValFkIter(complexValueFk, Collections.<String>emptySet()); @@ -255,6 +269,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> } } + @Override public void deleteAllValues(ComplexValueForeignKey complexValueFk) throws IOException { @@ -322,6 +337,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> } } + @Override public abstract ComplexDataType getType(); protected abstract V toValue( @@ -339,10 +355,12 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> _complexValueFk = complexValueFk; } + @Override public Id getId() { return _id; } + @Override public void setId(Id id) { if(_id == id) { // harmless, ignore @@ -354,10 +372,12 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> _id = id; } + @Override public ComplexValueForeignKey getComplexValueForeignKey() { return _complexValueFk; } + @Override public void setComplexValueForeignKey(ComplexValueForeignKey complexValueFk) { if(_complexValueFk == complexValueFk) { @@ -370,6 +390,7 @@ public abstract class ComplexColumnInfoImpl<V extends ComplexValue> _complexValueFk = complexValueFk; } + @Override public Column getColumn() { return _complexValueFk.getColumn(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java index a73d3ed..06c0cd7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java @@ -17,11 +17,14 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.complex; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; import java.util.Map; import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.complex.Attachment; import com.healthmarketscience.jackcess.complex.AttachmentColumnInfo; @@ -50,14 +53,15 @@ import com.healthmarketscience.jackcess.complex.VersionHistoryColumnInfo; * * @author James Ahlborn */ +@SuppressWarnings("deprecation") public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey { - private static final long serialVersionUID = 20110805L; - + private static final long serialVersionUID = 20110805L; + private transient final Column _column; private final int _value; private transient List<? extends ComplexValue> _values; - + public ComplexValueForeignKeyImpl(Column column, int value) { _column = column; _value = value; @@ -72,12 +76,12 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey public Column getColumn() { return _column; } - + @Override public ComplexDataType getComplexType() { return getComplexInfo().getType(); } - + protected ComplexColumnInfo<? extends ComplexValue> getComplexInfo() { return _column.getComplexInfo(); } @@ -85,7 +89,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey protected VersionHistoryColumnInfo getVersionInfo() { return (VersionHistoryColumnInfo)getComplexInfo(); } - + protected AttachmentColumnInfo getAttachmentInfo() { return (AttachmentColumnInfo)getComplexInfo(); } @@ -93,27 +97,27 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey protected MultiValueColumnInfo getMultiValueInfo() { return (MultiValueColumnInfo)getComplexInfo(); } - + protected UnsupportedColumnInfo getUnsupportedInfo() { return (UnsupportedColumnInfo)getComplexInfo(); } - + @Override public int countValues() throws IOException { return getComplexInfo().countValues(get()); } - + public List<Row> getRawValues() throws IOException { return getComplexInfo().getRawValues(get()); - } - + } + @Override public List<? extends ComplexValue> getValues() throws IOException { if(_values == null) { _values = getComplexInfo().getValues(this); } return _values; - } + } @Override @SuppressWarnings("unchecked") @@ -123,7 +127,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List<Version>)getValues(); } - + @Override @SuppressWarnings("unchecked") public List<Attachment> getAttachments() throws IOException { @@ -132,7 +136,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List<Attachment>)getValues(); } - + @Override @SuppressWarnings("unchecked") public List<SingleValue> getMultiValues() throws IOException { @@ -141,7 +145,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List<SingleValue>)getValues(); } - + @Override @SuppressWarnings("unchecked") public List<UnsupportedValue> getUnsupportedValues() throws IOException { @@ -150,20 +154,29 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List<UnsupportedValue>)getValues(); } - + @Override public void reset() { // discard any cached values _values = null; } - + @Override public Version addVersion(String value) throws IOException { - return addVersion(value, new Date()); + return addVersionImpl(value, now()); } - + @Override public Version addVersion(String value, Date modifiedDate) throws IOException { + return addVersionImpl(value, modifiedDate); + } + + @Override + public Version addVersion(String value, LocalDateTime modifiedDate) throws IOException { + return addVersionImpl(value, modifiedDate); + } + + private Version addVersionImpl(String value, Object modifiedDate) throws IOException { reset(); Version v = VersionHistoryColumnInfoImpl.newVersion(this, value, modifiedDate); getVersionInfo().addValue(v); @@ -172,15 +185,32 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey @Override public Attachment addAttachment(byte[] data) throws IOException { - return addAttachment(null, null, null, data, null, null); + return addAttachmentImpl(null, null, null, data, null, null); } - + @Override public Attachment addAttachment( String url, String name, String type, byte[] data, Date timeStamp, Integer flags) throws IOException { + return addAttachmentImpl(url, name, type, data, timeStamp, flags); + } + + @Override + public Attachment addAttachment( + String url, String name, String type, byte[] data, + LocalDateTime timeStamp, Integer flags) + throws IOException + { + return addAttachmentImpl(url, name, type, data, timeStamp, flags); + } + + private Attachment addAttachmentImpl( + String url, String name, String type, byte[] data, + Object timeStamp, Integer flags) + throws IOException + { reset(); Attachment a = AttachmentColumnInfoImpl.newAttachment( this, url, name, type, data, timeStamp, flags); @@ -192,36 +222,55 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey public Attachment addEncodedAttachment(byte[] encodedData) throws IOException { - return addEncodedAttachment(null, null, null, encodedData, null, null); + return addEncodedAttachmentImpl(null, null, null, encodedData, null, null); } - + @Override public Attachment addEncodedAttachment( String url, String name, String type, byte[] encodedData, Date timeStamp, Integer flags) throws IOException { + return addEncodedAttachmentImpl(url, name, type, encodedData, timeStamp, + flags); + } + + @Override + public Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + LocalDateTime timeStamp, Integer flags) + throws IOException + { + return addEncodedAttachmentImpl(url, name, type, encodedData, timeStamp, + flags); + } + + private Attachment addEncodedAttachmentImpl( + String url, String name, String type, byte[] encodedData, + Object timeStamp, Integer flags) + throws IOException + { reset(); Attachment a = AttachmentColumnInfoImpl.newEncodedAttachment( this, url, name, type, encodedData, timeStamp, flags); getAttachmentInfo().addValue(a); return a; } - + @Override public Attachment updateAttachment(Attachment attachment) throws IOException { reset(); getAttachmentInfo().updateValue(attachment); return attachment; } - + @Override public Attachment deleteAttachment(Attachment attachment) throws IOException { reset(); getAttachmentInfo().deleteValue(attachment); return attachment; } - + @Override public SingleValue addMultiValue(Object value) throws IOException { reset(); @@ -229,21 +278,21 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getMultiValueInfo().addValue(v); return v; } - + @Override public SingleValue updateMultiValue(SingleValue value) throws IOException { reset(); getMultiValueInfo().updateValue(value); return value; } - + @Override public SingleValue deleteMultiValue(SingleValue value) throws IOException { reset(); getMultiValueInfo().deleteValue(value); return value; } - + @Override public UnsupportedValue addUnsupportedValue(Map<String,?> values) throws IOException @@ -253,7 +302,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getUnsupportedInfo().addValue(v); return v; } - + @Override public UnsupportedValue updateUnsupportedValue(UnsupportedValue value) throws IOException @@ -262,7 +311,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getUnsupportedInfo().updateValue(value); return value; } - + @Override public UnsupportedValue deleteUnsupportedValue(UnsupportedValue value) throws IOException @@ -271,16 +320,24 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getUnsupportedInfo().deleteValue(value); return value; } - + @Override public void deleteAllValues() throws IOException { reset(); getComplexInfo().deleteAllValues(this); } - + @Override public boolean equals(Object o) { return(super.equals(o) && (_column == ((ComplexValueForeignKeyImpl)o)._column)); } + + private Object now() { + Database db = getColumn().getDatabase(); + if(db.getDateTimeType() == DateTimeType.DATE) { + return new Date(); + } + return LocalDateTime.now(db.getZoneId()); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java index 809cf88..65d77d8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnInfoImpl.java @@ -96,18 +96,22 @@ public class MultiValueColumnInfoImpl extends ComplexColumnInfoImpl<SingleValue> _value = value; } + @Override public Object get() { return _value; } + @Override public void set(Object value) { _value = value; } + @Override public void update() throws IOException { getComplexValueForeignKey().updateMultiValue(this); } + @Override public void delete() throws IOException { getComplexValueForeignKey().deleteMultiValue(this); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnPropertyMap.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnPropertyMap.java index 7adc2e3..49aed52 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnPropertyMap.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/MultiValueColumnPropertyMap.java @@ -49,18 +49,22 @@ public class MultiValueColumnPropertyMap implements PropertyMap _complex = complex; } + @Override public String getName() { return _primary.getName(); } + @Override public int getSize() { return _primary.getSize() + _complex.getSize(); } + @Override public boolean isEmpty() { return _primary.isEmpty() && _complex.isEmpty(); } + @Override public Property get(String name) { Property prop = _primary.get(name); if(prop != null) { @@ -69,23 +73,28 @@ public class MultiValueColumnPropertyMap implements PropertyMap return _complex.get(name); } + @Override public Object getValue(String name) { return getValue(name, null); } + @Override public Object getValue(String name, Object defaultValue) { Property prop = get(name); return ((prop != null) ? prop.getValue() : defaultValue); } + @Override public Property put(String name, Object value) { return put(name, null, value, false); } + @Override public Property put(String name, DataType type, Object value) { return put(name, type, value, false); } + @Override public Property put(String name, DataType type, Object value, boolean isDdl) { // the only property which seems to go in the "primary" is the "multi // value" property @@ -95,6 +104,7 @@ public class MultiValueColumnPropertyMap implements PropertyMap return _complex.put(name, type, value, isDdl); } + @Override public void putAll(Iterable<? extends Property> props) { if(props == null) { return; @@ -109,6 +119,7 @@ public class MultiValueColumnPropertyMap implements PropertyMap } } + @Override public Property remove(String name) { if(isPrimaryKey(name)) { return _primary.remove(name); @@ -116,11 +127,13 @@ public class MultiValueColumnPropertyMap implements PropertyMap return _complex.remove(name); } + @Override public void save() throws IOException { _primary.save(); _complex.save(); } + @Override public Iterator<Property> iterator() { final List<Iterator<Property>> iters = new ArrayList<Iterator<Property>>(2); iters.add(_primary.iterator()); @@ -142,10 +155,12 @@ public class MultiValueColumnPropertyMap implements PropertyMap return null; } + @Override public boolean hasNext() { return (_next != null); } + @Override public Property next() { if(!hasNext()) { throw new NoSuchElementException(); @@ -155,6 +170,7 @@ public class MultiValueColumnPropertyMap implements PropertyMap return prop; } + @Override public void remove() { if(_cur != null) { _cur.remove(); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java index a7a5dac..ce729a1 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/UnsupportedColumnInfoImpl.java @@ -108,22 +108,27 @@ public class UnsupportedColumnInfoImpl _values = values; } + @Override public Map<String,Object> getValues() { return _values; } + @Override public Object get(String columnName) { return getValues().get(columnName); } + @Override public void set(String columnName, Object value) { getValues().put(columnName, value); } + @Override public void update() throws IOException { getComplexValueForeignKey().updateUnsupportedValue(this); } + @Override public void delete() throws IOException { getComplexValueForeignKey().deleteUnsupportedValue(this); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java index a64788f..944e2d3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java @@ -17,6 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.complex; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Collections; import java.util.Date; import java.util.List; @@ -42,14 +43,14 @@ import com.healthmarketscience.jackcess.impl.ColumnImpl; * * @author James Ahlborn */ -public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> +public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> implements VersionHistoryColumnInfo { private final Column _valueCol; private final Column _modifiedCol; - + public VersionHistoryColumnInfoImpl(Column column, int complexId, - Table typeObjTable, Table flatTable) + Table typeObjTable, Table flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); @@ -83,7 +84,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> getValueColumn().getName()); ((ColumnImpl)versionedCol).setVersionHistoryColumn((ColumnImpl)getColumn()); } - + public Column getValueColumn() { return _valueCol; } @@ -91,7 +92,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> public Column getModifiedDateColumn() { return _modifiedCol; } - + @Override public ComplexDataType getType() { return ComplexDataType.VERSION_HISTORY; @@ -124,7 +125,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> // order versions newest to oldest Collections.sort(versions); - + return versions; } @@ -133,7 +134,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> Row rawValue) { ComplexValue.Id id = getValueId(rawValue); String value = (String)getValueColumn().getRowValue(rawValue); - Date modifiedDate = (Date)getModifiedDateColumn().getRowValue(rawValue); + Object modifiedDate = getModifiedDateColumn().getRowValue(rawValue); return new VersionImpl(id, complexValueFk, value, modifiedDate); } @@ -142,47 +143,60 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> protected Object[] asRow(Object[] row, Version version) throws IOException { super.asRow(row, version); getValueColumn().setRowValue(row, version.getValue()); - getModifiedDateColumn().setRowValue(row, version.getModifiedDate()); + getModifiedDateColumn().setRowValue(row, version.getModifiedDateObject()); return row; } - - public static Version newVersion(String value, Date modifiedDate) { + + public static Version newVersion(String value, Object modifiedDate) { return newVersion(INVALID_FK, value, modifiedDate); } - + public static Version newVersion(ComplexValueForeignKey complexValueFk, - String value, Date modifiedDate) { + String value, Object modifiedDate) { return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate); } - + @SuppressWarnings("deprecation") private static class VersionImpl extends ComplexValueImpl implements Version { private final String _value; - private final Date _modifiedDate; + private final Object _modifiedDate; private VersionImpl(Id id, ComplexValueForeignKey complexValueFk, - String value, Date modifiedDate) + String value, Object modifiedDate) { super(id, complexValueFk); _value = value; _modifiedDate = modifiedDate; } - + + @Override public String getValue() { return _value; } + @Override public Date getModifiedDate() { + return (Date)_modifiedDate; + } + + @Override + public LocalDateTime getModifiedLocalDate() { + return (LocalDateTime)_modifiedDate; + } + + @Override + public Object getModifiedDateObject() { return _modifiedDate; - } - + } + + @Override public int compareTo(Version o) { - Date d1 = getModifiedDate(); - Date d2 = o.getModifiedDate(); + Object d1 = getModifiedDateObject(); + Object d2 = o.getModifiedDateObject(); // sort by descending date (newest/greatest first) - int cmp = d2.compareTo(d1); + int cmp = compare(d2, d1); if(cmp != 0) { return cmp; } @@ -200,11 +214,22 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> ((id1 < id2) ? 1 : 0)); } + @SuppressWarnings("unchecked") + private static <C extends Comparable<C>> int compare(Object o1, Object o2) { + // each date/time type (Date, LocalDateTime) is mutually Comparable, so + // just silence the compiler + C c1 = (C)o1; + C c2 = (C)o2; + return c1.compareTo(c2); + } + + @Override public void update() throws IOException { throw new UnsupportedOperationException( "This column does not support value updates"); } - + + @Override public void delete() throws IOException { throw new UnsupportedOperationException( "This column does not support value deletes"); @@ -214,8 +239,8 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl<Version> public String toString() { return "Version(" + getComplexValueForeignKey() + "," + getId() + ") " + - getModifiedDate() + ", " + getValue(); - } + getModifiedDateObject() + ", " + getValue(); + } } - + } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java index d527c69..8c2b9de 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java @@ -17,7 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; @@ -40,42 +40,52 @@ public abstract class BaseDelayedValue implements Value return _val; } + @Override public boolean isNull() { return(getType() == Type.NULL); } + @Override public Value.Type getType() { return getDelegate().getType(); } + @Override public Object get() { return getDelegate().get(); } + @Override public boolean getAsBoolean(LocaleContext ctx) { return getDelegate().getAsBoolean(ctx); } + @Override public String getAsString(LocaleContext ctx) { return getDelegate().getAsString(ctx); } - public Date getAsDateTime(LocaleContext ctx) { - return getDelegate().getAsDateTime(ctx); + @Override + public LocalDateTime getAsLocalDateTime(LocaleContext ctx) { + return getDelegate().getAsLocalDateTime(ctx); } + @Override public Value getAsDateTimeValue(LocaleContext ctx) { return getDelegate().getAsDateTimeValue(ctx); } + @Override public Integer getAsLongInt(LocaleContext ctx) { return getDelegate().getAsLongInt(ctx); } + @Override public Double getAsDouble(LocaleContext ctx) { return getDelegate().getAsDouble(ctx); } + @Override public BigDecimal getAsBigDecimal(LocaleContext ctx) { return getDelegate().getAsBigDecimal(ctx); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java index 299cd2a..cae689d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java @@ -44,7 +44,7 @@ public abstract class BaseNumericValue extends BaseValue @Override public Value getAsDateTimeValue(LocaleContext ctx) { Value dateValue = DefaultDateFunctions.numberToDateValue( - ctx, getNumber().doubleValue()); + getNumber().doubleValue()); if(dateValue == null) { throw invalidConversion(Value.Type.DATE_TIME); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java index 2b172d3..e2a7453 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java @@ -17,11 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; -import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.LocaleContext; +import com.healthmarketscience.jackcess.expr.Value; /** * @@ -29,34 +29,42 @@ import com.healthmarketscience.jackcess.expr.LocaleContext; */ public abstract class BaseValue implements Value { + @Override public boolean isNull() { return(getType() == Type.NULL); } + @Override public boolean getAsBoolean(LocaleContext ctx) { throw invalidConversion(Type.LONG); } + @Override public String getAsString(LocaleContext ctx) { throw invalidConversion(Type.STRING); } - public Date getAsDateTime(LocaleContext ctx) { - return (Date)getAsDateTimeValue(ctx).get(); + @Override + public LocalDateTime getAsLocalDateTime(LocaleContext ctx) { + return (LocalDateTime)getAsDateTimeValue(ctx).get(); } + @Override public Value getAsDateTimeValue(LocaleContext ctx) { throw invalidConversion(Type.DATE_TIME); } + @Override public Integer getAsLongInt(LocaleContext ctx) { throw invalidConversion(Type.LONG); } + @Override public Double getAsDouble(LocaleContext ctx) { throw invalidConversion(Type.DOUBLE); } + @Override public BigDecimal getAsBigDecimal(LocaleContext ctx) { throw invalidConversion(Type.BIG_DEC); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java index 0e78f90..56bc911 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java @@ -33,10 +33,12 @@ public class BigDecimalValue extends BaseNumericValue _val = val; } + @Override public Type getType() { return Type.BIG_DEC; } + @Override public Object get() { return _val; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java index 5131a93..5f63dad 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java @@ -76,7 +76,7 @@ public class BuiltinOperators case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = -param1.getAsDouble(ctx); - return toDateValue(ctx, mathType, result); + return toDateValueIfPossible(mathType, result); case LONG: return toValue(-param1.getAsLongInt(ctx)); case DOUBLE: @@ -108,7 +108,7 @@ public class BuiltinOperators case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = param1.getAsDouble(ctx) + param2.getAsDouble(ctx); - return toDateValue(ctx, mathType, result); + return toDateValueIfPossible(mathType, result); case LONG: return toValue(param1.getAsLongInt(ctx) + param2.getAsLongInt(ctx)); case DOUBLE: @@ -138,7 +138,7 @@ public class BuiltinOperators case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = param1.getAsDouble(ctx) - param2.getAsDouble(ctx); - return toDateValue(ctx, mathType, result); + return toDateValueIfPossible(mathType, result); case LONG: return toValue(param1.getAsLongInt(ctx) - param2.getAsLongInt(ctx)); case DOUBLE: diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java index e2de36d..9b58941 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java @@ -17,11 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; -import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; /** * @@ -30,9 +30,9 @@ import com.healthmarketscience.jackcess.expr.Value; public class DateTimeValue extends BaseValue { private final Type _type; - private final Date _val; + private final LocalDateTime _val; - public DateTimeValue(Type type, Date val) { + public DateTimeValue(Type type, LocalDateTime val) { if(!type.isTemporal()) { throw new IllegalArgumentException("invalid date/time type"); } @@ -40,16 +40,18 @@ public class DateTimeValue extends BaseValue _val = val; } + @Override public Type getType() { return _type; } + @Override public Object get() { return _val; } protected Double getNumber(LocaleContext ctx) { - return ColumnImpl.toDateDouble(_val, ctx.getCalendar()); + return ColumnImpl.toDateDouble(_val); } @Override @@ -64,7 +66,7 @@ public class DateTimeValue extends BaseValue } @Override - public Date getAsDateTime(LocaleContext ctx) { + public LocalDateTime getAsLocalDateTime(LocaleContext ctx) { return _val; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java index a19ab0a..31d919b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java @@ -17,11 +17,20 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; -import java.math.BigDecimal; -import java.text.DateFormat; -import java.text.DateFormatSymbols; -import java.util.Calendar; -import java.util.Date; +import java.time.DateTimeException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.Year; +import java.time.format.DateTimeFormatter; +import java.time.format.TextStyle; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.WeekFields; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.EvalException; @@ -44,12 +53,6 @@ public class DefaultDateFunctions // 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 String INTV_YEAR = "yyyy"; private static final String INTV_QUARTER = "q"; private static final String INTV_MONTH = "m"; @@ -61,7 +64,8 @@ public class DefaultDateFunctions private static final String INTV_MINUTE = "n"; private static final String INTV_SECOND = "s"; - private enum WeekOpType { GET_WEEK, GET_NUM_WEEKS } + private static final WeekFields SUNDAY_FIRST = + WeekFields.of(DayOfWeek.SUNDAY, 1); private DefaultDateFunctions() {} @@ -72,8 +76,7 @@ public class DefaultDateFunctions public static final Function DATE = registerFunc(new Func0("Date") { @Override protected Value eval0(EvalContext ctx) { - double dd = dateOnly(currentTimeDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.DATE, dd); + return ValueSupport.toValue(LocalDate.now()); } }); @@ -84,8 +87,7 @@ public class DefaultDateFunctions if(dv.getType() == Value.Type.DATE) { return dv; } - double dd = dateOnly(dv.getAsDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.DATE, dd); + return ValueSupport.toValue(dv.getAsLocalDateTime(ctx).toLocalDate()); } }); @@ -101,15 +103,11 @@ public class DefaultDateFunctions year += ((year <= 29) ? 2000 : 1900); } - Calendar cal = ctx.getCalendar(); - cal.clear(); + // we have to construct incrementatlly to handle out of range values + LocalDate ld = LocalDate.of(year,1,1).plusMonths(month - 1) + .plusDays(day - 1); - cal.set(Calendar.YEAR, year); - // convert to 0 based value - cal.set(Calendar.MONTH, month - 1); - cal.set(Calendar.DAY_OF_MONTH, day); - - return ValueSupport.toValue(Value.Type.DATE, cal.getTime()); + return ValueSupport.toValue(ld); } }); @@ -127,27 +125,27 @@ public class DefaultDateFunctions String intv = params[0].getAsString(ctx).trim(); int result = -1; if(intv.equalsIgnoreCase(INTV_YEAR)) { - result = nonNullToCalendarField(ctx, param2, Calendar.YEAR); + result = param2.getAsLocalDateTime(ctx).getYear(); } else if(intv.equalsIgnoreCase(INTV_QUARTER)) { - result = getQuarter(nonNullToCalendar(ctx, param2)); + result = getQuarter(param2.getAsLocalDateTime(ctx)); } else if(intv.equalsIgnoreCase(INTV_MONTH)) { - // convert from 0 based to 1 based value - result = nonNullToCalendarField(ctx, param2, Calendar.MONTH) + 1; + result = param2.getAsLocalDateTime(ctx).getMonthValue(); } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR)) { - result = nonNullToCalendarField(ctx, param2, Calendar.DAY_OF_YEAR); + result = param2.getAsLocalDateTime(ctx).getDayOfYear(); } else if(intv.equalsIgnoreCase(INTV_DAY)) { - result = nonNullToCalendarField(ctx, param2, Calendar.DAY_OF_MONTH); + result = param2.getAsLocalDateTime(ctx).getDayOfMonth(); } else if(intv.equalsIgnoreCase(INTV_WEEKDAY)) { - int dayOfWeek = nonNullToCalendarField(ctx, param2, Calendar.DAY_OF_WEEK); + int dayOfWeek = param2.getAsLocalDateTime(ctx) + .get(SUNDAY_FIRST.dayOfWeek()); result = dayOfWeekToWeekDay(dayOfWeek, firstDay); } else if(intv.equalsIgnoreCase(INTV_WEEK)) { result = weekOfYear(ctx, param2, firstDay, firstWeekType); } else if(intv.equalsIgnoreCase(INTV_HOUR)) { - result = nonNullToCalendarField(ctx, param2, Calendar.HOUR_OF_DAY); + result = param2.getAsLocalDateTime(ctx).getHour(); } else if(intv.equalsIgnoreCase(INTV_MINUTE)) { - result = nonNullToCalendarField(ctx, param2, Calendar.MINUTE); + result = param2.getAsLocalDateTime(ctx).getMinute(); } else if(intv.equalsIgnoreCase(INTV_SECOND)) { - result = nonNullToCalendarField(ctx, param2, Calendar.SECOND); + result = param2.getAsLocalDateTime(ctx).getSecond(); } else { throw new EvalException("Invalid interval " + intv); } @@ -167,33 +165,31 @@ public class DefaultDateFunctions String intv = param1.getAsString(ctx).trim(); int val = param2.getAsLongInt(ctx); - Calendar cal = nonNullToCalendar(ctx, param3); + LocalDateTime ldt = param3.getAsLocalDateTime(ctx); if(intv.equalsIgnoreCase(INTV_YEAR)) { - cal.add(Calendar.YEAR, val); + ldt = ldt.plus(val, ChronoUnit.YEARS); } else if(intv.equalsIgnoreCase(INTV_QUARTER)) { - cal.add(Calendar.MONTH, val * 3); + ldt = ldt.plus(val * 3, ChronoUnit.MONTHS); } else if(intv.equalsIgnoreCase(INTV_MONTH)) { - cal.add(Calendar.MONTH, val); - } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR)) { - cal.add(Calendar.DAY_OF_YEAR, val); - } else if(intv.equalsIgnoreCase(INTV_DAY)) { - cal.add(Calendar.DAY_OF_YEAR, val); - } else if(intv.equalsIgnoreCase(INTV_WEEKDAY)) { - cal.add(Calendar.DAY_OF_WEEK, val); + ldt = ldt.plus(val, ChronoUnit.MONTHS); + } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR) || + intv.equalsIgnoreCase(INTV_DAY) || + intv.equalsIgnoreCase(INTV_WEEKDAY)) { + ldt = ldt.plus(val, ChronoUnit.DAYS); } else if(intv.equalsIgnoreCase(INTV_WEEK)) { - cal.add(Calendar.WEEK_OF_YEAR, val); + ldt = ldt.plus(val, ChronoUnit.WEEKS); } else if(intv.equalsIgnoreCase(INTV_HOUR)) { - cal.add(Calendar.HOUR, val); + ldt = ldt.plus(val, ChronoUnit.HOURS); } else if(intv.equalsIgnoreCase(INTV_MINUTE)) { - cal.add(Calendar.MINUTE, val); + ldt = ldt.plus(val, ChronoUnit.MINUTES); } else if(intv.equalsIgnoreCase(INTV_SECOND)) { - cal.add(Calendar.SECOND, val); + ldt = ldt.plus(val, ChronoUnit.SECONDS); } else { throw new EvalException("Invalid interval " + intv); } - return ValueSupport.toValue(cal); + return ValueSupport.toValue(ldt); } }); @@ -212,14 +208,14 @@ public class DefaultDateFunctions String intv = params[0].getAsString(ctx).trim(); - Calendar cal1 = nonNullToCalendar(ctx, param2); - Calendar cal2 = nonNullToCalendar(ctx, param3); + LocalDateTime ldt1 = param2.getAsLocalDateTime(ctx); + LocalDateTime ldt2 = param3.getAsLocalDateTime(ctx); int sign = 1; - if(cal1.after(cal2)) { - Calendar tmp = cal1; - cal1 = cal2; - cal2 = tmp; + if(ldt1.isAfter(ldt2)) { + LocalDateTime tmp = ldt1; + ldt1 = ldt2; + ldt2 = tmp; sign = -1; } @@ -229,22 +225,22 @@ public class DefaultDateFunctions int result = -1; if(intv.equalsIgnoreCase(INTV_YEAR)) { - result = cal2.get(Calendar.YEAR) - cal1.get(Calendar.YEAR); + result = ldt2.getYear() - ldt1.getYear(); } else if(intv.equalsIgnoreCase(INTV_QUARTER)) { - int y1 = cal1.get(Calendar.YEAR); - int q1 = getQuarter(cal1); - int y2 = cal2.get(Calendar.YEAR); - int q2 = getQuarter(cal2); + int y1 = ldt1.getYear(); + int q1 = getQuarter(ldt1); + int y2 = ldt2.getYear(); + int q2 = getQuarter(ldt2); while(y2 > y1) { q2 += 4; --y2; } result = q2 - q1; } else if(intv.equalsIgnoreCase(INTV_MONTH)) { - int y1 = cal1.get(Calendar.YEAR); - int m1 = cal1.get(Calendar.MONTH); - int y2 = cal2.get(Calendar.YEAR); - int m2 = cal2.get(Calendar.MONTH); + int y1 = ldt1.getYear(); + int m1 = ldt1.getMonthValue(); + int y2 = ldt2.getYear(); + int m2 = ldt2.getMonthValue(); while(y2 > y1) { m2 += 12; --y2; @@ -252,30 +248,30 @@ public class DefaultDateFunctions result = m2 - m1; } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR) || intv.equalsIgnoreCase(INTV_DAY)) { - result = getDayDiff(cal1, cal2); + result = getDayDiff(ldt1, ldt2); } else if(intv.equalsIgnoreCase(INTV_WEEKDAY)) { // this calulates number of 7 day periods between two dates - result = getDayDiff(cal1, cal2) / 7; + result = getDayDiff(ldt1, ldt2) / 7; } else if(intv.equalsIgnoreCase(INTV_WEEK)) { // this counts number of "week of year" intervals between two dates - int w1 = weekOfYear(cal1, firstDay, firstWeekType); - int y1 = getWeekOfYearYear(cal1, w1); - int w2 = weekOfYear(cal2, firstDay, firstWeekType); - int y2 = getWeekOfYearYear(cal2, w2); + WeekFields weekFields = weekFields(firstDay, firstWeekType); + int w1 = ldt1.get(weekFields.weekOfWeekBasedYear()); + int y1 = ldt1.get(weekFields.weekBasedYear()); + int w2 = ldt2.get(weekFields.weekOfWeekBasedYear()); + int y2 = ldt2.get(weekFields.weekBasedYear()); while(y2 > y1) { - cal2.add(Calendar.YEAR, -1); - w2 += weeksInYear(cal2, firstDay, firstWeekType); - y2 = cal2.get(Calendar.YEAR); + --y2; + w2 += weeksInYear(y2, weekFields); } result = w2 - w1; } else if(intv.equalsIgnoreCase(INTV_HOUR)) { - result = getHourDiff(cal1, cal2); + result = getHourDiff(ldt1, ldt2); } else if(intv.equalsIgnoreCase(INTV_MINUTE)) { - result = getMinuteDiff(cal1, cal2); + result = getMinuteDiff(ldt1, ldt2); } else if(intv.equalsIgnoreCase(INTV_SECOND)) { - int s1 = cal1.get(Calendar.SECOND); - int s2 = cal2.get(Calendar.SECOND); - int minuteDiff = getMinuteDiff(cal1, cal2); + int s1 = ldt1.getSecond(); + int s2 = ldt2.getSecond(); + int minuteDiff = getMinuteDiff(ldt1, ldt2); result = (s2 + (60 * minuteDiff)) - s1; } else { throw new EvalException("Invalid interval " + intv); @@ -288,15 +284,15 @@ public class DefaultDateFunctions public static final Function NOW = registerFunc(new Func0("Now") { @Override protected Value eval0(EvalContext ctx) { - return ValueSupport.toValue(Value.Type.DATE_TIME, new Date()); + return ValueSupport.toValue(Value.Type.DATE_TIME, + LocalDateTime.now(ctx.getZoneId())); } }); public static final Function TIME = registerFunc(new Func0("Time") { @Override protected Value eval0(EvalContext ctx) { - double dd = timeOnly(currentTimeDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.TIME, dd); + return ValueSupport.toValue(LocalTime.now(ctx.getZoneId())); } }); @@ -307,15 +303,15 @@ public class DefaultDateFunctions if(dv.getType() == Value.Type.TIME) { return dv; } - double dd = timeOnly(dv.getAsDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.TIME, dd); + return ValueSupport.toValue(dv.getAsLocalDateTime(ctx).toLocalTime()); } }); public static final Function TIMER = registerFunc(new Func0("Timer") { @Override protected Value eval0(EvalContext ctx) { - double dd = timeOnly(currentTimeDouble(ctx)) * DSECONDS_PER_DAY; + double dd = LocalTime.now(ctx.getZoneId()) + .get(ChronoField.MILLI_OF_DAY) / 1000d; return ValueSupport.toValue(dd); } }); @@ -327,59 +323,46 @@ public class DefaultDateFunctions int minutes = param2.getAsLongInt(ctx); int seconds = param3.getAsLongInt(ctx); - long totalSeconds = (hours * SECONDS_PER_HOUR) + - (minutes * SECONDS_PER_MINUTE) + seconds; - if(totalSeconds < 0L) { - do { - totalSeconds += SECONDS_PER_DAY; - } while(totalSeconds < 0L); - } else if(totalSeconds > SECONDS_PER_DAY) { - totalSeconds %= SECONDS_PER_DAY; - } + // we have to construct incrementatlly to handle out of range values + LocalTime lt = ColumnImpl.BASE_LT.plusHours(hours).plusMinutes(minutes) + .plusSeconds(seconds); - double dd = totalSeconds / DSECONDS_PER_DAY; - return ValueSupport.toDateValue(ctx, Value.Type.TIME, dd); + return ValueSupport.toValue(lt); } }); public static final Function HOUR = registerFunc(new Func1NullIsNull("Hour") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.HOUR_OF_DAY)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getHour()); } }); public static final Function MINUTE = registerFunc(new Func1NullIsNull("Minute") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.MINUTE)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getMinute()); } }); public static final Function SECOND = registerFunc(new Func1NullIsNull("Second") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.SECOND)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getSecond()); } }); public static final Function YEAR = registerFunc(new Func1NullIsNull("Year") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.YEAR)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getYear()); } }); 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 ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.MONTH) + 1); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getMonthValue()); } }); @@ -390,16 +373,12 @@ public class DefaultDateFunctions if(param1.isNull()) { return ValueSupport.NULL_VAL; } - // convert from 1 based to 0 based value - int month = param1.getAsLongInt(ctx) - 1; - - boolean abbreviate = getOptionalBooleanParam(ctx, params, 1); + Month month = Month.of(param1.getAsLongInt(ctx)); - DateFormatSymbols syms = ctx.getTemporalConfig().getDateFormatSymbols(); - String[] monthNames = (abbreviate ? - syms.getShortMonths() : syms.getMonths()); - // note, the array is 1 based - return ValueSupport.toValue(monthNames[month]); + TextStyle textStyle = getTextStyle(ctx, params, 1); + String monthName = month.getDisplayName( + textStyle, ctx.getTemporalConfig().getLocale()); + return ValueSupport.toValue(monthName); } }); @@ -407,7 +386,7 @@ public class DefaultDateFunctions @Override protected Value eval1(EvalContext ctx, Value param1) { return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_MONTH)); + param1.getAsLocalDateTime(ctx).getDayOfMonth()); } }); @@ -418,7 +397,8 @@ public class DefaultDateFunctions if(param1.isNull()) { return ValueSupport.NULL_VAL; } - int dayOfWeek = nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_WEEK); + int dayOfWeek = param1.getAsLocalDateTime(ctx) + .get(SUNDAY_FIRST.dayOfWeek()); int firstDay = getFirstDayParam(ctx, params, 1); @@ -435,31 +415,17 @@ public class DefaultDateFunctions } int weekday = param1.getAsLongInt(ctx); - boolean abbreviate = getOptionalBooleanParam(ctx, params, 1); + TextStyle textStyle = getTextStyle(ctx, params, 1); int firstDay = getFirstDayParam(ctx, params, 2); int dayOfWeek = weekDayToDayOfWeek(weekday, firstDay); - - DateFormatSymbols syms = ctx.getTemporalConfig().getDateFormatSymbols(); - String[] weekdayNames = (abbreviate ? - syms.getShortWeekdays() : syms.getWeekdays()); - // note, the array is 1 based - return ValueSupport.toValue(weekdayNames[dayOfWeek]); + String weekdayName = dayOfWeek(dayOfWeek).getDisplayName( + textStyle, ctx.getTemporalConfig().getLocale()); + return ValueSupport.toValue(weekdayName); } }); - private static int nonNullToCalendarField(EvalContext ctx, Value param, - int field) { - return nonNullToCalendar(ctx, param).get(field); - } - - private static Calendar nonNullToCalendar(EvalContext ctx, Value param) { - Calendar cal = ctx.getCalendar(); - cal.setTime(param.getAsDateTime(ctx)); - return cal; - } - static Value stringToDateValue(LocaleContext ctx, String valStr) { // see if we can coerce to date/time TemporalConfig.Type valTempType = ExpressionTokenizer.determineDateType( @@ -467,26 +433,31 @@ public class DefaultDateFunctions if(valTempType != null) { - DateFormat parseDf = ExpressionTokenizer.createParseDateTimeFormat( - valTempType, ctx); + DateTimeFormatter parseDf = ctx.createDateFormatter( + ctx.getTemporalConfig().getDateTimeFormat(valTempType)); try { - Date dateVal = ExpressionTokenizer.parseComplete(parseDf, valStr); - return ValueSupport.toValue(valTempType.getValueType(), dateVal); - } catch(java.text.ParseException pe) { + TemporalAccessor parsedInfo = parseDf.parse(valStr); + LocalDate ld = ColumnImpl.BASE_LD; if(valTempType.includesDate()) { - // the date may not include a year value, in which case it means - // to use the "current" year. see if this is an implicit year date - parseDf = ExpressionTokenizer.createParseImplicitYearDateTimeFormat( - valTempType, ctx); - try { - Date dateVal = ExpressionTokenizer.parseComplete(parseDf, valStr); - return ValueSupport.toValue(valTempType.getValueType(), dateVal); - } catch(java.text.ParseException pe2) { - // guess not, continue on to failure + // the year may not be explicitly specified + if(parsedInfo.isSupported(ChronoField.YEAR)) { + ld = LocalDate.from(parsedInfo); + } else { + ld = MonthDay.from(parsedInfo).atYear( + Year.now(ctx.getZoneId()).getValue()); } } + + LocalTime lt = ColumnImpl.BASE_LT; + if(valTempType.includesTime()) { + lt = LocalTime.from(parsedInfo); + } + + return ValueSupport.toValue(LocalDateTime.of(ld, lt)); + } catch(DateTimeException de) { + // note a valid date/time } } @@ -494,35 +465,18 @@ public class DefaultDateFunctions return null; } - static Value numberToDateValue(LocaleContext ctx, double dd) { - if((dd < MIN_DATE) || (dd > MAX_DATE)) { + static boolean isValidDateDouble(double dd) { + return ((dd >= MIN_DATE) && (dd <= MAX_DATE)); + } + + static Value numberToDateValue(double dd) { + if(!isValidDateDouble(dd)) { // 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); - return ValueSupport.toDateValue(ctx, type, dd); - } - - 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 - return new BigDecimal(dd).remainder(BigDecimal.ONE).doubleValue(); - } - - private static double currentTimeDouble(LocaleContext ctx) { - return ColumnImpl.toDateDouble(System.currentTimeMillis(), ctx.getCalendar()); + LocalDateTime ldt = ColumnImpl.ldtFromLocalDateDouble(dd); + return ValueSupport.toValue(ldt); } private static int dayOfWeekToWeekDay(int day, int firstDay) { @@ -551,114 +505,85 @@ public class DefaultDateFunctions return getOptionalIntParam(ctx, params, idx, 1, 0); } - private static int weekOfYear(EvalContext ctx, Value param, int firstDay, - int firstWeekType) { - return doWeekOp(nonNullToCalendar(ctx, param), firstDay, firstWeekType, - WeekOpType.GET_WEEK); - } + static WeekFields weekFields(int firstDay, int firstWeekType) { + + int minDays = 1; + switch(firstWeekType) { + case 1: + // vbUseSystem 0 + // vbFirstJan1 1 (default) + break; + case 2: + // vbFirstFourDays 2 + minDays = 4; + break; + case 3: + // vbFirstFullWeek 3 + minDays = 7; + break; + default: + throw new EvalException("Invalid first week of year type " + + firstWeekType); + } - private static int weekOfYear(Calendar cal, int firstDay, int firstWeekType) { - return doWeekOp(cal, firstDay, firstWeekType, WeekOpType.GET_WEEK); + return WeekFields.of(dayOfWeek(firstDay), minDays); } - private static int weeksInYear(Calendar cal, int firstDay, int firstWeekType) { - return doWeekOp(cal, firstDay, firstWeekType, WeekOpType.GET_NUM_WEEKS); + private static DayOfWeek dayOfWeek(int dayOfWeek) { + return DayOfWeek.SUNDAY.plus(dayOfWeek - 1); } - private static int doWeekOp(Calendar cal, int firstDay, int firstWeekType, - WeekOpType opType) { - // need to mess with some calendar settings, but they need to be restored - // when done because the Calendar instance may be shared - int origFirstDay = cal.getFirstDayOfWeek(); - int origMinDays = cal.getMinimalDaysInFirstWeek(); - try { - - int minDays = 1; - switch(firstWeekType) { - case 1: - // vbUseSystem 0 - // vbFirstJan1 1 (default) - break; - case 2: - // vbFirstFourDays 2 - minDays = 4; - break; - case 3: - // vbFirstFullWeek 3 - minDays = 7; - break; - default: - throw new EvalException("Invalid first week of year type " + - firstWeekType); - } - - cal.setFirstDayOfWeek(firstDay); - cal.setMinimalDaysInFirstWeek(minDays); + private static TextStyle getTextStyle(EvalContext ctx, Value[] params, + int idx) { + boolean abbreviate = getOptionalBooleanParam(ctx, params, 1); + return (abbreviate ? TextStyle.SHORT : TextStyle.FULL); + } - switch(opType) { - case GET_WEEK: - return cal.get(Calendar.WEEK_OF_YEAR); - case GET_NUM_WEEKS: - return cal.getActualMaximum(Calendar.WEEK_OF_YEAR); - default: - throw new RuntimeException("Unknown op type " + opType); - } + private static int weekOfYear(EvalContext ctx, Value param, int firstDay, + int firstWeekType) { + return weekOfYear(param.getAsLocalDateTime(ctx), firstDay, firstWeekType); + } - } finally { - cal.setFirstDayOfWeek(origFirstDay); - cal.setMinimalDaysInFirstWeek(origMinDays); - } + private static int weekOfYear(LocalDateTime ldt, int firstDay, + int firstWeekType) { + WeekFields weekFields = weekFields(firstDay, firstWeekType); + return ldt.get(weekFields.weekOfWeekBasedYear()); } - private static int getQuarter(Calendar cal) { - // month is 0 based - int month = cal.get(Calendar.MONTH); - return (month / 3) + 1; + private static int weeksInYear(int year, WeekFields weekFields) { + return (int)LocalDate.of(year,2,1).range(weekFields.weekOfWeekBasedYear()) + .getMaximum(); } - private static int getWeekOfYearYear(Calendar cal, int weekOfYear) { - // the "week of year" gets weird at the beginning/end of the year. - // e.g. 12/31 might be int the first week of the next year, and 1/1 might - // be in the last week of the previous year. we need to detect this and - // adjust the intervals accordingly - if(cal.get(Calendar.MONTH) == Calendar.JANUARY) { - if(weekOfYear >= 52) { - // this week of year is effectively for the previous year - cal.add(Calendar.YEAR, -1); - } - } else { - if(weekOfYear == 1) { - // this week of year is effectively for next year - cal.add(Calendar.YEAR, 1); - } - } - return cal.get(Calendar.YEAR); + private static int getQuarter(LocalDateTime ldt) { + int month = ldt.getMonthValue() - 1; + return (month / 3) + 1; } - private static int getDayDiff(Calendar cal1, Calendar cal2) { - int y1 = cal1.get(Calendar.YEAR); - int d1 = cal1.get(Calendar.DAY_OF_YEAR); - int y2 = cal2.get(Calendar.YEAR); - int d2 = cal2.get(Calendar.DAY_OF_YEAR); - while(y2 > y1) { - cal2.add(Calendar.YEAR, -1); - d2 += cal2.getActualMaximum(Calendar.DAY_OF_YEAR); - y2 = cal2.get(Calendar.YEAR); + private static int getDayDiff(LocalDateTime ldt1, LocalDateTime ldt2) { + int y1 = ldt1.getYear(); + int d1 = ldt1.getDayOfYear(); + int y2 = ldt2.getYear(); + int d2 = ldt2.getDayOfYear(); + while(y2 > y1) { + ldt2 = ldt2.minusYears(1); + d2 += ldt2.range(ChronoField.DAY_OF_YEAR).getMaximum(); + y2 = ldt2.getYear(); } return d2 - d1; } - private static int getHourDiff(Calendar cal1, Calendar cal2) { - int h1 = cal1.get(Calendar.HOUR_OF_DAY); - int h2 = cal2.get(Calendar.HOUR_OF_DAY); - int dayDiff = getDayDiff(cal1, cal2); + private static int getHourDiff(LocalDateTime ldt1, LocalDateTime ldt2) { + int h1 = ldt1.getHour(); + int h2 = ldt2.getHour(); + int dayDiff = getDayDiff(ldt1, ldt2); return (h2 + (24 * dayDiff)) - h1; } - private static int getMinuteDiff(Calendar cal1, Calendar cal2) { - int m1 = cal1.get(Calendar.MINUTE); - int m2 = cal2.get(Calendar.MINUTE); - int hourDiff = getHourDiff(cal1, cal2); + private static int getMinuteDiff(LocalDateTime ldt1, LocalDateTime ldt2) { + int m1 = ldt1.getMinute(); + int m2 = ldt2.getMinute(); + int hourDiff = getHourDiff(ldt1, ldt2); return (m2 + (60 * hourDiff)) - m1; } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java index 29c0f71..5a871e4 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java @@ -18,10 +18,9 @@ package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.DateFormat; import java.text.DecimalFormat; -import java.util.Calendar; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; @@ -54,6 +53,7 @@ public class DefaultFunctions } public static final FunctionLookup LOOKUP = new FunctionLookup() { + @Override public Function getFunction(String name) { return FUNCS.get(DatabaseImpl.toLookupName(name)); } @@ -310,16 +310,14 @@ public class DefaultFunctions return ValueSupport.NULL_VAL; } - Date d = param1.getAsDateTime(ctx); + LocalDateTime ldt = param1.getAsLocalDateTime(ctx); int fmtType = getOptionalIntParam(ctx, params, 1, 0); TemporalConfig.Type tempType = null; switch(fmtType) { case 0: // vbGeneralDate - Calendar cal = ctx.getCalendar(); - cal.setTime(d); - Value.Type valType = ValueSupport.getDateTimeType(cal); + Value.Type valType = ValueSupport.getDateTimeType(ldt); switch(valType) { case DATE: tempType = TemporalConfig.Type.SHORT_DATE; @@ -351,9 +349,9 @@ public class DefaultFunctions throw new EvalException("Unknown format " + fmtType); } - DateFormat sdf = ctx.createDateFormat( + DateTimeFormatter dtf = ctx.createDateFormatter( ctx.getTemporalConfig().getDateTimeFormat(tempType)); - return ValueSupport.toValue(sdf.format(d)); + return ValueSupport.toValue(dtf.format(ldt)); } }); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java index 1ec08db..70eb5a9 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java @@ -49,7 +49,7 @@ public class DefaultNumberFunctions case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = Math.abs(param1.getAsDouble(ctx)); - return ValueSupport.toDateValue(ctx, mathType, result); + return ValueSupport.toDateValueIfPossible(mathType, result); case LONG: return ValueSupport.toValue(Math.abs(param1.getAsLongInt(ctx))); case DOUBLE: diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java index 74856aa..77edf80 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java @@ -13,7 +13,6 @@ 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; @@ -23,7 +22,6 @@ import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.Function; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; -import org.apache.commons.lang.WordUtils; import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; import static com.healthmarketscience.jackcess.impl.expr.FunctionSupport.*; @@ -360,6 +358,7 @@ public class DefaultTextFunctions } }); + @SuppressWarnings("deprecation") public static final Function STRCONV = registerStringFunc(new FuncVar("StrConv", 2, 3) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { @@ -387,7 +386,8 @@ public class DefaultTextFunctions break; case 3: // vbProperCase - str = WordUtils.capitalize(str.toLowerCase()); + str = org.apache.commons.lang3.text.WordUtils.capitalize( + str.toLowerCase()); break; default: // do nothing diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java index d37c90b..e00e24a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java @@ -33,10 +33,12 @@ public class DoubleValue extends BaseNumericValue _val = val; } + @Override public Type getType() { return Type.DOUBLE; } + @Override public Object get() { return _val; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java index 8705490..39f7050 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -17,26 +17,27 @@ 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.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; import java.util.EnumMap; 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.LocaleContext; import com.healthmarketscience.jackcess.expr.ParseException; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; /** @@ -53,15 +54,6 @@ class ExpressionTokenizer private static final char DATE_LIT_QUOTE_CHAR = '#'; private static final char EQUALS_CHAR = '='; - // access times are based on this date (not the UTC base) - static final int BASE_DATE_YEAR = 1899; - static final int BASE_DATE_MONTH = 12; - static final int BASE_DATE_DAY = 30; - private static final String BASE_DATE_PREFIX = "1899/12/30 "; - private static final String BASE_DATE_FMT_PREFIX = "yyyy/M/d "; - - private static final String IMPLICIT_YEAR_FMT_PREFIX = "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; @@ -247,23 +239,29 @@ class ExpressionTokenizer } private static String parseQuotedString(ExprBuf buf, char quoteChar) { - return parseStringUntil(buf, quoteChar, null, true); + return parseStringUntil(buf, null, quoteChar, true); } private static String parseObjNameString(ExprBuf buf) { - return parseStringUntil(buf, OBJ_NAME_END_CHAR, OBJ_NAME_START_CHAR, false); + return parseStringUntil(buf, OBJ_NAME_START_CHAR, OBJ_NAME_END_CHAR, false); } private static String parseDateLiteralString(ExprBuf buf) { - return parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null, false); + return parseStringUntil(buf, null, DATE_LIT_QUOTE_CHAR, false); } - private static String parseStringUntil(ExprBuf buf, char endChar, - Character startChar, - boolean allowDoubledEscape) + static String parseStringUntil(ExprBuf buf, Character startChar, + char endChar, boolean allowDoubledEscape) { - StringBuilder sb = buf.getScratchBuffer(); + return parseStringUntil(buf, startChar, endChar, allowDoubledEscape, + buf.getScratchBuffer()) + .toString(); + } + static StringBuilder parseStringUntil( + ExprBuf buf, Character startChar, char endChar, boolean allowDoubledEscape, + StringBuilder sb) + { boolean complete = false; while(buf.hasNext()) { char c = buf.next(); @@ -288,7 +286,7 @@ class ExpressionTokenizer "' for quoted string " + buf); } - return sb.toString(); + return sb; } private static Token parseDateLiteral(ExprBuf buf) @@ -304,14 +302,26 @@ class ExpressionTokenizer // note that although we may parse in the time "24" format, we will // display as the default time format - DateFormat parseDf = buf.getParseDateTimeFormat(type); + DateTimeFormatter parseDf = buf.getParseDateTimeFormat(type); try { - return new Token(TokenType.LITERAL, parseComplete(parseDf, dateStr), + TemporalAccessor parsedInfo = parseDf.parse(dateStr); + + LocalDate ld = ColumnImpl.BASE_LD; + if(type.includesDate()) { + ld = LocalDate.from(parsedInfo); + } + + LocalTime lt = ColumnImpl.BASE_LT; + if(type.includesTime()) { + lt = LocalTime.from(parsedInfo); + } + + return new Token(TokenType.LITERAL, LocalDateTime.of(ld, lt), dateStr, type.getValueType()); - } catch(java.text.ParseException pe) { + } catch(DateTimeException de) { throw new ParseException( - "Invalid date/time literal " + dateStr + " " + buf, pe); + "Invalid date/time literal " + dateStr + " " + buf, de); } } @@ -324,7 +334,7 @@ class ExpressionTokenizer boolean hasAmPm = false; if(hasTime) { - String[] amPmStrs = cfg.getDateFormatSymbols().getAmPmStrings(); + String[] amPmStrs = cfg.getAmPmStrings(); String amStr = " " + amPmStrs[0]; String pmStr = " " + amPmStrs[1]; hasAmPm = (hasSuffix(dateStr, amStr) || hasSuffix(dateStr, pmStr)); @@ -351,23 +361,6 @@ class ExpressionTokenizer suffStr, 0, suffStrLen)); } - static DateFormat createParseDateTimeFormat(TemporalConfig.Type type, - LocaleContext ctx) - { - if(type.isTimeOnly()) { - return new ParseTimeFormat(type, ctx); - } - - TemporalConfig cfg = ctx.getTemporalConfig(); - return ctx.createDateFormat(cfg.getDateTimeFormat(type)); - } - - static DateFormat createParseImplicitYearDateTimeFormat( - TemporalConfig.Type type, LocaleContext ctx) - { - return new ParseImplicitYearFormat(type, ctx); - } - private static Token maybeParseNumberLiteral(char firstChar, ExprBuf buf) { StringBuilder sb = buf.getScratchBuffer().append(firstChar); boolean hasDigit = isDigit(firstChar); @@ -463,32 +456,17 @@ class ExpressionTokenizer return new AbstractMap.SimpleImmutableEntry<K,V>(a, b); } - static Date parseComplete(DateFormat df, String str) - throws java.text.ParseException - { - // the java parsers will parse "successfully" even if there is leftover - // information. we only want to consider a parse operation successful if - // it parses the entire string (ignoring surrounding whitespace) - str = str.trim(); - ParsePosition pp = new ParsePosition(0); - Object d = df.parse(str, pp); - if(pp.getIndex() < str.length()) { - throw new java.text.ParseException("Failed parsing '" + str + "'", - pp.getIndex()); - } - return (Date)d; - } - - private static final class ExprBuf + static final class ExprBuf { private final String _str; private final ParseContext _ctx; private int _pos; - private final Map<TemporalConfig.Type,DateFormat> _dateTimeFmts = - new EnumMap<TemporalConfig.Type,DateFormat>(TemporalConfig.Type.class); + private final Map<TemporalConfig.Type,DateTimeFormatter> _dateTimeFmts = + new EnumMap<TemporalConfig.Type,DateTimeFormatter>( + TemporalConfig.Type.class); private final StringBuilder _scratch = new StringBuilder(); - private ExprBuf(String str, ParseContext ctx) { + ExprBuf(String str, ParseContext ctx) { _str = str; _ctx = ctx; } @@ -537,10 +515,11 @@ class ExpressionTokenizer return _ctx; } - public DateFormat getParseDateTimeFormat(TemporalConfig.Type type) { - DateFormat df = _dateTimeFmts.get(type); + public DateTimeFormatter getParseDateTimeFormat(TemporalConfig.Type type) { + DateTimeFormatter df = _dateTimeFmts.get(type); if(df == null) { - df = createParseDateTimeFormat(type, _ctx); + df = _ctx.createDateFormatter( + _ctx.getTemporalConfig().getDateTimeFormat(type)); _dateTimeFmts.put(type, df); } return df; @@ -604,98 +583,4 @@ class ExpressionTokenizer } } - /** - * Base DateFormat implementation for parsing date/time formats where - * additional information is added on to the format in order for it to be - * parsed correctly. - */ - private static abstract class ParsePrefixFormat extends DateFormat - { - private static final long serialVersionUID = 0L; - - private final DateFormat _parseDelegate; - - private ParsePrefixFormat(String formatPrefix, String formatStr, - LocaleContext ctx) { - _parseDelegate = ctx.createDateFormat(formatPrefix + formatStr); - } - - @Override - public StringBuffer format(Date date, StringBuffer toAppendTo, - FieldPosition fieldPosition) { - throw new UnsupportedOperationException(); - } - - @Override - public Date parse(String source, ParsePosition pos) { - String prefix = getPrefix(); - - Date result = _parseDelegate.parse(prefix + source, pos); - - // adjust index for original string - pos.setIndex(pos.getIndex() - prefix.length()); - - return result; - } - - @Override - public Calendar getCalendar() { - return _parseDelegate.getCalendar(); - } - - @Override - public TimeZone getTimeZone() { - return _parseDelegate.getTimeZone(); - } - - protected abstract String getPrefix(); - } - - /** - * Special date/time format which will parse time-only strings "correctly" - * according to how access handles time-only values. - */ - private static final class ParseTimeFormat extends ParsePrefixFormat - { - private static final long serialVersionUID = 0L; - - private ParseTimeFormat(TemporalConfig.Type timeType, LocaleContext ctx) { - super(BASE_DATE_FMT_PREFIX, - ctx.getTemporalConfig().getDateTimeFormat(timeType), ctx); - } - - @Override - protected String getPrefix() { - // we parse as a full date/time in order to get the correct "base date" - // used by access - return BASE_DATE_PREFIX; - } - } - - /** - * Special date/time format which will parse dates with implicit (current) - * years. - */ - private static final class ParseImplicitYearFormat extends ParsePrefixFormat - { - private static final long serialVersionUID = 0L; - - private ParseImplicitYearFormat(TemporalConfig.Type type, - LocaleContext ctx) { - super(IMPLICIT_YEAR_FMT_PREFIX, - ctx.getTemporalConfig().getImplicitYearDateTimeFormat(type), - ctx); - } - - @Override - protected String getPrefix() { - // need to get the current year - Calendar cal = getCalendar(); - cal.setTimeInMillis(System.currentTimeMillis()); - int year = cal.get(Calendar.YEAR); - // return a value matching IMPLICIT_YEAR_FMT_PREFIX - return year + " "; - } - } - } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java index d2bb847..d9cf5b9 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java @@ -17,13 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -43,11 +41,10 @@ import com.healthmarketscience.jackcess.expr.FunctionLookup; import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.ParseException; -import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** @@ -68,8 +65,6 @@ public class Expressionator } public interface ParseContext extends LocaleContext { - public TemporalConfig getTemporalConfig(); - public SimpleDateFormat createDateFormat(String formatStr); public FunctionLookup getFunctionLookup(); } @@ -1318,7 +1313,7 @@ public class Expressionator case DATE: case TIME: case DATE_TIME: - return ValueSupport.toValue(valType, (Date)value); + return ValueSupport.toValue(valType, (LocalDateTime)value); case LONG: return ValueSupport.toValue((Integer)value); case DOUBLE: @@ -1690,22 +1685,27 @@ public class Expressionator return areConstant(_left, _right); } + @Override public OpType getOp() { return _op; } + @Override public Expr getLeft() { return _left; } + @Override public void setLeft(Expr left) { _left = left; } + @Override public Expr getRight() { return _right; } + @Override public void setRight(Expr right) { _right = right; } @@ -1753,14 +1753,17 @@ public class Expressionator return _expr.isConstant(); } + @Override public OpType getOp() { return _op; } + @Override public Expr getRight() { return _expr; } + @Override public void setRight(Expr right) { _expr = right; } @@ -1859,14 +1862,17 @@ public class Expressionator return _expr.isConstant(); } + @Override public OpType getOp() { return _op; } + @Override public Expr getLeft() { return _expr; } + @Override public void setLeft(Expr left) { _expr = left; } @@ -1991,10 +1997,12 @@ public class Expressionator return _expr.isConstant() && areConstant(_startRangeExpr, _endRangeExpr); } + @Override public Expr getRight() { return _endRangeExpr; } + @Override public void setRight(Expr right) { _endRangeExpr = right; } @@ -2037,22 +2045,27 @@ public class Expressionator _expr = expr; } + @Override public String toDebugString(LocaleContext ctx) { return _expr.toDebugString(ctx); } + @Override public String toRawString() { return _rawExprStr; } + @Override public String toCleanString(LocaleContext ctx) { return _expr.toCleanString(ctx); } + @Override public boolean isConstant() { return _expr.isConstant(); } + @Override public void collectIdentifiers(Collection<Identifier> identifiers) { _expr.collectIdentifiers(identifiers); } @@ -2081,7 +2094,7 @@ public class Expressionator case DATE: case TIME: case DATE_TIME: - return val.getAsDateTime(ctx); + return val.getAsLocalDateTime(ctx); case LONG: return val.getAsLongInt(ctx); case DOUBLE: @@ -2117,6 +2130,7 @@ public class Expressionator _resultType = resultType; } + @Override public Object eval(EvalContext ctx) { return evalValue(_resultType, ctx); } @@ -2132,6 +2146,7 @@ public class Expressionator super(rawExprStr, expr); } + @Override public Object eval(EvalContext ctx) { return evalCondition(ctx); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index 2c475eb..6dfc2e3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -17,16 +17,31 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalField; +import java.time.temporal.WeekFields; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; import com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.NumericConfig; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; +import static com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.ExprBuf; /** * @@ -35,10 +50,21 @@ import com.healthmarketscience.jackcess.expr.Value; public class FormatUtil { public enum NumPatternType { - GENERAL, CURRENCY { + GENERAL, + CURRENCY { @Override protected void appendPrefix(StringBuilder fmt) { - fmt.append("\u00A4"); + fmt.append('\u00A4'); + } + @Override + protected boolean useParensForNegatives(NumericConfig cfg) { + return cfg.useParensForCurrencyNegatives(); + } + }, + EURO { + @Override + protected void appendPrefix(StringBuilder fmt) { + fmt.append('\u20AC'); } @Override protected boolean useParensForNegatives(NumericConfig cfg) { @@ -48,7 +74,7 @@ public class FormatUtil PERCENT { @Override protected void appendSuffix(StringBuilder fmt) { - fmt.append("%"); + fmt.append('%'); } }, SCIENTIFIC { @@ -67,10 +93,29 @@ public class FormatUtil } } + private enum TextCase { + NONE, + UPPER { + @Override public char apply(char c) { + return Character.toUpperCase(c); + } + }, + LOWER { + @Override public char apply(char c) { + return Character.toLowerCase(c); + } + }; + + public char apply(char c) { + return c; + } + } + private static final Map<String,Fmt> PREDEF_FMTS = new HashMap<String,Fmt>(); static { - PREDEF_FMTS.put("General Date", new GenPredefDateFmt()); + PREDEF_FMTS.put("General Date", args -> ValueSupport.toValue( + args.coerceToDateTimeValue().getAsString())); PREDEF_FMTS.put("Long Date", new PredefDateFmt(TemporalConfig.Type.LONG_DATE)); PREDEF_FMTS.put("Medium Date", @@ -84,12 +129,11 @@ public class FormatUtil PREDEF_FMTS.put("Short Time", new PredefDateFmt(TemporalConfig.Type.SHORT_TIME)); - PREDEF_FMTS.put("General Number", new GenPredefNumberFmt()); + PREDEF_FMTS.put("General Number", args -> ValueSupport.toValue( + args.coerceToNumberValue().getAsString())); PREDEF_FMTS.put("Currency", new PredefNumberFmt(NumericConfig.Type.CURRENCY)); - // FIXME ? - // PREDEF_FMTS.put("Euro", - // new PredefNumberFmt(???)); + PREDEF_FMTS.put("Euro", new PredefNumberFmt(NumericConfig.Type.EURO)); PREDEF_FMTS.put("Fixed", new PredefNumberFmt(NumericConfig.Type.FIXED)); PREDEF_FMTS.put("Standard", @@ -103,23 +147,966 @@ public class FormatUtil PREDEF_FMTS.put("On/Off", new PredefBoolFmt("On", "Off")); } + private static final Fmt NULL_FMT = args -> ValueSupport.EMPTY_STR_VAL; + + private static final char QUOTE_CHAR = '"'; + private static final char ESCAPE_CHAR = '\\'; + private static final char LEFT_ALIGN_CHAR = '!'; + private static final char START_COLOR_CHAR = '['; + private static final char END_COLOR_CHAR = ']'; + private static final char CHOICE_SEP_CHAR = ';'; + // this only seems to be useful if you have fixed length string fields which + // isn't a normal thing in ms access + private static final char FILL_ESCAPE_CHAR = '*'; + private static final char REQ_PLACEHOLDER_CHAR = '@'; + private static final char OPT_PLACEHOLDER_CHAR = '&'; + private static final char TO_UPPER_CHAR = '>'; + private static final char TO_LOWER_CHAR = '<'; + private static final char DT_LIT_COLON_CHAR = ':'; + private static final char DT_LIT_SLASH_CHAR = '/'; + private static final char SINGLE_QUOTE_CHAR = '\''; + private static final char EXP_E_CHAR = 'E'; + private static final char EXP_e_CHAR = 'e'; + private static final char PLUS_CHAR = '+'; + private static final char MINUS_CHAR = '-'; + private static final char REQ_DIGIT_CHAR = '0'; + private static final int NO_CHAR = -1; + + private static final byte FCT_UNKNOWN = 0; + private static final byte FCT_LITERAL = 1; + private static final byte FCT_GENERAL = 2; + private static final byte FCT_DATE = 3; + private static final byte FCT_NUMBER = 4; + private static final byte FCT_TEXT = 5; + + private static final byte[] FORMAT_CODE_TYPES = new byte[127]; + static { + setFormatCodeTypes(" $+-()", FCT_LITERAL); + setFormatCodeTypes("\"!*\\[];", FCT_GENERAL); + setFormatCodeTypes(":/cdwmqyhnstampmAMPM", FCT_DATE); + setFormatCodeTypes(".,0#%Ee", FCT_NUMBER); + setFormatCodeTypes("@&<>", FCT_TEXT); + } + + @FunctionalInterface + interface Fmt { + public Value format(Args args); + } + + @FunctionalInterface + interface DateFormatBuilder { + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType); + } + + private static final DateFormatBuilder PARTIAL_PREFIX = + (dtfb, args, hasAmPm, dtType) -> { + throw new UnsupportedOperationException(); + }; + + private static final Map<String,DateFormatBuilder> DATE_FMT_BUILDERS = + new HashMap<>(); + static { + DATE_FMT_BUILDERS.put("c", + (dtfb, args, hasAmPm, dtType) -> + dtfb.append(ValueSupport.getDateFormatForType( + args._ctx, dtType))); + DATE_FMT_BUILDERS.put("d", new SimpleDFB("d")); + DATE_FMT_BUILDERS.put("dd", new SimpleDFB("dd")); + DATE_FMT_BUILDERS.put("ddd", new SimpleDFB("eee")); + DATE_FMT_BUILDERS.put("dddd", new SimpleDFB("eeee")); + DATE_FMT_BUILDERS.put("ddddd", new PredefDFB(TemporalConfig.Type.SHORT_DATE)); + DATE_FMT_BUILDERS.put("dddddd", new PredefDFB(TemporalConfig.Type.LONG_DATE)); + DATE_FMT_BUILDERS.put("w", new WeekBasedDFB() { + @Override + protected TemporalField getField(WeekFields weekFields) { + return weekFields.dayOfWeek(); + } + }); + DATE_FMT_BUILDERS.put("ww", new WeekBasedDFB() { + @Override + protected TemporalField getField(WeekFields weekFields) { + return weekFields.weekOfWeekBasedYear(); + } + }); + DATE_FMT_BUILDERS.put("m", new SimpleDFB("L")); + DATE_FMT_BUILDERS.put("mm", new SimpleDFB("LL")); + DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("MMM")); + DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("MMMM")); + DATE_FMT_BUILDERS.put("q", new SimpleDFB("Q")); + DATE_FMT_BUILDERS.put("y", new SimpleDFB("D")); + DATE_FMT_BUILDERS.put("yy", new SimpleDFB("yy")); + DATE_FMT_BUILDERS.put("yyyy", new SimpleDFB("yyyy")); + DATE_FMT_BUILDERS.put("h", new HourlyDFB("h", "H")); + DATE_FMT_BUILDERS.put("hh", new HourlyDFB("hh", "HH")); + DATE_FMT_BUILDERS.put("n", new SimpleDFB("m")); + DATE_FMT_BUILDERS.put("nn", new SimpleDFB("mm")); + DATE_FMT_BUILDERS.put("s", new SimpleDFB("s")); + DATE_FMT_BUILDERS.put("ss", new SimpleDFB("ss")); + DATE_FMT_BUILDERS.put("ttttt", new PredefDFB(TemporalConfig.Type.LONG_TIME)); + DATE_FMT_BUILDERS.put("AM/PM", new AmPmDFB("AM", "PM")); + DATE_FMT_BUILDERS.put("am/pm", new AmPmDFB("am", "pm")); + DATE_FMT_BUILDERS.put("A/P", new AmPmDFB("A", "P")); + DATE_FMT_BUILDERS.put("a/p", new AmPmDFB("a", "p")); + DATE_FMT_BUILDERS.put("AMPM", + (dtfb, args, hasAmPm, dtType) -> { + String[] amPmStrs = args._ctx.getTemporalConfig().getAmPmStrings(); + new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args, hasAmPm, dtType); + } + ); + fillInPartialPrefixes(); + } + + private static final int NF_POS_IDX = 0; + private static final int NF_NEG_IDX = 1; + private static final int NF_ZERO_IDX = 2; + private static final int NF_NULL_IDX = 3; + private static final int NUM_NF_FMTS = 4; + + private static final NumberFormatter.NotationType[] NO_EXP_TYPES = + new NumberFormatter.NotationType[NUM_NF_FMTS]; + private static final boolean[] NO_FMT_TYPES = new boolean[NUM_NF_FMTS]; + + + private static final class Args + { + private final EvalContext _ctx; + private Value _expr; + private final int _firstDay; + private final int _firstWeekType; + + private Args(EvalContext ctx, Value expr, int firstDay, int firstWeekType) { + _ctx = ctx; + _expr = expr; + _firstDay = firstDay; + _firstWeekType = firstWeekType; + } + + public boolean isNullOrEmptyString() { + return(_expr.isNull() || + // only a string value could ever be an empty string + (_expr.getType().isString() && getAsString().isEmpty())); + } + + public boolean maybeCoerceToEmptyString() { + if(isNullOrEmptyString()) { + // ensure that we have a non-null value when formatting (null acts + // like empty string) + _expr = ValueSupport.EMPTY_STR_VAL; + return true; + } + return false; + } + + public Args coerceToDateTimeValue() { + if(!_expr.getType().isTemporal()) { + + // format coerces boolean strings to numbers + Value boolExpr = null; + if(_expr.getType().isString() && + ((boolExpr = maybeGetStringAsBooleanValue()) != null)) { + _expr = boolExpr; + } + + // StringValue already handles most String -> Number -> Date/Time, so + // most other convertions work here (and failures are thrown so that + // default handling kicks in) + _expr = _expr.getAsDateTimeValue(_ctx); + } + return this; + } + + public Args coerceToNumberValue() { + if(!_expr.getType().isNumeric()) { + if(_expr.getType().isString()) { + + // format coerces "true"/"false" to boolean values + Value boolExpr = maybeGetStringAsBooleanValue(); + if(boolExpr != null) { + _expr = boolExpr; + } else { + BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(_ctx, _expr); + if(bd != null) { + _expr = ValueSupport.toValue(bd); + } else { + // convert to date to number. this doesn't happen as part of the + // default value coercion behavior, but the format method tries + // harder + Value maybe = DefaultFunctions.maybeGetAsDateTimeValue( + _ctx, _expr); + if(maybe != null) { + _expr = ValueSupport.toValue(maybe.getAsDouble(_ctx)); + } else { + // string which can't be converted to number force failure + // here so default formatting will kick in + throw new EvalException("invalid number value"); + } + } + } + } else { + // convert date to number + _expr = ValueSupport.toValue(_expr.getAsDouble(_ctx)); + } + } + return this; + } + + private Value maybeGetStringAsBooleanValue() { + // format coerces "true"/"false" to boolean values + String val = getAsString(); + if("true".equalsIgnoreCase(val)) { + return ValueSupport.TRUE_VAL; + } + if("false".equalsIgnoreCase(val)) { + return ValueSupport.FALSE_VAL; + } + return null; + } + + public BigDecimal getAsBigDecimal() { + coerceToNumberValue(); + return _expr.getAsBigDecimal(_ctx); + } + + public LocalDateTime getAsLocalDateTime() { + coerceToDateTimeValue(); + return _expr.getAsLocalDateTime(_ctx); + } + + public boolean getAsBoolean() { + // even though string values have a "boolean" value, for formatting, + // strings which don't convert to valid boolean/number/date are just + // returned as is. so we use coerceToNumberValue to force the exception + // to be thrown which results in the "default" formatting behavior. + coerceToNumberValue(); + return _expr.getAsBoolean(_ctx); + } + + public String getAsString() { + return _expr.getAsString(_ctx); + } + } + private FormatUtil() {} public static Value format(EvalContext ctx, Value expr, String fmtStr, int firstDay, int firstWeekType) { - Fmt predefFmt = PREDEF_FMTS.get(fmtStr); - if(predefFmt != null) { - if(expr.isNull()) { - // predefined formats return null for null - return ValueSupport.NULL_VAL; + try { + Args args = new Args(ctx, expr, firstDay, firstWeekType); + + Fmt predefFmt = PREDEF_FMTS.get(fmtStr); + if(predefFmt != null) { + if(args.isNullOrEmptyString()) { + // predefined formats return empty string for null + return ValueSupport.EMPTY_STR_VAL; + } + return predefFmt.format(args); + } + + // TODO implement caching for custom formats? put into Bindings. use + // special "cache" prefix to know which caches to clear when evalconfig + // is altered (could also cache other Format* functions) + + return parseCustomFormat(fmtStr, args).format(args); + + } catch(EvalException ee) { + // values which cannot be formatted as the target type are just + // returned "as is" + return expr; + } + } + + private static Fmt parseCustomFormat(String fmtStr, Args args) { + + ExprBuf buf = new ExprBuf(fmtStr, null); + + // do partial pass to determine what type of format this is + byte curFormatType = determineFormatType(buf); + + // reset buffer for real parse + buf.reset(0); + + switch(curFormatType) { + case FCT_GENERAL: + return parseCustomGeneralFormat(buf, args); + case FCT_DATE: + return parseCustomDateFormat(buf, args); + case FCT_NUMBER: + return parseCustomNumberFormat(buf, args); + case FCT_TEXT: + return parseCustomTextFormat(buf, args); + default: + throw new EvalException("Invalid format type " + curFormatType); + } + } + + private static byte determineFormatType(ExprBuf buf) { + + while(buf.hasNext()) { + char c = buf.next(); + byte fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_UNKNOWN: + case FCT_LITERAL: + // meaningless, ignore for now + break; + case FCT_GENERAL: + switch(c) { + case QUOTE_CHAR: + parseQuotedString(buf); + break; + case START_COLOR_CHAR: + parseColorString(buf); + break; + case ESCAPE_CHAR: + case FILL_ESCAPE_CHAR: + if(buf.hasNext()) { + buf.next(); + } + break; + default: + // meaningless, ignore for now + } + break; + case FCT_DATE: + case FCT_NUMBER: + case FCT_TEXT: + // found specific type + return fmtType; + default: + throw new EvalException("Invalid format type " + fmtType); + } + } + + // no specific type + return FCT_GENERAL; + } + + private static Fmt parseCustomGeneralFormat(ExprBuf buf, Args args) { + + // a "general" format is actually a "yes/no" format which functions almost + // exactly like a number format (without any number format specific chars) + + StringBuilder sb = new StringBuilder(); + String[] fmtStrs = new String[NUM_NF_FMTS]; + int fmtIdx = 0; + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case LEFT_ALIGN_CHAR: + // no effect + break; + case QUOTE_CHAR: + parseQuotedString(buf, sb); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + sb.append(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // yes/no (number) format supports up to 4 formats: pos, neg, zero, + // null. after that, ignore the rest + if(fmtIdx == (NUM_NF_FMTS - 1)) { + // last possible format, ignore remaining + break BUF_LOOP; + } + addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + break; + default: + sb.append(c); + } + break; + default: + sb.append(c); + } + } + + // fill in remaining formats + while(fmtIdx < NUM_NF_FMTS) { + addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + } + + return new CustomGeneralFmt( + ValueSupport.toValue(fmtStrs[NF_POS_IDX]), + ValueSupport.toValue(fmtStrs[NF_NEG_IDX]), + ValueSupport.toValue(fmtStrs[NF_ZERO_IDX]), + ValueSupport.toValue(fmtStrs[NF_NULL_IDX])); + } + + private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx, + StringBuilder sb) + { + addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, NO_FMT_TYPES, + fmtIdx, sb); + } + + private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) { + + // keep track of some extra state while parsing the format, whether or not + // there was an am/pm pattern and whether or not there was a general + // date/time pattern + boolean[] fmtState = new boolean[]{false, false}; + List<DateFormatBuilder> dfbs = new ArrayList<>(); + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case QUOTE_CHAR: + String str = parseQuotedString(buf); + dfbs.add((dtfb, argsParam, hasAmPmParam, dtType) -> + dtfb.appendLiteral(str)); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + dfbs.add(buildLiteralCharDFB(buf.next())); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // date/time doesn't use multiple pattern choices, but it does + // respect the char. ignore everything after the first choice + break BUF_LOOP; + default: + dfbs.add(buildLiteralCharDFB(c)); + } + break; + case FCT_DATE: + parseCustomDateFormatPattern(c, buf, dfbs, fmtState, args); + break; + default: + dfbs.add(buildLiteralCharDFB(c)); + } + } + + boolean hasAmPm = fmtState[0]; + boolean hasGeneralFormat = fmtState[1]; + if(!hasGeneralFormat) { + // simple situation, one format for every value + DateTimeFormatter dtf = createDateTimeFormatter(dfbs, args, hasAmPm, null); + return new CustomFmt(argsParam -> ValueSupport.toValue( + dtf.format(argsParam.getAsLocalDateTime()))); + } + + // we need separate formatters for date, time, and date/time values + DateTimeFormatter dateFmt = createDateTimeFormatter(dfbs, args, hasAmPm, + Value.Type.DATE); + DateTimeFormatter timeFmt = createDateTimeFormatter(dfbs, args, hasAmPm, + Value.Type.TIME); + DateTimeFormatter dtFmt = createDateTimeFormatter(dfbs, args, hasAmPm, + Value.Type.DATE_TIME); + + return new CustomFmt(argsParam -> formatDateTime( + argsParam, dateFmt, timeFmt, dtFmt)); + } + + private static void parseCustomDateFormatPattern( + char c, ExprBuf buf, List<DateFormatBuilder> dfbs, + boolean[] fmtState, Args args) { + + if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) { + // date/time literal char, nothing more to do + dfbs.add(buildLiteralCharDFB(c)); + return; + } + + StringBuilder sb = buf.getScratchBuffer(); + sb.append(c); + + char firstChar = c; + int firstPos = buf.curPos(); + String bestMatchPat = sb.toString(); + + DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(bestMatchPat); + int bestPos = firstPos; + while(buf.hasNext()) { + sb.append(buf.next()); + String tmpPat = sb.toString(); + DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(tmpPat); + if(dfb == null) { + // no more possible matches + break; + } + if(dfb != PARTIAL_PREFIX) { + // this is the longest, valid pattern we have seen so far + bestMatch = dfb; + bestPos = buf.curPos(); + bestMatchPat = tmpPat; + } + } + + if(bestMatch != PARTIAL_PREFIX) { + + // apply valid pattern + buf.reset(bestPos); + dfbs.add(bestMatch); + + switch(firstChar) { + case 'a': + case 'A': + // this was an am/pm pattern + fmtState[0] = true; + break; + case 'c': + // this was a general date/time format + fmtState[1] = true; + break; + default: + // don't care + } + + } else { + + // just consume the first char + buf.reset(firstPos); + dfbs.add(buildLiteralCharDFB(firstChar)); + } + } + + private static DateFormatBuilder buildLiteralCharDFB(char c) { + return (dtfb, args, hasAmPm, dtType) -> dtfb.appendLiteral(c); + } + + private static DateTimeFormatter createDateTimeFormatter( + List<DateFormatBuilder> dfbs, Args args, boolean hasAmPm, + Value.Type dtType) + { + DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder(); + dfbs.forEach(d -> d.build(dtfb, args, hasAmPm, dtType)); + return dtfb.toFormatter(args._ctx.getTemporalConfig().getLocale()); + } + + private static Value formatDateTime( + Args args, DateTimeFormatter dateFmt, + DateTimeFormatter timeFmt, DateTimeFormatter dtFmt) + { + LocalDateTime ldt = args.getAsLocalDateTime(); + DateTimeFormatter fmt = null; + switch(args._expr.getType()) { + case DATE: + fmt = dateFmt; + break; + case TIME: + fmt = timeFmt; + break; + default: + fmt = dtFmt; + } + + return ValueSupport.toValue(fmt.format(ldt)); + } + + private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) { + + StringBuilder sb = new StringBuilder(); + String[] fmtStrs = new String[NUM_NF_FMTS]; + int fmtIdx = 0; + StringBuilder pendingLiteral = new StringBuilder(); + NumberFormatter.NotationType[] expTypes = + new NumberFormatter.NotationType[NUM_NF_FMTS]; + boolean[] hasFmts = new boolean[NUM_NF_FMTS]; + boolean[] hasReqDigit = new boolean[NUM_NF_FMTS]; + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case LEFT_ALIGN_CHAR: + // no effect + break; + case QUOTE_CHAR: + parseQuotedString(buf, pendingLiteral); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + pendingLiteral.append(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // number format supports up to 4 formats: pos, neg, zero, null. + // after that, ignore the rest + if(fmtIdx == (NUM_NF_FMTS - 1)) { + // last possible format, ignore remaining + break BUF_LOOP; + } + flushPendingNumberLiteral(pendingLiteral, sb); + addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + fmtIdx++, sb); + break; + default: + pendingLiteral.append(c); + } + break; + case FCT_NUMBER: + hasFmts[fmtIdx] = true; + switch(c) { + case EXP_E_CHAR: + int signChar = buf.peekNext(); + if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) { + buf.next(); + expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ? + NumberFormatter.NotationType.EXP_E_PLUS : + NumberFormatter.NotationType.EXP_E_MINUS); + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(EXP_E_CHAR); + } else { + pendingLiteral.append(c); + } + break; + case EXP_e_CHAR: + signChar = buf.peekNext(); + if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) { + buf.next(); + expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ? + NumberFormatter.NotationType.EXP_e_PLUS : + NumberFormatter.NotationType.EXP_e_MINUS); + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(EXP_E_CHAR); + } else { + pendingLiteral.append(c); + } + break; + case REQ_DIGIT_CHAR: + hasReqDigit[fmtIdx] = true; + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(c); + break; + default: + // most number format chars pass straight through + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(c); + } + break; + default: + pendingLiteral.append(c); + } + } + + // fill in remaining formats + while(fmtIdx < NUM_NF_FMTS) { + flushPendingNumberLiteral(pendingLiteral, sb); + addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + fmtIdx++, sb); + } + + return new CustomNumberFmt( + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_POS_IDX, false, args, buf), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_NEG_IDX, false, args, buf), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_ZERO_IDX, true, args, buf), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_NULL_IDX, true, args, buf)); + } + + private static void addCustomNumberFormat( + String[] fmtStrs, NumberFormatter.NotationType[] expTypes, + boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx, StringBuilder sb) + { + if(sb.length() == 0) { + // do special empty format handling on a per-format-type basis + switch(fmtIdx) { + case NF_NEG_IDX: + // re-use "pos" format + sb.append('-').append(fmtStrs[NF_POS_IDX]); + expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX]; + hasFmts[NF_NEG_IDX] = hasFmts[NF_POS_IDX]; + hasReqDigit[NF_NEG_IDX] = hasReqDigit[NF_POS_IDX]; + break; + case NF_ZERO_IDX: + // re-use "pos" format + sb.append(fmtStrs[NF_POS_IDX]); + expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX]; + hasFmts[NF_ZERO_IDX] = hasFmts[NF_POS_IDX]; + hasReqDigit[NF_ZERO_IDX] = hasReqDigit[NF_POS_IDX]; + break; + default: + // use empty string result + } + } + + fmtStrs[fmtIdx] = sb.toString(); + sb.setLength(0); + } + + private static void flushPendingNumberLiteral( + StringBuilder pendingLiteral, StringBuilder sb) { + if(pendingLiteral.length() == 0) { + return; + } + + if((pendingLiteral.length() == 1) && + pendingLiteral.charAt(0) == SINGLE_QUOTE_CHAR) { + // handle standalone single quote + sb.append(SINGLE_QUOTE_CHAR).append(SINGLE_QUOTE_CHAR); + pendingLiteral.setLength(0); + return; + } + + sb.append(SINGLE_QUOTE_CHAR); + int startPos = sb.length(); + sb.append(pendingLiteral); + + // we need to quote any single quotes in the literal string + for(int i = startPos; i < sb.length(); ++i) { + char c = sb.charAt(i); + if(c == SINGLE_QUOTE_CHAR) { + sb.insert(++i, SINGLE_QUOTE_CHAR); + } + } + + sb.append(SINGLE_QUOTE_CHAR); + pendingLiteral.setLength(0); + } + + private static BDFormat createCustomNumberFormat( + String[] fmtStrs, NumberFormatter.NotationType[] expTypes, + boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx, + boolean isZeroFmt, Args args, ExprBuf buf) { + + String fmtStr = fmtStrs[fmtIdx]; + if(!hasFmts[fmtIdx]) { + // convert the literal string to a dummy number format + if(fmtStr.length() > 0) { + // strip quoting + StringBuilder sb = buf.getScratchBuffer().append(fmtStr) + .deleteCharAt(fmtStr.length() - 1) + .deleteCharAt(0); + if(sb.length() > 0) { + for(int i = 0; i < sb.length(); ++i) { + if(sb.charAt(i) == SINGLE_QUOTE_CHAR) { + // delete next single quote char + sb.deleteCharAt(++i); + } + } + } else { + // this was a single, literal single quote + sb.append(SINGLE_QUOTE_CHAR); + } + fmtStr = sb.toString(); + } + return new LiteralBDFormat(fmtStr); + } + + NumberFormatter.NotationType expType = expTypes[fmtIdx]; + DecimalFormat df = args._ctx.createDecimalFormat(fmtStr); + + if(df.getMaximumFractionDigits() > 0) { + // if the decimal is included in the format, access always shows it + df.setDecimalSeparatorAlwaysShown(true); + } + + if(expType != null) { + NumberFormat nf = new NumberFormatter.ScientificFormat(df, expType); + if(isZeroFmt) { + return new LiteralBDFormat(nf.format(BigDecimal.ZERO)); + } + return new BaseBDFormat(nf); + } + + if(!hasReqDigit[fmtIdx]) { + // java likes to force extra 0's while access doesn't + df.setMinimumIntegerDigits(0); + } + + if(isZeroFmt) { + + String zeroVal = df.format(BigDecimal.ZERO); + if(!hasReqDigit[fmtIdx]) { + // java forces a 0 but access doesn't. delete any 0 chars which were + // inserted by the java format + int prefLen = df.getPositivePrefix().length(); + int len = zeroVal.length() - df.getPositiveSuffix().length(); + StringBuilder sb = buf.getScratchBuffer().append(zeroVal); + for(int i = len - 1; i >= prefLen; --i) { + if(sb.charAt(i) == '0') { + sb.deleteCharAt(i); + } + } + zeroVal = sb.toString(); + } + + return new LiteralBDFormat(zeroVal); + } + + return new DecimalBDFormat(df); + } + + private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) { + + Fmt fmt = null; + + List<BiConsumer<StringBuilder,CharSource>> subFmts = new ArrayList<>(); + int numPlaceholders = 0; + boolean rightAligned = true; + TextCase textCase = TextCase.NONE; + StringBuilder pendingLiteral = new StringBuilder(); + boolean hasFmtChars = false; + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + hasFmtChars = true; + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case LEFT_ALIGN_CHAR: + rightAligned = false; + break; + case QUOTE_CHAR: + parseQuotedString(buf, pendingLiteral); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + pendingLiteral.append(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // text format supports up to 2 formats: normal and empty/null. + // after that, ignore the rest + if(fmt != null) { + // ignore remaining format + break BUF_LOOP; + } + flushPendingTextLiteral(pendingLiteral, subFmts); + fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned, + textCase); + // reset for next format + subFmts = new ArrayList<>(); + numPlaceholders = 0; + rightAligned = true; + textCase = TextCase.NONE; + hasFmtChars = false; + break; + default: + pendingLiteral.append(c); + } + break; + case FCT_TEXT: + switch(c) { + case REQ_PLACEHOLDER_CHAR: + flushPendingTextLiteral(pendingLiteral, subFmts); + ++numPlaceholders; + subFmts.add((sb,cs) -> { + int tmp = cs.next(); + sb.append((tmp != NO_CHAR) ? (char)tmp : ' '); + }); + break; + case OPT_PLACEHOLDER_CHAR: + flushPendingTextLiteral(pendingLiteral, subFmts); + ++numPlaceholders; + subFmts.add((sb,cs) -> { + int tmp = cs.next(); + if(tmp != NO_CHAR) { + sb.append((char)tmp); + } + }); + break; + case TO_UPPER_CHAR: + // an uppper and lower symbol cancel each other out + textCase = ((textCase == TextCase.LOWER) ? + TextCase.NONE : TextCase.UPPER); + break; + case TO_LOWER_CHAR: + // an uppper and lower symbol cancel each other out + textCase = ((textCase == TextCase.UPPER) ? + TextCase.NONE : TextCase.LOWER); + break; + default: + pendingLiteral.append(c); + } + break; + default: + pendingLiteral.append(c); } - return predefFmt.format(ctx, expr, null, firstDay, firstWeekType); } - // FIXME, - throw new UnsupportedOperationException(); + flushPendingTextLiteral(pendingLiteral, subFmts); + + Fmt emptyFmt = null; + if(fmt == null) { + fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned, + textCase); + emptyFmt = NULL_FMT; + } else { + emptyFmt = (hasFmtChars ? + new CharSourceFmt(subFmts, numPlaceholders, rightAligned, + textCase) : + NULL_FMT); + } + + return new CustomFmt(fmt, emptyFmt); + } + + private static void flushPendingTextLiteral( + StringBuilder pendingLiteral, + List<BiConsumer<StringBuilder,CharSource>> subFmts) { + if(pendingLiteral.length() == 0) { + return; + } + + String literal = pendingLiteral.toString(); + pendingLiteral.setLength(0); + subFmts.add((sb, cs) -> sb.append(literal)); } public static String createNumberFormatPattern( @@ -153,16 +1140,45 @@ public class FormatUtil return fmt.toString(); } + private static byte getFormatCodeType(char c) { + if((c >= 0) && (c < 127)) { + return FORMAT_CODE_TYPES[c]; + } + return FCT_UNKNOWN; + } - private static abstract class Fmt - { - // FIXME, no null - // FIXME, need fmtStr? - public abstract Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType); + private static void setFormatCodeTypes(String chars, byte type) { + for(char c : chars.toCharArray()) { + FORMAT_CODE_TYPES[c] = type; + } + } + + private static String parseQuotedString(ExprBuf buf) { + return ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true); + } + + private static void parseQuotedString(ExprBuf buf, StringBuilder sb) { + ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true, sb); + } + + private static String parseColorString(ExprBuf buf) { + return ExpressionTokenizer.parseStringUntil( + buf, START_COLOR_CHAR, END_COLOR_CHAR, false); + } + + private static void fillInPartialPrefixes() { + List<String> validPrefixes = new ArrayList<>(DATE_FMT_BUILDERS.keySet()); + for(String validPrefix : validPrefixes) { + int len = validPrefix.length(); + while(len > 1) { + --len; + validPrefix = validPrefix.substring(0, len); + DATE_FMT_BUILDERS.putIfAbsent(validPrefix, PARTIAL_PREFIX); + } + } } - private static class PredefDateFmt extends Fmt + private static final class PredefDateFmt implements Fmt { private final TemporalConfig.Type _type; @@ -171,31 +1187,14 @@ public class FormatUtil } @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - DateFormat sdf = ctx.createDateFormat( - ctx.getTemporalConfig().getDateTimeFormat(_type)); - return ValueSupport.toValue(sdf.format(expr.getAsDateTime(ctx))); + public Value format(Args args) { + DateTimeFormatter dtf = args._ctx.createDateFormatter( + args._ctx.getTemporalConfig().getDateTimeFormat(_type)); + return ValueSupport.toValue(dtf.format(args.getAsLocalDateTime())); } } - private static class GenPredefDateFmt extends Fmt - { - @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - Value tempExpr = expr; - if(!expr.getType().isTemporal()) { - Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(ctx, expr); - if(maybe != null) { - tempExpr = maybe; - } - } - return ValueSupport.toValue(tempExpr.getAsString(ctx)); - } - } - - private static class PredefBoolFmt extends Fmt + private static final class PredefBoolFmt implements Fmt { private final Value _trueVal; private final Value _falseVal; @@ -206,13 +1205,23 @@ public class FormatUtil } @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - return(expr.getAsBoolean(ctx) ? _trueVal : _falseVal); + public Value format(Args args) { + return(args.getAsBoolean() ? _trueVal : _falseVal); } } - private static class PredefNumberFmt extends Fmt + private static abstract class BaseNumberFmt implements Fmt + { + @Override + public Value format(Args args) { + NumberFormat df = getNumberFormat(args); + return ValueSupport.toValue(df.format(args.getAsBigDecimal())); + } + + protected abstract NumberFormat getNumberFormat(Args args); + } + + private static final class PredefNumberFmt extends BaseNumberFmt { private final NumericConfig.Type _type; @@ -221,51 +1230,381 @@ public class FormatUtil } @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - DecimalFormat df = ctx.createDecimalFormat( - ctx.getNumericConfig().getNumberFormat(_type)); - return ValueSupport.toValue(df.format(expr.getAsBigDecimal(ctx))); + protected NumberFormat getNumberFormat(Args args) { + return args._ctx.createDecimalFormat( + args._ctx.getNumericConfig().getNumberFormat(_type)); } } - private static class GenPredefNumberFmt extends Fmt + private static final class ScientificPredefNumberFmt extends BaseNumberFmt { @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - Value numExpr = expr; - if(!expr.getType().isNumeric()) { - if(expr.getType().isString()) { - BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(ctx, expr); - if(bd != null) { - numExpr = ValueSupport.toValue(bd); - } else { - // convert to date to number - Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(ctx, expr); - if(maybe != null) { - numExpr = ValueSupport.toValue(maybe.getAsDouble(ctx)); - } - } - } else { - // convert date to number - numExpr = ValueSupport.toValue(expr.getAsDouble(ctx)); + protected NumberFormat getNumberFormat(Args args) { + NumberFormat df = args._ctx.createDecimalFormat( + args._ctx.getNumericConfig().getNumberFormat( + NumericConfig.Type.SCIENTIFIC)); + df = new NumberFormatter.ScientificFormat(df); + return df; + } + } + + private static final class SimpleDFB implements DateFormatBuilder + { + private final String _pat; + + private SimpleDFB(String pat) { + _pat = pat; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { + dtfb.appendPattern(_pat); + } + } + + private static final class HourlyDFB implements DateFormatBuilder + { + private final String _pat12; + private final String _pat24; + + private HourlyDFB(String pat12, String pat24) { + _pat12 = pat12; + _pat24 = pat24; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtTypePm) { + // annoyingly the "hour" patterns are the same and depend on the + // existence of the am/pm pattern to determine how they function (12 vs + // 24 hour). + dtfb.appendPattern(hasAmPm ? _pat12 : _pat24); + } + } + + private static final class PredefDFB implements DateFormatBuilder + { + private final TemporalConfig.Type _type; + + private PredefDFB(TemporalConfig.Type type) { + _type = type; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { + dtfb.appendPattern(args._ctx.getTemporalConfig().getDateTimeFormat(_type)); + } + } + + private static abstract class WeekBasedDFB implements DateFormatBuilder + { + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { + dtfb.appendValue(getField(DefaultDateFunctions.weekFields( + args._firstDay, args._firstWeekType))); + } + + protected abstract TemporalField getField(WeekFields weekFields); + } + + private static final class AmPmDFB extends AbstractMap<Long,String> + implements DateFormatBuilder + { + private static final Long ZERO_KEY = 0L; + private final String _am; + private final String _pm; + + private AmPmDFB(String am, String pm) { + _am = am; + _pm = pm; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { + dtfb.appendText(ChronoField.AMPM_OF_DAY, this); + } + @Override + public int size() { + return 2; + } + @Override + public String get(Object key) { + return(ZERO_KEY.equals(key) ? _am : _pm); + } + @Override + public Set<Map.Entry<Long,String>> entrySet() { + return new AbstractSet<Map.Entry<Long,String>>() { + @Override + public int size() { + return 2; + } + @Override + public Iterator<Map.Entry<Long,String>> iterator() { + return Arrays.<Map.Entry<Long,String>>asList( + new AbstractMap.SimpleImmutableEntry<Long,String>(0L, _am), + new AbstractMap.SimpleImmutableEntry<Long,String>(1L, _pm)) + .iterator(); } + }; + } + } + + private static final class CustomFmt implements Fmt + { + private final Fmt _fmt; + private final Fmt _emptyFmt; + + private CustomFmt(Fmt fmt) { + this(fmt, NULL_FMT); + } + + private CustomFmt(Fmt fmt, Fmt emptyFmt) { + _fmt = fmt; + _emptyFmt = emptyFmt; + } + + @Override + public Value format(Args args) { + Fmt fmt = _fmt; + if(args.maybeCoerceToEmptyString()) { + fmt = _emptyFmt; } - return ValueSupport.toValue(numExpr.getAsString(ctx)); + return fmt.format(args); } } - private static class ScientificPredefNumberFmt extends Fmt + private static final class CharSourceFmt implements Fmt { + private final List<BiConsumer<StringBuilder,CharSource>> _subFmts; + private final int _numPlaceholders; + private final boolean _rightAligned; + private final TextCase _textCase; + + private CharSourceFmt(List<BiConsumer<StringBuilder,CharSource>> subFmts, + int numPlaceholders, boolean rightAligned, + TextCase textCase) { + _subFmts = subFmts; + _numPlaceholders = numPlaceholders; + _rightAligned = rightAligned; + _textCase = textCase; + } + @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - NumberFormat df = ctx.createDecimalFormat( - ctx.getNumericConfig().getNumberFormat( - NumericConfig.Type.SCIENTIFIC)); - df = new NumberFormatter.ScientificFormat(df); - return ValueSupport.toValue(df.format(expr.getAsBigDecimal(ctx))); + public Value format(Args args) { + CharSource cs = new CharSource(args.getAsString(), _numPlaceholders, + _rightAligned, _textCase); + StringBuilder sb = new StringBuilder(); + _subFmts.stream().forEach(fmt -> fmt.accept(sb, cs)); + cs.appendRemaining(sb); + return ValueSupport.toValue(sb.toString()); + } + } + + private static final class CharSource + { + private int _prefLen; + private final String _str; + private int _strPos; + private final TextCase _textCase; + + private CharSource(String str, int len, boolean rightAligned, + TextCase textCase) { + _str = str; + _textCase = textCase; + int strLen = str.length(); + if(len > strLen) { + if(rightAligned) { + _prefLen = len - strLen; + } + } else if(len < strLen) { + // it doesn't make sense to me, but the meaning of "right aligned" + // seems to flip when the string is longer than the format length + if(!rightAligned) { + _strPos = strLen - len; + } + } + } + + public int next() { + if(_prefLen > 0) { + --_prefLen; + return NO_CHAR; + } + if(_strPos < _str.length()) { + return _textCase.apply(_str.charAt(_strPos++)); + } + return NO_CHAR; + } + + public void appendRemaining(StringBuilder sb) { + int strLen = _str.length(); + while(_strPos < strLen) { + sb.append(_textCase.apply(_str.charAt(_strPos++))); + } + } + } + + private static abstract class BaseCustomNumberFmt implements Fmt + { + @Override + public Value format(Args args) { + if(args._expr.isNull()) { + return formatNull(args); + } + + BigDecimal bd = args.getAsBigDecimal(); + int cmp = BigDecimal.ZERO.compareTo(bd); + + return ((cmp < 0) ? formatPos(bd, args) : + ((cmp > 0) ? formatNeg(bd, args) : + formatZero(bd, args))); + } + + protected abstract Value formatNull(Args args); + protected abstract Value formatPos(BigDecimal bd, Args args); + protected abstract Value formatNeg(BigDecimal bd, Args args); + protected abstract Value formatZero(BigDecimal bd, Args args); + } + + private static final class CustomGeneralFmt extends BaseCustomNumberFmt + { + private final Value _posVal; + private final Value _negVal; + private final Value _zeroVal; + private final Value _nullVal; + + private CustomGeneralFmt(Value posVal, Value negVal, + Value zeroVal, Value nullVal) { + _posVal = posVal; + _negVal = negVal; + _zeroVal = zeroVal; + _nullVal = nullVal; + } + + @Override + protected Value formatNull(Args args) { + return _nullVal; + } + @Override + protected Value formatPos(BigDecimal bd, Args args) { + return _posVal; + } + @Override + protected Value formatNeg(BigDecimal bd, Args args) { + return _negVal; + } + @Override + protected Value formatZero(BigDecimal bd, Args args) { + return _zeroVal; + } + } + + private static final class CustomNumberFmt extends BaseCustomNumberFmt + { + private final BDFormat _posFmt; + private final BDFormat _negFmt; + private final BDFormat _zeroFmt; + private final BDFormat _nullFmt; + + private CustomNumberFmt(BDFormat posFmt, BDFormat negFmt, + BDFormat zeroFmt, BDFormat nullFmt) { + _posFmt = posFmt; + _negFmt = negFmt; + _zeroFmt = zeroFmt; + _nullFmt = nullFmt; + } + + private Value formatMaybeZero(BigDecimal bd, BDFormat fmt) { + // in theory we want to use the given format. however, if, due to + // rounding, we end up with a number equivalent to zero, then we fall + // back to the zero format. if we are using scientific notation, + // however, then don't worry about this + int maxDecDigits = fmt.getMaxDecimalDigits(); + if(maxDecDigits < bd.scale()) { + bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE); + if(BigDecimal.ZERO.compareTo(bd) == 0) { + // fall back to zero format + fmt = _zeroFmt; + } + } + + return ValueSupport.toValue(fmt.format(bd)); + } + + @Override + protected Value formatNull(Args args) { + return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO)); + } + @Override + protected Value formatPos(BigDecimal bd, Args args) { + return formatMaybeZero(bd, _posFmt); + } + @Override + protected Value formatNeg(BigDecimal bd, Args args) { + return formatMaybeZero(bd.negate(), _negFmt); + } + @Override + protected Value formatZero(BigDecimal bd, Args args) { + return ValueSupport.toValue(_zeroFmt.format(bd)); + } + } + + private static abstract class BDFormat + { + public int getMaxDecimalDigits() { + return Integer.MAX_VALUE; + } + + public abstract String format(BigDecimal bd); + } + + private static final class LiteralBDFormat extends BDFormat + { + private final String _str; + + private LiteralBDFormat(String str) { + _str = str; + } + + @Override + public String format(BigDecimal bd) { + return _str; + } + } + + private static class BaseBDFormat extends BDFormat + { + private final NumberFormat _nf; + + private BaseBDFormat(NumberFormat nf) { + _nf = nf; + } + + @Override + public String format(BigDecimal bd) { + return _nf.format(bd); + } + } + + private static final class DecimalBDFormat extends BaseBDFormat + { + private final int _maxDecDigits; + + private DecimalBDFormat(DecimalFormat df) { + super(df); + + int maxDecDigits = df.getMaximumFractionDigits(); + int mult = df.getMultiplier(); + while(mult > 1) { + ++maxDecDigits; + mult /= 10; + } + _maxDecDigits = maxDecDigits; + } + + @Override + public int getMaxDecimalDigits() { + return _maxDecDigits; } } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FunctionSupport.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FunctionSupport.java index aa978e2..b9cbb1a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FunctionSupport.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FunctionSupport.java @@ -47,10 +47,12 @@ public class FunctionSupport _maxParams = maxParams; } + @Override public String getName() { return _name; } + @Override public boolean isPure() { // most functions are probably pure, so make this the default return true; @@ -94,6 +96,7 @@ public class FunctionSupport return false; } + @Override public final Value eval(EvalContext ctx, Value... params) { try { validateNumParams(params); @@ -112,6 +115,7 @@ public class FunctionSupport super(name, 1, 1); } + @Override public final Value eval(EvalContext ctx, Value... params) { try { validateNumParams(params); @@ -130,6 +134,7 @@ public class FunctionSupport super(name, 1, 1); } + @Override public final Value eval(EvalContext ctx, Value... params) { try { validateNumParams(params); @@ -152,6 +157,7 @@ public class FunctionSupport super(name, 2, 2); } + @Override public final Value eval(EvalContext ctx, Value... params) { try { validateNumParams(params); @@ -170,6 +176,7 @@ public class FunctionSupport super(name, 3, 3); } + @Override public final Value eval(EvalContext ctx, Value... params) { try { validateNumParams(params); @@ -193,6 +200,7 @@ public class FunctionSupport super(name, minParams, maxParams); } + @Override public final Value eval(EvalContext ctx, Value... params) { try { validateNumParams(params); @@ -215,14 +223,17 @@ public class FunctionSupport _name = _delegate.getName() + NON_VAR_SUFFIX; } + @Override public String getName() { return _name; } + @Override public boolean isPure() { return _delegate.isPure(); } + @Override public Value eval(EvalContext ctx, Value... params) { Value result = _delegate.eval(ctx, params); if(result.isNull()) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java index 30025ad..27aa715 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java @@ -33,10 +33,12 @@ public class LongValue extends BaseNumericValue _val = val; } + @Override public Type getType() { return Type.LONG; } + @Override public Object get() { return _val; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java index ce251c2..57a5e35 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java @@ -222,5 +222,15 @@ public class NumberFormatter FieldPosition pos) { throw new UnsupportedOperationException(); } + + @Override + public int getMaximumFractionDigits() { + return _df.getMaximumFractionDigits(); + } + + @Override + public int getMinimumFractionDigits() { + return _df.getMinimumFractionDigits(); + } } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java index 19b22eb..ff799be 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java @@ -17,13 +17,12 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.math.BigInteger; import java.text.DecimalFormatSymbols; import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** * @@ -41,10 +40,12 @@ public class StringValue extends BaseValue _val = val; } + @Override public Type getType() { return Type.STRING; } + @Override public Object get() { return _val; } @@ -84,7 +85,7 @@ public class StringValue extends BaseValue // numberToDateValue may return null for out of range numbers) try { dateValue = DefaultDateFunctions.numberToDateValue( - ctx, getNumber(ctx).doubleValue()); + getNumber(ctx).doubleValue()); } catch(EvalException ignored) { // not a number, not a date/time } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java index 3040920..e7be9bc 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java @@ -18,9 +18,10 @@ package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.regex.Pattern; import com.healthmarketscience.jackcess.expr.EvalException; @@ -38,9 +39,11 @@ public class ValueSupport @Override public boolean isNull() { return true; } + @Override public Type getType() { return Type.NULL; } + @Override public Object get() { return null; } @@ -99,36 +102,42 @@ public class ValueSupport return new BigDecimalValue(normalize(s)); } - public static Value toDateValue(LocaleContext ctx, Value.Type type, double dd) - { - return toValue(type, new Date( - ColumnImpl.fromDateDouble(dd, ctx.getCalendar()))); + static Value toDateValueIfPossible(Value.Type dateType, double dd) { + if(DefaultDateFunctions.isValidDateDouble(dd)) { + return ValueSupport.toValue( + dateType, ColumnImpl.ldtFromLocalDateDouble(dd)); + } + return ValueSupport.toValue(dd); + } + + public static Value toValue(LocalDate ld) { + return new DateTimeValue( + Value.Type.DATE, LocalDateTime.of(ld, ColumnImpl.BASE_LT)); } - public static Value toValue(Calendar cal) { - return new DateTimeValue(getDateTimeType(cal), cal.getTime()); + public static Value toValue(LocalTime lt) { + return new DateTimeValue( + Value.Type.TIME, LocalDateTime.of(ColumnImpl.BASE_LD, lt)); } - public static Value.Type getDateTimeType(Calendar cal) { - boolean hasTime = ((cal.get(Calendar.HOUR_OF_DAY) != 0) || - (cal.get(Calendar.MINUTE) != 0) || - (cal.get(Calendar.SECOND) != 0)); + public static Value toValue(LocalDateTime ldt) { + return new DateTimeValue(getDateTimeType(ldt), ldt); + } - boolean hasDate = - ((cal.get(Calendar.YEAR) != ExpressionTokenizer.BASE_DATE_YEAR) || - ((cal.get(Calendar.MONTH) + 1) != ExpressionTokenizer.BASE_DATE_MONTH) || - (cal.get(Calendar.DAY_OF_MONTH) != ExpressionTokenizer.BASE_DATE_DAY)); + public static Value.Type getDateTimeType(LocalDateTime ldt) { + boolean hasDate = !ColumnImpl.BASE_LD.equals(ldt.toLocalDate()); + boolean hasTime = !ColumnImpl.BASE_LT.equals(ldt.toLocalTime()); return (hasDate ? (hasTime ? Value.Type.DATE_TIME : Value.Type.DATE) : Value.Type.TIME); } - public static Value toValue(Value.Type type, Date d) { - return new DateTimeValue(type, d); + public static Value toValue(Value.Type type, LocalDateTime ldt) { + return new DateTimeValue(type, ldt); } - public static DateFormat getDateFormatForType(LocaleContext ctx, Value.Type type) { + public static DateTimeFormatter getDateFormatForType(LocaleContext ctx, Value.Type type) { String fmtStr = null; switch(type) { case DATE: @@ -143,7 +152,7 @@ public class ValueSupport default: throw new EvalException("Unexpected date/time type " + type); } - return ctx.createDateFormat(fmtStr); + return ctx.createDateFormatter(fmtStr); } /** diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java index 140fde3..3567c57 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/AppendQueryImpl.java @@ -36,10 +36,12 @@ public class AppendQueryImpl extends BaseSelectQueryImpl implements AppendQuery super(name, rows, objectId, objectFlag, Type.APPEND); } + @Override public String getTargetTable() { return getTypeRow().name1; } + @Override public List<String> getTargetColumns() { return new RowFormatter(getTargetRows()) { @Override protected void format(StringBuilder builder, Row row) { @@ -48,14 +50,17 @@ public class AppendQueryImpl extends BaseSelectQueryImpl implements AppendQuery }.format(); } + @Override public String getRemoteDbPath() { return getTypeRow().name2; } + @Override public String getRemoteDbType() { return getTypeRow().expression; } + @Override public List<String> getValues() { return new RowFormatter(getValueRows()) { @Override protected void format(StringBuilder builder, Row row) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java index c213dd0..691ca9a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/BaseSelectQueryImpl.java @@ -77,6 +77,7 @@ public abstract class BaseSelectQueryImpl extends QueryImpl } } + @Override public String getSelectType() { if(hasFlag(DISTINCT_SELECT_TYPE)) { @@ -99,6 +100,7 @@ public abstract class BaseSelectQueryImpl extends QueryImpl return DEFAULT_TYPE; } + @Override public List<String> getSelectColumns() { List<String> result = (new RowFormatter(getColumnRows()) { @@ -143,6 +145,7 @@ public abstract class BaseSelectQueryImpl extends QueryImpl return super.getWhereExpression(); } + @Override public List<String> getGroupings() { return (new RowFormatter(getGroupByRows()) { @@ -152,6 +155,7 @@ public abstract class BaseSelectQueryImpl extends QueryImpl }).format(); } + @Override public String getHavingExpression() { return getHavingRow().expression; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java index 930f785..240e085 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/CrossTabQueryImpl.java @@ -59,6 +59,7 @@ public class CrossTabQueryImpl extends BaseSelectQueryImpl CROSSTAB_PIVOT_FLAG)); } + @Override public String getTransformExpression() { Row row = getTransformRow(); if(row.expression == null) { @@ -69,6 +70,7 @@ public class CrossTabQueryImpl extends BaseSelectQueryImpl return toAlias(builder, row.name1).toString(); } + @Override public String getPivotExpression() { return getPivotRow().expression; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java index 5d1024f..5496e29 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/DataDefinitionQueryImpl.java @@ -34,6 +34,7 @@ public class DataDefinitionQueryImpl extends QueryImpl super(name, rows, objectId, objectFlag, Type.DATA_DEFINITION); } + @Override public String getDDLString() { return getTypeRow().expression; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java index 7a153a5..27880cf 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/MakeTableQueryImpl.java @@ -35,14 +35,17 @@ public class MakeTableQueryImpl extends BaseSelectQueryImpl super(name, rows, objectId, objectFlag, Type.MAKE_TABLE); } + @Override public String getTargetTable() { return getTypeRow().name1; } + @Override public String getRemoteDbPath() { return getTypeRow().name2; } + @Override public String getRemoteDbType() { return getTypeRow().expression; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java index cfbcdc5..56b672b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/PassthroughQueryImpl.java @@ -34,10 +34,12 @@ public class PassthroughQueryImpl extends QueryImpl implements PassthroughQuery super(name, rows, objectId, objectFlag, Type.PASSTHROUGH); } + @Override public String getConnectionString() { return getTypeRow().name1; } + @Override public String getPassthroughString() { return getTypeRow().expression; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java index f9513d4..f0272f3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java @@ -22,14 +22,13 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.query.Query; -import org.apache.commons.lang.SystemUtils; /** * Constants used by the query data parsing. - * + * * @author James Ahlborn */ -public class QueryFormat +public class QueryFormat { private QueryFormat() {} @@ -90,7 +89,7 @@ public class QueryFormat public static final short APPEND_VALUE_FLAG = (short)0x8000; public static final short CROSSTAB_PIVOT_FLAG = 0x01; - public static final short CROSSTAB_NORMAL_FLAG = 0x02; + public static final short CROSSTAB_NORMAL_FLAG = 0x02; public static final String UNION_PART1 = "X7YZ_____1"; public static final String UNION_PART2 = "X7YZ_____2"; @@ -102,10 +101,10 @@ public class QueryFormat public static final Pattern IDENTIFIER_SEP_PAT = Pattern.compile("\\."); public static final char IDENTIFIER_SEP_CHAR = '.'; - public static final String NEWLINE = SystemUtils.LINE_SEPARATOR; + public static final String NEWLINE = System.lineSeparator(); - public static final Map<Short,String> PARAM_TYPE_MAP = + public static final Map<Short,String> PARAM_TYPE_MAP = new HashMap<Short,String>(); static { PARAM_TYPE_MAP.put((short)0, "Value"); @@ -123,7 +122,7 @@ public class QueryFormat PARAM_TYPE_MAP.put((short)DataType.GUID.getValue(), "Guid"); } - public static final Map<Short,String> JOIN_TYPE_MAP = + public static final Map<Short,String> JOIN_TYPE_MAP = new HashMap<Short,String>(); static { JOIN_TYPE_MAP.put((short)1, " INNER JOIN "); @@ -131,7 +130,7 @@ public class QueryFormat JOIN_TYPE_MAP.put((short)3, " RIGHT JOIN "); } - public static final Map<Short,Query.Type> TYPE_MAP = + public static final Map<Short,Query.Type> TYPE_MAP = new HashMap<Short,Query.Type>(); static { for(Query.Type type : Query.Type.values()) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java index 6b51236..2bbc279 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java @@ -27,7 +27,7 @@ import com.healthmarketscience.jackcess.impl.RowIdImpl; import com.healthmarketscience.jackcess.impl.RowImpl; import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; import com.healthmarketscience.jackcess.query.Query; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -73,6 +73,7 @@ public abstract class QueryImpl implements Query /** * Returns the name of the query. */ + @Override public String getName() { return _name; } @@ -80,10 +81,12 @@ public abstract class QueryImpl implements Query /** * Returns the type of the query. */ + @Override public Type getType() { return _type; } + @Override public boolean isHidden() { return((_objectFlag & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0); } @@ -91,10 +94,12 @@ public abstract class QueryImpl implements Query /** * Returns the unique object id of the query. */ + @Override public int getObjectId() { return _objectId; } + @Override public int getObjectFlag() { return _objectFlag; } @@ -170,6 +175,7 @@ public abstract class QueryImpl implements Query } } + @Override public List<String> getParameters() { return (new RowFormatter(getParameterRows()) { @@ -299,6 +305,7 @@ public abstract class QueryImpl implements Query }).format(); } + @Override public String getOwnerAccessType() { return(hasFlag(OWNER_ACCESS_SELECT_TYPE) ? "WITH OWNERACCESS OPTION" : DEFAULT_TYPE); @@ -316,6 +323,7 @@ public abstract class QueryImpl implements Query /** * Returns the actual SQL string which this query data represents. */ + @Override public String toSQLString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java index f74aefd..c9b198b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/UnionQueryImpl.java @@ -35,14 +35,17 @@ public class UnionQueryImpl extends QueryImpl implements UnionQuery super(name, rows, objectId, objectFlag, Type.UNION); } + @Override public String getUnionType() { return(hasFlag(UNION_FLAG) ? DEFAULT_TYPE : "ALL"); } + @Override public String getUnionString1() { return getUnionString(UNION_PART1); } + @Override public String getUnionString2() { return getUnionString(UNION_PART2); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java index 9cf722a..afea512 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/UpdateQueryImpl.java @@ -36,21 +36,25 @@ public class UpdateQueryImpl extends QueryImpl implements UpdateQuery super(name, rows, objectId, objectFlag, Type.UPDATE); } + @Override public List<String> getTargetTables() { return super.getFromTables(); } + @Override public String getRemoteDbPath() { return super.getFromRemoteDbPath(); } + @Override public String getRemoteDbType() { return super.getFromRemoteDbType(); } + @Override public List<String> getNewValues() { return (new RowFormatter(getColumnRows()) { diff --git a/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java index e44002e..f527bb2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/CaseInsensitiveColumnMatcher.java @@ -41,6 +41,7 @@ public class CaseInsensitiveColumnMatcher implements ColumnMatcher { public CaseInsensitiveColumnMatcher() { } + @Override public boolean matches(Table table, String columnName, Object value1, Object value2) { diff --git a/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java b/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java index fce5289..e36c569 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java @@ -17,10 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.util; import java.io.Closeable; -import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Random; import com.healthmarketscience.jackcess.Database; @@ -64,11 +65,11 @@ public abstract class CustomLinkResolver implements LinkResolver /** temp dbs default to the filesystem, not in memory */ public static final boolean DEFAULT_IN_MEMORY = false; /** temp dbs end up in the system temp dir by default */ - public static final File DEFAULT_TEMP_DIR = null; + public static final Path DEFAULT_TEMP_DIR = null; private final FileFormat _defaultFormat; private final boolean _defaultInMemory; - private final File _defaultTempDir; + private final Path _defaultTempDir; /** * Creates a CustomLinkResolver using the default behavior for creating temp @@ -93,7 +94,7 @@ public abstract class CustomLinkResolver implements LinkResolver * directory) */ protected CustomLinkResolver(FileFormat defaultFormat, boolean defaultInMemory, - File defaultTempDir) + Path defaultTempDir) { _defaultFormat = defaultFormat; _defaultInMemory = defaultInMemory; @@ -108,7 +109,7 @@ public abstract class CustomLinkResolver implements LinkResolver return _defaultInMemory; } - protected File getDefaultTempDirectory() { + protected Path getDefaultTempDirectory() { return _defaultTempDir; } @@ -132,6 +133,7 @@ public abstract class CustomLinkResolver implements LinkResolver * @see #createTempDb * @see LinkResolver#DEFAULT */ + @Override public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName) throws IOException { @@ -161,24 +163,26 @@ public abstract class CustomLinkResolver implements LinkResolver * @return the temp db for holding the linked table info */ protected Database createTempDb(Object customFile, FileFormat format, - boolean inMemory, File tempDir, + boolean inMemory, Path tempDir, boolean readOnly) throws IOException { - File dbFile = null; + Path dbFile = null; FileChannel channel = null; boolean success = false; try { if(inMemory) { - dbFile = new File(MEM_DB_PREFIX + DB_ID.nextLong() + - format.getFileExtension()); + dbFile = Paths.get(MEM_DB_PREFIX + DB_ID.nextLong() + + format.getFileExtension()); channel = MemFileChannel.newChannel(); } else { - dbFile = File.createTempFile(FILE_DB_PREFIX, format.getFileExtension(), - tempDir); - channel = new RandomAccessFile(dbFile, DatabaseImpl.RW_CHANNEL_MODE) - .getChannel(); + dbFile = ((tempDir != null) ? + Files.createTempFile(tempDir, FILE_DB_PREFIX, + format.getFileExtension()) : + Files.createTempFile(FILE_DB_PREFIX, + format.getFileExtension())); + channel = FileChannel.open(dbFile, DatabaseImpl.RW_CHANNEL_OPTS); } TempDatabaseImpl.initDbChannel(channel, format); @@ -196,9 +200,12 @@ public abstract class CustomLinkResolver implements LinkResolver } } - private static void deleteDbFile(File dbFile) { - if((dbFile != null) && (dbFile.getName().startsWith(FILE_DB_PREFIX))) { - dbFile.delete(); + private static void deleteDbFile(Path dbFile) { + if((dbFile != null) && + dbFile.getFileName().toString().startsWith(FILE_DB_PREFIX)) { + try { + Files.deleteIfExists(dbFile); + } catch(IOException ignores) {} } } @@ -256,7 +263,7 @@ public abstract class CustomLinkResolver implements LinkResolver private final Object _customFile; protected TempDatabaseImpl(CustomLinkResolver resolver, Object customFile, - File file, FileChannel channel, + Path file, FileChannel channel, FileFormat fileFormat, boolean readOnly) throws IOException { @@ -283,7 +290,7 @@ public abstract class CustomLinkResolver implements LinkResolver try { super.close(); } finally { - deleteDbFile(getFile()); + deleteDbFile(getPath()); closeCustomFile(_customFile); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java b/src/main/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java index 9a112d8..220d525 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/EntryIterableBuilder.java @@ -106,6 +106,7 @@ public class EntryIterableBuilder implements Iterable<Row> return this; } + @Override public Iterator<Row> iterator() { return ((IndexCursorImpl)_cursor).entryIterator(this); } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/ErrorHandler.java b/src/main/java/com/healthmarketscience/jackcess/util/ErrorHandler.java index f47ea54..fe9197b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/ErrorHandler.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/ErrorHandler.java @@ -38,6 +38,7 @@ public interface ErrorHandler * @usage _general_field_ */ public static final ErrorHandler DEFAULT = new ErrorHandler() { + @Override public Object handleRowError(Column column, byte[] columnData, Location location, Exception error) throws IOException @@ -84,6 +85,7 @@ public interface ErrorHandler /** * Contains details about the errored row, useful for debugging. */ + @Override public String toString(); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/IterableBuilder.java b/src/main/java/com/healthmarketscience/jackcess/util/IterableBuilder.java index edb635d..3865152 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/IterableBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/IterableBuilder.java @@ -178,6 +178,7 @@ public class IterableBuilder implements Iterable<Row> return this; } + @Override public Iterator<Row> iterator() { return ((CursorImpl)_cursor).iterator(this); } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/LinkResolver.java b/src/main/java/com/healthmarketscience/jackcess/util/LinkResolver.java index 5310449..c1beee3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/LinkResolver.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/LinkResolver.java @@ -36,6 +36,7 @@ public interface LinkResolver * @usage _general_field_ */ public static final LinkResolver DEFAULT = new LinkResolver() { + @Override public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName) throws IOException diff --git a/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java b/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java index 528ddbb..ba9a037 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java @@ -29,6 +29,9 @@ import java.nio.channels.FileLock; import java.nio.channels.NonWritableChannelException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseBuilder; @@ -41,11 +44,11 @@ import com.healthmarketscience.jackcess.impl.DatabaseImpl; * where disk usage may not be possible or desirable). Obviously, this * requires enough jvm heap space to fit the file data. Use one of the * {@code newChannel()} methods to construct an instance of this class. - * <p/> + * <p> * In order to use this class with a Database, you <i>must</i> use the {@link * DatabaseBuilder} to open/create the Database instance, passing an instance * of this class to the {@link DatabaseBuilder#setChannel} method. - * <p/> + * <p> * Implementation note: this class is optimized for use with {@link Database}. * Therefore not all methods may be implemented and individual read/write * operations are only supported within page boundaries. @@ -53,8 +56,13 @@ import com.healthmarketscience.jackcess.impl.DatabaseImpl; * @author James Ahlborn * @usage _advanced_class_ */ -public class MemFileChannel extends FileChannel +public class MemFileChannel extends FileChannel { + /** read-only channel access mode */ + public static final String RO_CHANNEL_MODE = "r"; + /** read/write channel access mode */ + public static final String RW_CHANNEL_MODE = "rw"; + private static final byte[][] EMPTY_DATA = new byte[0][]; // use largest possible Jet "page size" to ensure that reads/writes will @@ -68,10 +76,10 @@ public class MemFileChannel extends FileChannel /** current amount of actual data in the file */ private long _size; /** chunks containing the file data. the length of the chunk array is - always a power of 2 and the chunks are always CHUNK_SIZE. */ + always a power of 2 and the chunks are always CHUNK_SIZE. */ private byte[][] _data; - private MemFileChannel() + private MemFileChannel() { this(0L, 0L, EMPTY_DATA); } @@ -95,7 +103,7 @@ public class MemFileChannel extends FileChannel * affect the original File source. */ public static MemFileChannel newChannel(File file) throws IOException { - return newChannel(file, DatabaseImpl.RW_CHANNEL_MODE); + return newChannel(file, RW_CHANNEL_MODE); } /** @@ -105,13 +113,41 @@ public class MemFileChannel extends FileChannel * modifications to the returned channel will <i>not</i> affect the original * File source. */ - public static MemFileChannel newChannel(File file, String mode) - throws IOException + public static MemFileChannel newChannel(File file, String mode) + throws IOException { FileChannel in = null; try { return newChannel(in = new RandomAccessFile( - file, DatabaseImpl.RO_CHANNEL_MODE).getChannel(), + file, RO_CHANNEL_MODE).getChannel(), + mode); + } finally { + ByteUtil.closeQuietly(in); + } + } + + /** + * Creates a new MemFileChannel containing the contents of the + * given Path with the given mode (for mode details see + * {@link RandomAccessFile#RandomAccessFile(File,String)}). Note, + * modifications to the returned channel will <i>not</i> affect the original + * File source. + */ + public static MemFileChannel newChannel(Path file, OpenOption... opts) + throws IOException + { + FileChannel in = null; + try { + String mode = RO_CHANNEL_MODE; + if(opts != null) { + for(OpenOption opt : opts) { + if(opt == StandardOpenOption.WRITE) { + mode = RW_CHANNEL_MODE; + break; + } + } + } + return newChannel(in = FileChannel.open(file, StandardOpenOption.READ), mode); } finally { ByteUtil.closeQuietly(in); @@ -120,10 +156,19 @@ public class MemFileChannel extends FileChannel /** * Creates a new read/write MemFileChannel containing the contents of the + * given Path. Note, modifications to the returned channel will <i>not</i> + * affect the original File source. + */ + public static MemFileChannel newChannel(Path file) throws IOException { + return newChannel(file, DatabaseImpl.RW_CHANNEL_OPTS); + } + + /** + * Creates a new read/write MemFileChannel containing the contents of the * given InputStream. */ public static MemFileChannel newChannel(InputStream in) throws IOException { - return newChannel(in, DatabaseImpl.RW_CHANNEL_MODE); + return newChannel(in, RW_CHANNEL_MODE); } /** @@ -131,8 +176,8 @@ public class MemFileChannel extends FileChannel * given InputStream with the given mode (for mode details see * {@link RandomAccessFile#RandomAccessFile(File,String)}). */ - public static MemFileChannel newChannel(InputStream in, String mode) - throws IOException + public static MemFileChannel newChannel(InputStream in, String mode) + throws IOException { return newChannel(Channels.newChannel(in), mode); } @@ -141,10 +186,10 @@ public class MemFileChannel extends FileChannel * Creates a new read/write MemFileChannel containing the contents of the * given ReadableByteChannel. */ - public static MemFileChannel newChannel(ReadableByteChannel in) + public static MemFileChannel newChannel(ReadableByteChannel in) throws IOException { - return newChannel(in, DatabaseImpl.RW_CHANNEL_MODE); + return newChannel(in, RW_CHANNEL_MODE); } /** @@ -152,7 +197,7 @@ public class MemFileChannel extends FileChannel * given ReadableByteChannel with the given mode (for mode details see * {@link RandomAccessFile#RandomAccessFile(File,String)}). */ - public static MemFileChannel newChannel(ReadableByteChannel in, String mode) + public static MemFileChannel newChannel(ReadableByteChannel in, String mode) throws IOException { MemFileChannel channel = new MemFileChannel(); @@ -282,7 +327,7 @@ public class MemFileChannel extends FileChannel if(position >= _size) { return 0L; } - + count = Math.min(count, _size - position); int chunkIndex = getChunkIndex(position); @@ -304,7 +349,7 @@ public class MemFileChannel extends FileChannel numBytes += bytesWritten; count -= bytesWritten; } while(src.hasRemaining()); - + ++chunkIndex; chunkOffset = 0; } @@ -360,11 +405,11 @@ public class MemFileChannel extends FileChannel count -= bytesRead; _size = Math.max(_size, position + numBytes); } while(dst.hasRemaining()); - + ++chunkIndex; - chunkOffset = 0; + chunkOffset = 0; } - + return numBytes; } @@ -410,7 +455,7 @@ public class MemFileChannel extends FileChannel private static int getChunkIndex(long pos) { return (int)(pos / CHUNK_SIZE); } - + private static int getChunkOffset(long pos) { return (int)(pos % CHUNK_SIZE); } @@ -418,7 +463,7 @@ public class MemFileChannel extends FileChannel private static int getNumChunks(long size) { return getChunkIndex(size + CHUNK_SIZE - 1); } - + @Override public long write(ByteBuffer[] srcs, int offset, int length) throws IOException @@ -433,7 +478,7 @@ public class MemFileChannel extends FileChannel @Override public long read(ByteBuffer[] dsts, int offset, int length) throws IOException - { + { long numBytes = 0L; for(int i = offset; i < offset + length; ++i) { if(_position >= _size) { @@ -474,7 +519,7 @@ public class MemFileChannel extends FileChannel { super(channel._position, channel._size, channel._data); } - + @Override public int write(ByteBuffer src, long position) throws IOException { throw new NonWritableChannelException(); @@ -491,6 +536,6 @@ public class MemFileChannel extends FileChannel throws IOException { throw new NonWritableChannelException(); - } + } } } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java b/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java index 8a775ad..a84f122 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/OleBlob.java @@ -34,7 +34,7 @@ import com.healthmarketscience.jackcess.impl.OleUtil; * with nested filesystems!), and jackcess only supports a small portion of * it. That said, jackcess should support the bulk of the common * functionality. - * <p/> + * <p> * The main Blob methods will interact with the <i>entire</i> OLE field data * which, in most cases, contains additional wrapper information. In order to * access the ultimate "content" contained within the OLE data, the {@link @@ -42,15 +42,15 @@ import com.healthmarketscience.jackcess.impl.OleUtil; * variety of formats, so additional sub-interfaces are available to interact * with it. The most specific sub-interface can be determined by the {@link * ContentType} of the Content. - * <p/> + * <p> * Once an OleBlob is no longer useful, <i>it should be closed</i> using * {@link #free} or {@link #close} methods (after which, the instance will no * longer be functional). - * <p/> + * <p> * Note, the OleBlob implementation is read-only (through the interface). In * order to modify blob contents, create a new OleBlob instance using {@link * OleBlob.Builder} and write it to the access database. - * <p/> + * <p> * <b>Example for interpreting an existing OLE field:</b> * <pre> * OleBlob oleBlob = null; @@ -66,7 +66,7 @@ import com.healthmarketscience.jackcess.impl.OleUtil; * if(oleBlob != null) { oleBlob.close(); } * } * </pre> - * <p/> + * <p> * <b>Example for creating new, embedded ole data:</b> * <pre> * OleBlob oleBlob = null; @@ -79,7 +79,7 @@ import com.healthmarketscience.jackcess.impl.OleUtil; * if(oleBlob != null) { oleBlob.close(); } * } * </pre> - * <p/> + * <p> * <b>Example for creating new, linked ole data:</b> * <pre> * OleBlob oleBlob = null; diff --git a/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java b/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java index 85620e8..023a917 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/ReadOnlyFileChannel.java @@ -28,7 +28,7 @@ import com.healthmarketscience.jackcess.Database; /** * Wrapper for existing FileChannel which is read-only. - * <p/> + * <p> * Implementation note: this class is optimized for use with {@link Database}. * Therefore not all methods may be implemented. * diff --git a/src/main/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java b/src/main/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java index d7e2e2b..3298fd3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/ReplacementErrorHandler.java @@ -47,6 +47,7 @@ public class ReplacementErrorHandler implements ErrorHandler _replacement = replacement; } + @Override public Object handleRowError(Column column, byte[] columnData, Location location, Exception error) throws IOException diff --git a/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java b/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java index 440d9dd..a579176 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java @@ -19,17 +19,17 @@ package com.healthmarketscience.jackcess.util; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.Row; -import org.apache.commons.lang.ObjectUtils; /** * The RowFilter class encapsulates a filter test for a table row. This can * be used by the {@link #apply(Iterable)} method to create an Iterable over a * table which returns only rows matching some criteria. - * + * * @author Patricia Donaldson, Xerox Corporation * @usage _general_class_ */ @@ -59,22 +59,22 @@ public abstract class RowFilter /** * Creates a filter based on a row pattern. - * + * * @param rowPattern Map from column names to the values to be matched. * A table row will match the target if - * {@code ObjectUtils.equals(rowPattern.get(s), row.get(s))} + * {@code Objects.equals(rowPattern.get(s), row.get(s))} * for all column names in the pattern map. * @return a filter which matches table rows which match the values in the * row pattern */ - public static RowFilter matchPattern(final Map<String,?> rowPattern) + public static RowFilter matchPattern(final Map<String,?> rowPattern) { return new RowFilter() { @Override - public boolean matches(Row row) + public boolean matches(Row row) { for(Map.Entry<String,?> e : rowPattern.entrySet()) { - if(!ObjectUtils.equals(e.getValue(), row.get(e.getKey()))) { + if(!Objects.equals(e.getValue(), row.get(e.getKey()))) { return false; } } @@ -89,18 +89,18 @@ public abstract class RowFilter * @param columnPattern column to be matched * @param valuePattern value to be matched. * A table row will match the target if - * {@code ObjectUtils.equals(valuePattern, row.get(columnPattern.getName()))}. + * {@code Objects.equals(valuePattern, row.get(columnPattern.getName()))}. * @return a filter which matches table rows which match the value in the * row pattern */ - public static RowFilter matchPattern(final Column columnPattern, - final Object valuePattern) + public static RowFilter matchPattern(final Column columnPattern, + final Object valuePattern) { return new RowFilter() { @Override - public boolean matches(Row row) + public boolean matches(Row row) { - return ObjectUtils.equals(valuePattern, columnPattern.getRowValue(row)); + return Objects.equals(valuePattern, columnPattern.getRowValue(row)); } }; } @@ -118,7 +118,7 @@ public abstract class RowFilter { return new RowFilter() { @Override - public boolean matches(Row row) + public boolean matches(Row row) { return !filter.matches(row); } @@ -140,7 +140,7 @@ public abstract class RowFilter public static Iterable<Row> apply(RowFilter rowFilter, Iterable<? extends Row> iterable) { - return((rowFilter != null) ? rowFilter.apply(iterable) : + return((rowFilter != null) ? rowFilter.apply(iterable) : (Iterable<Row>)iterable); } @@ -152,7 +152,7 @@ public abstract class RowFilter { private final Iterable<? extends Row> _iterable; - private FilterIterable(Iterable<? extends Row> iterable) + private FilterIterable(Iterable<? extends Row> iterable) { _iterable = iterable; } @@ -163,12 +163,14 @@ public abstract class RowFilter * iterable, returning only rows for which the {@link RowFilter#matches} * method returns {@code true} */ - public Iterator<Row> iterator() + @Override + public Iterator<Row> iterator() { return new Iterator<Row>() { private final Iterator<? extends Row> _iter = _iterable.iterator(); private Row _next; + @Override public boolean hasNext() { while(_iter.hasNext()) { _next = _iter.next(); @@ -180,6 +182,7 @@ public abstract class RowFilter return false; } + @Override public Row next() { if(_next == null) { throw new NoSuchElementException(); @@ -187,6 +190,7 @@ public abstract class RowFilter return _next; } + @Override public void remove() { throw new UnsupportedOperationException(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java index a564834..6661948 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java @@ -19,12 +19,12 @@ package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.util.Arrays; +import java.util.Objects; 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; /** * Simple concrete implementation of ColumnMatcher which tests for equality. @@ -41,6 +41,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { public SimpleColumnMatcher() { } + @Override public boolean matches(Table table, String columnName, Object value1, Object value2) { @@ -48,7 +49,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { return true; } - if((value1 != null) && (value2 != null) && + if((value1 != null) && (value2 != null) && (value1.getClass() != value2.getClass())) { // the values aren't the same type, try coercing them to "internal" @@ -58,7 +59,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { 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) { // ignored, just go with the original result @@ -73,7 +74,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { */ private static boolean equals(Object o1, Object o2) { - return (ObjectUtils.equals(o1, o2) || + return (Objects.equals(o1, o2) || ((o1 instanceof byte[]) && (o2 instanceof byte[]) && Arrays.equals((byte[])o1, (byte[])o2))); } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidator.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidator.java index 3c77cda..62fe900 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidator.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidator.java @@ -31,6 +31,7 @@ public class SimpleColumnValidator implements ColumnValidator { public static final SimpleColumnValidator INSTANCE = new SimpleColumnValidator(); + @Override public Object validate(Column col, Object val) throws IOException { return val; } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidatorFactory.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidatorFactory.java index f36556b..0c9e6b6 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidatorFactory.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnValidatorFactory.java @@ -30,6 +30,7 @@ public class SimpleColumnValidatorFactory implements ColumnValidatorFactory public static final SimpleColumnValidatorFactory INSTANCE = new SimpleColumnValidatorFactory(); + @Override public ColumnValidator createValidator(Column col) { return SimpleColumnValidator.INSTANCE; } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java index f86a236..15a55b7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleExportFilter.java @@ -34,10 +34,12 @@ public class SimpleExportFilter implements ExportFilter { public SimpleExportFilter() { } + @Override public List<Column> filterColumns(List<Column> columns) throws IOException { return columns; } + @Override public Object[] filterRow(Object[] row) throws IOException { return row; } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java index f8fbab1..2d6b726 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleImportFilter.java @@ -36,6 +36,7 @@ public class SimpleImportFilter implements ImportFilter { public SimpleImportFilter() { } + @Override public List<ColumnBuilder> filterColumns(List<ColumnBuilder> destColumns, ResultSetMetaData srcColumns) throws SQLException, IOException @@ -43,6 +44,7 @@ public class SimpleImportFilter implements ImportFilter { return destColumns; } + @Override public Object[] filterRow(Object[] row) throws SQLException, IOException { diff --git a/src/main/java/com/healthmarketscience/jackcess/util/TableIterableBuilder.java b/src/main/java/com/healthmarketscience/jackcess/util/TableIterableBuilder.java index 168d758..da6d1b5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/TableIterableBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/TableIterableBuilder.java @@ -87,6 +87,7 @@ public class TableIterableBuilder implements Iterable<Table> return setIncludeLinkedTables(false); } + @Override public Iterator<Table> iterator() { return ((DatabaseImpl)_db).iterator(this); } |