diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2019-02-06 01:14:51 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2019-02-06 01:14:51 +0000 |
commit | d231efd46cd5f1e4c5c0fff850e10f860b7fced2 (patch) | |
tree | 184d70f9ca54fb22cdda8e600a505c0ec0d70f57 | |
parent | 67240a9bb59b3ddf05ce3833fde27de7d38b6ca5 (diff) | |
parent | 116fad0ad42bf89867091710d64ef8ef5d4ee0d4 (diff) | |
download | jackcess-d231efd46cd5f1e4c5c0fff850e10f860b7fced2.tar.gz jackcess-d231efd46cd5f1e4c5c0fff850e10f860b7fced2.zip |
merge branch jdk8 changes through r1279
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1280 f203690c-595d-4dc9-a70b-905162fa7fd2
120 files changed, 4968 insertions, 1545 deletions
@@ -3,13 +3,13 @@ <parent> <groupId>com.healthmarketscience</groupId> <artifactId>openhms-parent</artifactId> - <version>1.1.10</version> + <version>2.0.0</version> </parent> <groupId>com.healthmarketscience.jackcess</groupId> <artifactId>jackcess</artifactId> <name>Jackcess</name> <description>A pure Java library for reading from and writing to MS Access databases.</description> - <version>2.2.4-SNAPSHOT</version> + <version>3.0.0-SNAPSHOT</version> <url>https://jackcess.sourceforge.io</url> <inceptionYear>2005</inceptionYear> <developers> @@ -124,8 +124,8 @@ <threadCount>2</threadCount> <systemProperties> <property> - <name>log4j.configuration</name> - <value>log4j_test.properties</value> + <name>java.util.logging.config.file</name> + <value>logging_test.properties</value> </property> <property> <name>com.healthmarketscience.jackcess.testFormats</name> @@ -189,31 +189,53 @@ <plugin> <groupId>com.google.code.maven-replacer-plugin</groupId> <artifactId>replacer</artifactId> + <executions> + <execution> + <id>with-regex2</id> + <phase>site</phase> + <goals> + <goal>replace</goal> + </goals> + <configuration> + <includes> + <include>${project.build.directory}/site/apidocs/**/*.html</include> + </includes> + <regex>true</regex> + <replacements> + <replacement> + <token>_general_(method|class|field)_</token> + <value><span class="UsageGeneral"><span class="UsageGeneralHeader">General</span>: This $1 is general use.</span></value> + </replacement> + <replacement> + <token>_intermediate_(method|class|field)_</token> + <value><span class="UsageIntermediate"><span class="UsageIntermediateHeader">Intermediate</span>: This $1 requires moderate API knowledge.</span></value> + </replacement> + <replacement> + <token>_advanced_(method|class|field)_</token> + <value><span class="UsageAdvanced"><span class="UsageAdvancedHeader">Advanced</span>: This $1 is for advanced/internal use.</span></value> + </replacement> + </replacements> + </configuration> + </execution> + </executions> </plugin> </plugins> </build> <dependencies> <dependency> - <groupId>commons-lang</groupId> - <artifactId>commons-lang</artifactId> - <version>2.6</version> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>3.8.1</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> - <version>1.1.3</version> - </dependency> - <dependency> - <groupId>log4j</groupId> - <artifactId>log4j</artifactId> - <version>1.2.7</version> - <scope>runtime</scope> - <optional>true</optional> + <version>1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> - <version>4.11</version> + <version>4.12</version> <scope>test</scope> </dependency> @@ -221,7 +243,7 @@ <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> - <version>3.9</version> + <version>4.0.0</version> <optional>true</optional> </dependency> @@ -250,14 +272,13 @@ <plugin> <artifactId>maven-javadoc-plugin</artifactId> <configuration> - <additionalJOption>-J-DTaglets.ConfigurationFile=${basedir}/src/site/javadoc/taglets.properties -J-DTaglets.NoDefaultConfiguration=true</additionalJOption> <minmemory>128m</minmemory> <maxmemory>512</maxmemory> <links> - <list>http://docs.oracle.com/javase/1.5.0/docs/api/</list> + <list>https://docs.oracle.com/javase/8/docs/api/</list> <list>http://docs.oracle.com/javaee/5/api/</list> </links> - <source>1.5</source> + <source>1.8</source> <show>public</show> <stylesheetfile>${basedir}/src/site/javadoc/stylesheet.css</stylesheetfile> <tags> @@ -267,16 +288,6 @@ <head>Usage:</head> </tag> </tags> - <taglets> - <taglet> - <tagletClass>net.sourceforge.taglets.Taglets</tagletClass> - <tagletArtifact> - <groupId>net.sourceforge.taglets</groupId> - <artifactId>taglets</artifactId> - <version>2.0.3</version> - </tagletArtifact> - </taglet> - </taglets> <quiet>true</quiet> </configuration> </plugin> @@ -289,20 +300,6 @@ </plugin> </plugins> </reporting> - <repositories> - <repository> - <releases> - <enabled>true</enabled> - </releases> - <snapshots> - <enabled>false</enabled> - </snapshots> - <id>taglets</id> - <name>Taglets</name> - <url>http://maven.geotoolkit.org/</url> - <layout>default</layout> - </repository> - </repositories> <distributionManagement> <site> <id>jackcess-build-site</id> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 589d500..8f8d654 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,31 @@ <author email="javajedi@users.sf.net">Tim McCune</author> </properties> <body> + <release version="3.0.0" date="TBD" description="Update to Java 8"> + <action dev="jahlborn" type="update"> + Jackcess now requires a Java 8+ runtime. As part of this update, all + dependencies have been updated to their latest versions. + </action> + <action dev="jahlborn" type="update"> + Add support for Java 8 Temporal types. Date/time fields will now + accept as input most Temporal types (e.g. LocalDate, LocalTime, + LocalDateTime, etc). Additionally, date/time fields can optionally be + changed to output LocalDateTime instead of Date. This behavior is + configurable on a per-Database basis by setting the DateTimeType for + the database. All Date based APIs now have a parallel LocalDateTime + API. Note that only one of those will work depending on how + the Database is configured. The legacy Date support will be the + default initially, but is deprecated and may be removed in the future. + </action> + <action dev="jahlborn" type="update"> + Add support for Path. Existing File based APIs now have parallel + versions which utilize Path. + </action> + <action dev="jahlborn" type="update"> + The Apache commons-lang dependency has been replaced with + commons-lang3. + </action> + </release> <release version="2.2.3" date="2019-02-05"> <action dev="jahlborn" type="fix"> Fix parsing of escaped double quotes in expressions. 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); } diff --git a/src/main/resources/com/healthmarketscience/jackcess/log4j.properties b/src/main/resources/com/healthmarketscience/jackcess/log4j.properties deleted file mode 100644 index 092468c..0000000 --- a/src/main/resources/com/healthmarketscience/jackcess/log4j.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.rootCategory=INFO, stdout -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=**** %-5p %d{MMM d HH:mm:ss} [%F] - %m%n - -log4j.category.com.healthmarketscience.jackcess=INFO diff --git a/src/site/javadoc/stylesheet.css b/src/site/javadoc/stylesheet.css index e877e69..063d6a2 100644 --- a/src/site/javadoc/stylesheet.css +++ b/src/site/javadoc/stylesheet.css @@ -17,51 +17,580 @@ specific language governing permissions and limitations under the License. */ -/* Javadoc style sheet */ - -/* Define colors, fonts and other style attributes here to override the defaults */ +/* Javadoc style sheet from java 8 */ +/* +Overall document style +*/ -/* Page background color */ -body { background-color: #FFFFFF } +@import url('resources/fonts/dejavu.css'); +body { + background-color:#ffffff; + color:#353833; + font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:14px; + margin:0; +} a:link, a:visited { - color: blue; - } + text-decoration:none; + color:#4A6782; +} +a:hover, a:focus { + text-decoration:none; + color:#bb7a2a; +} +a:active { + text-decoration:none; + color:#4A6782; +} +a[name] { + color:#353833; +} +a[name]:hover { + text-decoration:none; + color:#353833; +} +pre { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; +} +h1 { + font-size:20px; +} +h2 { + font-size:18px; +} +h3 { + font-size:16px; + font-style:italic; +} +h4 { + font-size:13px; +} +h5 { + font-size:12px; +} +h6 { + font-size:11px; +} +ul { + list-style-type:disc; +} +code, tt { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; + margin-top:8px; + line-height:1.4em; +} +dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; +} +table tr td dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + vertical-align:top; + padding-top:4px; +} +sup { + font-size:8px; +} +/* +Document title and Copyright styles +*/ +.clear { + clear:both; + height:0px; + overflow:hidden; +} +.aboutLanguage { + float:right; + padding:0px 21px; + font-size:11px; + z-index:200; + margin-top:-9px; +} +.legalCopy { + margin-left:.5em; +} +.bar a, .bar a:link, .bar a:visited, .bar a:active { + color:#FFFFFF; + text-decoration:none; +} +.bar a:hover, .bar a:focus { + color:#bb7a2a; +} +.tab { + background-color:#0066FF; + color:#ffffff; + padding:8px; + width:5em; + font-weight:bold; +} +/* +Navigation bar styles +*/ +.bar { + background-color:#4D7A97; + color:#FFFFFF; + padding:.8em .5em .4em .8em; + height:auto;/*height:1.8em;*/ + font-size:11px; + margin:0; +} +.topNav { + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.bottomNav { + margin-top:10px; + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.subNav { + background-color:#dee3e9; + float:left; + width:100%; + overflow:hidden; + font-size:12px; +} +.subNav div { + clear:left; + float:left; + padding:0 0 5px 6px; + text-transform:uppercase; +} +ul.navList, ul.subNavList { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.navList li{ + list-style:none; + float:left; + padding: 5px 6px; + text-transform:uppercase; +} +ul.subNavList li{ + list-style:none; + float:left; +} +.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { + color:#FFFFFF; + text-decoration:none; + text-transform:uppercase; +} +.topNav a:hover, .bottomNav a:hover { + text-decoration:none; + color:#bb7a2a; + text-transform:uppercase; +} +.navBarCell1Rev { + background-color:#F8981D; + color:#253441; + margin: auto 5px; +} +.skipNav { + position:absolute; + top:auto; + left:-9999px; + overflow:hidden; +} +/* +Page header and footer styles +*/ +.header, .footer { + clear:both; + margin:0 20px; + padding:5px 0 0 0; +} +.indexHeader { + margin:10px; + position:relative; +} +.indexHeader span{ + margin-right:15px; +} +.indexHeader h1 { + font-size:13px; +} +.title { + color:#2c4557; + margin:10px 0; +} +.subTitle { + margin:5px 0 0 0; +} +.header ul { + margin:0 0 15px 0; + padding:0; +} +.footer ul { + margin:20px 0 5px 0; +} +.header ul li, .footer ul li { + list-style:none; + font-size:13px; +} +/* +Heading styles +*/ +div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +ul.blockList ul.blockList ul.blockList li.blockList h3 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +ul.blockList ul.blockList li.blockList h3 { + padding:0; + margin:15px 0; +} +ul.blockList li.blockList h2 { + padding:0px 0 20px 0; +} +/* +Page layout container styles +*/ +.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { + clear:both; + padding:10px 20px; + position:relative; +} +.indexContainer { + margin:10px; + position:relative; + font-size:12px; +} +.indexContainer h2 { + font-size:13px; + padding:0 0 3px 0; +} +.indexContainer ul { + margin:0; + padding:0; +} +.indexContainer ul li { + list-style:none; + padding-top:2px; +} +.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { + font-size:12px; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + margin:5px 0 10px 0px; + font-size:14px; + font-family:'DejaVu Sans Mono',monospace; +} +.serializedFormContainer dl.nameValue dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +.serializedFormContainer dl.nameValue dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* +List styles +*/ +ul.horizontal li { + display:inline; + font-size:0.9em; +} +ul.inheritance { + margin:0; + padding:0; +} +ul.inheritance li { + display:inline; + list-style:none; +} +ul.inheritance li ul.inheritance { + margin-left:15px; + padding-left:15px; + padding-top:1px; +} +ul.blockList, ul.blockListLast { + margin:10px 0 10px 0; + padding:0; +} +ul.blockList li.blockList, ul.blockListLast li.blockList { + list-style:none; + margin-bottom:15px; + line-height:1.4; +} +ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { + padding:0px 20px 5px 10px; + border:1px solid #ededed; + background-color:#f8f8f8; +} +ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { + padding:0 0 5px 8px; + background-color:#ffffff; + border:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { + margin-left:0; + padding-left:0; + padding-bottom:15px; + border:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { + list-style:none; + border-bottom:none; + padding-bottom:0; +} +table tr td dl, table tr td dl dt, table tr td dl dd { + margin-top:0; + margin-bottom:1px; +} +/* +Table styles +*/ +.overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary { + width:100%; + border-left:1px solid #EEE; + border-right:1px solid #EEE; + border-bottom:1px solid #EEE; +} +.overviewSummary, .memberSummary { + padding:0px; +} +.overviewSummary caption, .memberSummary caption, .typeSummary caption, +.useSummary caption, .constantsSummary caption, .deprecatedSummary caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:#253441; + font-weight:bold; + clear:none; + overflow:hidden; + padding:0px; + padding-top:10px; + padding-left:1px; + margin:0px; + white-space:pre; +} +.overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link, +.useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link, +.overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover, +.useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover, +.overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active, +.useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active, +.overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited, +.useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited { + color:#FFFFFF; +} +.overviewSummary caption span, .memberSummary caption span, .typeSummary caption span, +.useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + padding-bottom:7px; + display:inline-block; + float:left; + background-color:#F8981D; + border: none; + height:16px; +} +.memberSummary caption span.activeTableTab span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + margin-right:3px; + display:inline-block; + float:left; + background-color:#F8981D; + height:16px; +} +.memberSummary caption span.tableTab span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + margin-right:3px; + display:inline-block; + float:left; + background-color:#4D7A97; + height:16px; +} +.memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab { + padding-top:0px; + padding-left:0px; + padding-right:0px; + background-image:none; + float:none; + display:inline; +} +.overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd, +.useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd { + display:none; + width:5px; + position:relative; + float:left; + background-color:#F8981D; +} +.memberSummary .activeTableTab .tabEnd { + display:none; + width:5px; + margin-right:3px; + position:relative; + float:left; + background-color:#F8981D; +} +.memberSummary .tableTab .tabEnd { + display:none; + width:5px; + margin-right:3px; + position:relative; + background-color:#4D7A97; + float:left; -a:active, a:hover, #leftcol a:active, #leftcol a:hover { - color: #f30 !important; - } +} +.overviewSummary td, .memberSummary td, .typeSummary td, +.useSummary td, .constantsSummary td, .deprecatedSummary td { + text-align:left; + padding:0px 0px 12px 10px; +} +th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th, +td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{ + vertical-align:top; + padding-right:0px; + padding-top:8px; + padding-bottom:3px; +} +th.colFirst, th.colLast, th.colOne, .constantsSummary th { + background:#dee3e9; + text-align:left; + padding:8px 3px 3px 7px; +} +td.colFirst, th.colFirst { + white-space:nowrap; + font-size:13px; +} +td.colLast, th.colLast { + font-size:13px; +} +td.colOne, th.colOne { + font-size:13px; +} +.overviewSummary td.colFirst, .overviewSummary th.colFirst, +.useSummary td.colFirst, .useSummary th.colFirst, +.overviewSummary td.colOne, .overviewSummary th.colOne, +.memberSummary td.colFirst, .memberSummary th.colFirst, +.memberSummary td.colOne, .memberSummary th.colOne, +.typeSummary td.colFirst{ + width:25%; + vertical-align:top; +} +td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { + font-weight:bold; +} +.tableSubHeadingColor { + background-color:#EEEEFF; +} +.altColor { + background-color:#FFFFFF; +} +.rowColor { + background-color:#EEEEEF; +} +/* +Content styles +*/ +.description pre { + margin-top:0; +} +.deprecatedContent { + margin:0; + padding:10px 0; +} +.docSummary { + padding:0; +} -a:link.selfref, a:visited.selfref { - color: #555 !important; - } +ul.blockList ul.blockList ul.blockList li.blockList h3 { + font-style:normal; +} -.a td { - background: #ddd; - color: #000; - } +div.block { + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; +} -/* Table colors */ -.TableHeadingColor { background: #036; color:#FFFFFF } /* Dark blue */ -.TableSubHeadingColor { background: #bbb; color:#fff } /* Dark grey */ -.TableRowColor { background: #efefef } /* White */ +td.colLast div { + padding-top:0px; +} -/* Font used in left-hand frame lists */ -.FrameTitleFont { font-size: medium; font-family: normal; color:#000000 } -.FrameHeadingFont { font-size: medium; font-family: normal; color:#000000 } -.FrameItemFont { font-size: medium; font-family: normal; color:#000000 } -/* Example of smaller, sans-serif font in frames */ -/* .FrameItemFont { font-size: 10pt; font-family: Helvetica, Arial, sans-serif } */ +td.colLast a { + padding-bottom:3px; +} +/* +Formatting effect styles +*/ +.sourceLineNo { + color:green; + padding:0 30px 0 0; +} +h1.hidden { + visibility:hidden; + overflow:hidden; + font-size:10px; +} +.block { + display:block; + margin:3px 10px 2px 0px; + color:#474747; +} +.deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink, +.overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel, +.seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink { + font-weight:bold; +} +.deprecationComment, .emphasizedPhrase, .interfaceName { + font-style:italic; +} -/* Navigation bar fonts and colors */ -.NavBarCell1 { background-color:#ddd;}/* Light mauve */ -.NavBarCell1Rev { background-color:#888;}/* Dark Blue */ -.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} -.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} +div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase, +div.block div.block span.interfaceName { + font-style:normal; +} -.NavBarCell2 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} -.NavBarCell3 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} +div.contentContainer ul.blockList li.blockList h2{ + padding-bottom:0px; +} /* usage tag classes */ .UsageGeneralHeader { background: #efefef; color:#008000; text-decoration:underline } @@ -75,34 +604,34 @@ a:link.selfref, a:visited.selfref { /* Customizations pulled in from fluido 1.4 (and custom site.css) */ ul,ol { padding:0; - margin:0 0 10px 25px + margin:0 0 10px 25px; } ul ul,ul ol,ol ol,ol ul { - margin-bottom:0 -} -li { - line-height:20px + margin-bottom:0; } +/* li { */ +/* line-height:20px */ +/* } */ ul.unstyled,ol.unstyled { margin-left:0; - list-style:none + list-style:none; } ul.inline,ol.inline { margin-left:0; - list-style:none + list-style:none; } ul.inline>li,ol.inline>li { display:inline-block; *display:inline; padding-right:5px; padding-left:5px; - *zoom:1 + *zoom:1; } pre.prettyprint { border:1px solid #999; - page-break-inside:avoid + page-break-inside:avoid; } pre.prettyprint { padding:0 3px 2px; @@ -111,7 +640,7 @@ pre.prettyprint { color:#333; -webkit-border-radius:3px; -moz-border-radius:3px; - border-radius:3px + border-radius:3px; } /* code { */ /* padding:2px 4px; */ @@ -135,10 +664,10 @@ pre.prettyprint { border:1px solid rgba(0,0,0,0.15); -webkit-border-radius:4px; -moz-border-radius:4px; - border-radius:4px + border-radius:4px; } pre.prettyprint { - margin-bottom:20px + margin-bottom:20px; } pre.prettyprint code { padding:0; @@ -146,11 +675,11 @@ pre.prettyprint code { white-space:pre; white-space:pre-wrap; background-color:transparent; - border:0 + border:0; } .pre-scrollable { max-height:340px; - overflow-y:scroll + overflow-y:scroll; } .pln { @@ -228,22 +757,21 @@ pre.prettyprint code { } pre.prettyprint { padding:2px; - border:1px solid #888 + border:1px solid #888; } ol.linenums { margin-top:0; margin-bottom:0; - padding-left:15px + padding-left:15px; } li.L1,li.L3,li.L5,li.L7,li.L9 { - background:#eee -} - -li.L1,li.L3,li.L5,li.L7,li.L9 { - color:#888 + line-height:20px; + background:#eee; + color:#888; } li.L0,li.L2,li.L4,li.L6,li.L8 { - color:#888 + line-height:20px; + color:#888; } .source { diff --git a/src/site/javadoc/taglets.properties b/src/site/javadoc/taglets.properties deleted file mode 100644 index b24a1e3..0000000 --- a/src/site/javadoc/taglets.properties +++ /dev/null @@ -1,41 +0,0 @@ -# basic taglets config -Taglets.splash=false -Taglets.verbose=false -Taglets.debug=false -Taglets.drivers= drivers/j2se15.jar, drivers/j2se14.jar - -# custom usage formatting -Taglets.shutdown.usage-tag= net.sourceforge.taglets.simple.shutdown.RegexReplacer -Taglets.shutdown.usage-tag.files= **/*.html -Taglets.shutdown.usage-tag.token.0=_general_method_ -Taglets.shutdown.usage-tag.value.0=<span class="UsageGeneral"><span class="UsageGeneralHeader">General</span>: This method is general use.</span> - -Taglets.shutdown.usage-tag.token.1=_intermediate_method_ -Taglets.shutdown.usage-tag.value.1=<span class="UsageIntermediate"><span class="UsageIntermediateHeader">Intermediate</span>: This method requires moderate API knowledge.</span> - -Taglets.shutdown.usage-tag.token.2=_advanced_method_ -Taglets.shutdown.usage-tag.value.2=<span class="UsageAdvanced"><span class="UsageAdvancedHeader">Advanced</span>: This method is for advanced/internal use.</span> - -Taglets.shutdown.usage-tag.token.3=_general_class_ -Taglets.shutdown.usage-tag.value.3=<span class="UsageGeneral"><span class="UsageGeneralHeader">General</span>: This class is general use.</span> - -Taglets.shutdown.usage-tag.token.4=_intermediate_class_ -Taglets.shutdown.usage-tag.value.4=<span class="UsageIntermediate"><span class="UsageIntermediateHeader">Intermediate</span>: This class requires moderate API knowledge.</span> - -Taglets.shutdown.usage-tag.token.5=_advanced_class_ -Taglets.shutdown.usage-tag.value.5=<span class="UsageAdvanced"><span class="UsageAdvancedHeader">Advanced</span>: This class is for advanced/internal use.</span> - -Taglets.shutdown.usage-tag.token.6=_general_field_ -Taglets.shutdown.usage-tag.value.6=<span class="UsageGeneral"><span class="UsageGeneralHeader">General</span>: This field is general use.</span> - -Taglets.shutdown.usage-tag.token.7=_intermediate_field_ -Taglets.shutdown.usage-tag.value.7=<span class="UsageIntermediate"><span class="UsageIntermediateHeader">Intermediate</span>: This field requires moderate API knowledge.</span> - -Taglets.shutdown.usage-tag.token.8=_advanced_field_ -Taglets.shutdown.usage-tag.value.8=<span class="UsageAdvanced"><span class="UsageAdvancedHeader">Advanced</span>: This field is for advanced/internal use.</span> - - -# apparently we need one "normal" tag or the taglets code gets unhappy -Taglets.taglet.todo= net.sourceforge.taglets.simple.block.ParamBlockTaglet -Taglets.taglet.todo.dl.class= tagletsTodo -Taglets.taglet.todo.dl.header= <b>Todo:</b> diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml index 7dd82ef..326735a 100644 --- a/src/site/xdoc/index.xml +++ b/src/site/xdoc/index.xml @@ -19,6 +19,17 @@ for more info. </p> + <subsection name="Java 8+ Support (TBD)"> + <p> + Jackcess now requires Java 8+ as of the 3.0.0 release. All third + party dependencies have been updated to the latest versions. + Jackcess now supports Java 8+ data types like LocalDateTime and + Path. Databases can now optionally return Date values (legacy, + backwards compatible) or LocalDateTime values. See <a href="apidocs/com/healthmarketscience/jackcess/DateTimeType.html">DateTimeType</a> + for more details. + </p> + </subsection> + <subsection name="Expression Evaluation (2018-09-08)"> <p> Have you ever wished that Jackcess could handle field "default diff --git a/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java b/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java index b4704bf..c8b81a3 100644 --- a/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java @@ -39,7 +39,8 @@ import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; * * @author James Ahlborn */ -public class ComplexColumnTest extends TestCase +@SuppressWarnings("deprecation") +public class ComplexColumnTest extends TestCase { public ComplexColumnTest(String name) { @@ -66,7 +67,7 @@ public class ComplexColumnTest extends TestCase (ComplexValueForeignKey)verCol.getRowValue(row); String curValue = (String)col.getRowValue(row); - + if(rowId.equals("row1")) { checkVersions(1, complexValueFk, curValue); } else if(rowId.equals("row2")) { @@ -94,7 +95,7 @@ public class ComplexColumnTest extends TestCase Date upTime = new Date(); row8ValFk.addVersion("row8-memo", upTime); checkVersions(row8ValFk.get(), row8ValFk, "row8-memo", - "row8-memo", upTime); + "row8-memo", upTime); Cursor cursor = CursorBuilder.createCursor(t1); assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row3")); @@ -120,7 +121,7 @@ public class ComplexColumnTest extends TestCase "row3-memo-again", new Date(1315876965382L), "row3-memo-revised", new Date(1315876953077L), "row3-memo", new Date(1315876879126L)); - + try { v.delete(); fail("UnsupportedOperationException should have been thrown"); @@ -133,7 +134,7 @@ public class ComplexColumnTest extends TestCase "row3-memo-again", new Date(1315876965382L), "row3-memo-revised", new Date(1315876953077L), "row3-memo", new Date(1315876879126L)); - + try { v.getComplexValueForeignKey().deleteAllValues(); fail("UnsupportedOperationException should have been thrown"); @@ -146,7 +147,7 @@ public class ComplexColumnTest extends TestCase "row3-memo-again", new Date(1315876965382L), "row3-memo-revised", new Date(1315876953077L), "row3-memo", new Date(1315876879126L)); - + db.close(); } } @@ -154,7 +155,7 @@ public class ComplexColumnTest extends TestCase public void testAttachments() throws Exception { for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMPLEX)) { - + Database db = openCopy(testDB); Table t1 = db.getTable("Table1"); @@ -187,12 +188,12 @@ public class ComplexColumnTest extends TestCase ComplexValueForeignKey row8ValFk = (ComplexValueForeignKey) col.getRowValue(row8); row8ValFk.addAttachment(null, "test_data.txt", "txt", - getFileBytes("test_data.txt"), null, null); + getFileBytes("test_data.txt"), (Date)null, null); checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt"); row8ValFk.addEncodedAttachment(null, "test_data2.txt", "txt", - getEncodedFileBytes("test_data2.txt"), null, - null); - checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", + getEncodedFileBytes("test_data2.txt"), + (Date)null, null); + checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", "test_data2.txt"); Cursor cursor = CursorBuilder.createCursor(t1); @@ -200,8 +201,8 @@ public class ComplexColumnTest extends TestCase ComplexValueForeignKey row4ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(col); Attachment a = row4ValFk.addAttachment(null, "test_data.txt", "txt", - getFileBytes("test_data.txt"), null, - null); + getFileBytes("test_data.txt"), + (Date)null, null); checkAttachments(4, row4ValFk, "test_data2.txt", "test_data.txt"); a.setFileType("zip"); @@ -230,8 +231,8 @@ public class ComplexColumnTest extends TestCase ComplexValueForeignKey row2ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(col); row2ValFk.deleteAllValues(); - checkAttachments(2, row2ValFk); - + checkAttachments(2, row2ValFk); + db.close(); } } @@ -239,7 +240,7 @@ public class ComplexColumnTest extends TestCase public void testMultiValues() throws Exception { for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMPLEX)) { - + Database db = openCopy(testDB); Table t1 = db.getTable("Table1"); @@ -264,7 +265,7 @@ public class ComplexColumnTest extends TestCase } else { assertTrue(false); } - } + } Object[] row8 = {"row8", Column.AUTO_NUMBER, "some-data", "row8-memo", Column.AUTO_NUMBER, Column.AUTO_NUMBER}; @@ -307,17 +308,17 @@ public class ComplexColumnTest extends TestCase PropertyMap props = col.getProperties(); assertEquals(Boolean.TRUE, props.getValue(PropertyMap.ALLOW_MULTI_VALUE_PROP)); assertEquals("Value List", props.getValue(PropertyMap.ROW_SOURCE_TYPE_PROP)); - assertEquals("\"value1\";\"value2\";\"value3\";\"value4\"", + assertEquals("\"value1\";\"value2\";\"value3\";\"value4\"", props.getValue(PropertyMap.ROW_SOURCE_PROP)); - + db.close(); } } - + public void testUnsupported() throws Exception { for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.UNSUPPORTED)) { - + Database db = openCopy(testDB); Table t1 = db.getTable("Test"); @@ -331,7 +332,7 @@ public class ComplexColumnTest extends TestCase (ComplexValueForeignKey)col.getRowValue(row); if(rowId.equals(1)) { - checkUnsupportedValues(1, complexValueFk, + checkUnsupportedValues(1, complexValueFk, "RawData[(5) FF FE 62 61 7A]"); } else if(rowId.equals(2)) { checkUnsupportedValues(2, complexValueFk, "RawData[(5) FF FE 66 6F 6F]", "RawData[(5) FF FE 62 61 7A]"); @@ -340,12 +341,12 @@ public class ComplexColumnTest extends TestCase } else { assertTrue(false); } - } - + } + db.close(); } } - + private static void checkVersions( int cValId, ComplexValueForeignKey complexValueFk, String curValue, Object... versionInfos) @@ -376,7 +377,7 @@ public class ComplexColumnTest extends TestCase throws Exception { assertEquals(cValId, complexValueFk.get()); - + List<Attachment> attachments = complexValueFk.getAttachments(); if(fileNames.length == 0) { assertTrue(attachments.isEmpty()); @@ -388,12 +389,12 @@ public class ComplexColumnTest extends TestCase assertEquals(fname, a.getFileName()); assertEquals("txt", a.getFileType()); assertTrue(Arrays.equals(getFileBytes(fname), a.getFileData())); - assertTrue(Arrays.equals(getEncodedFileBytes(fname), + assertTrue(Arrays.equals(getEncodedFileBytes(fname), a.getEncodedFileData())); } } } - + private static void checkMultiValues( int cValId, ComplexValueForeignKey complexValueFk, Object... expectedValues) @@ -411,7 +412,7 @@ public class ComplexColumnTest extends TestCase SingleValue v = values.get(i); assertEquals(value, v.get()); } - } + } } private static void checkUnsupportedValues( @@ -434,7 +435,7 @@ public class ComplexColumnTest extends TestCase assertTrue(ColumnImpl.isRawData(rv)); assertEquals(value, rv.toString()); } - } + } } private static byte[] getFileBytes(String fname) throws Exception @@ -447,7 +448,7 @@ public class ComplexColumnTest extends TestCase } throw new RuntimeException("unexpected bytes"); } - + private static byte[] getEncodedFileBytes(String fname) throws Exception { if("test_data.txt".equals(fname)) { @@ -458,9 +459,9 @@ public class ComplexColumnTest extends TestCase } throw new RuntimeException("unexpected bytes"); } - + private static byte b(int i) { return (byte)i; } - + private static byte[] getAsciiBytes(String str) { try { return str.getBytes("US-ASCII"); @@ -468,7 +469,7 @@ public class ComplexColumnTest extends TestCase throw new RuntimeException(e); } } - + private static final byte[] TEST_ENC_BYTES = new byte[] { b(0x01),b(0x00),b(0x00),b(0x00),b(0x3A),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62), b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89),b(0x25),b(0x89),b(0x0A),b(0x69),b(0xF9), @@ -476,7 +477,7 @@ public class ComplexColumnTest extends TestCase }; private static final byte[] TEST_BYTES = getAsciiBytes("this is some test data for attachment."); - + private static final byte[] TEST2_ENC_BYTES = new byte[] { b(0x01),b(0x00),b(0x00),b(0x00),b(0x3F),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62), b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0xB9),b(0xF9),b(0x45),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89), @@ -484,5 +485,5 @@ public class ComplexColumnTest extends TestCase }; private static final byte[] TEST2_BYTES = getAsciiBytes("this is some more test data for attachment."); - + } diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 025e180..0a33a99 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -51,7 +52,8 @@ import static com.healthmarketscience.jackcess.TestUtil.*; /** * @author Tim McCune */ -public class DatabaseTest extends TestCase +@SuppressWarnings("deprecation") +public class DatabaseTest extends TestCase { public DatabaseTest(String name) throws Exception { super(name); @@ -114,7 +116,7 @@ public class DatabaseTest extends TestCase db.close(); } } - + public void testReadDeletedRows() throws Exception { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL, true)) { Table table = open(testDB).getTable("Table"); @@ -122,11 +124,11 @@ public class DatabaseTest extends TestCase while (table.getNextRow() != null) { rows++; } - assertEquals(2, rows); + assertEquals(2, rows); table.getDatabase().close(); } } - + public void testGetColumns() throws Exception { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { @@ -143,9 +145,9 @@ public class DatabaseTest extends TestCase checkColumn(columns, 8, "I", DataType.BOOLEAN); } } - + private static void checkColumn( - List<? extends Column> columns, int columnNumber, String name, + List<? extends Column> columns, int columnNumber, String name, DataType dataType) throws Exception { @@ -153,7 +155,7 @@ public class DatabaseTest extends TestCase assertEquals(name, column.getName()); assertEquals(dataType, column.getType()); } - + public void testGetNextRow() throws Exception { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { final Database db = open(testDB); @@ -183,7 +185,7 @@ public class DatabaseTest extends TestCase db.close(); } } - + public void testDeleteCurrentRow() throws Exception { // make sure correct row is deleted @@ -269,7 +271,7 @@ public class DatabaseTest extends TestCase table.reset(); List<Row> rows = RowFilterTest.toList(table); - + Row r1 = rows.remove(7); Row r2 = rows.remove(3); assertEquals(8, rows.size()); @@ -282,10 +284,10 @@ public class DatabaseTest extends TestCase table.deleteRow(r2); table.deleteRow(r1); - assertTable(rows, table); + assertTable(rows, table); } } - + public void testMissingFile() throws Exception { File bogusFile = new File("fooby-dooby.mdb"); assertTrue(!bogusFile.exists()); @@ -326,7 +328,7 @@ public class DatabaseTest extends TestCase } rowNum++; } - + table.getDatabase().close(); } } @@ -508,7 +510,7 @@ public class DatabaseTest extends TestCase db.close(); } - } + } public void testMultiPageTableDef() throws Exception { @@ -580,7 +582,7 @@ public class DatabaseTest extends TestCase db.close(); } - } + } public void testLargeTableDef() throws Exception { @@ -685,8 +687,8 @@ public class DatabaseTest extends TestCase TimeZone tz = TimeZone.getTimeZone("America/New_York"); SimpleDateFormat sdf = DatabaseBuilder.createDateFormat("yyyy-MM-dd"); sdf.getCalendar().setTimeZone(tz); - - List<String> dates = Arrays.asList("1582-10-15", "1582-10-14", + + List<String> dates = Arrays.asList("1582-10-15", "1582-10-14", "1492-01-10", "1392-01-10"); @@ -703,7 +705,7 @@ public class DatabaseTest extends TestCase Date d = sdf.parse(dateStr); table.addRow("row " + dateStr, d); } - + List<String> foundDates = new ArrayList<String>(); for(Row row : table) { foundDates.add(sdf.format(row.getDate("date"))); @@ -741,7 +743,7 @@ public class DatabaseTest extends TestCase sysTables.addAll( Arrays.asList("MSysObjects", "MSysQueries", "MSysACES", "MSysRelationships")); - + if (fileFormat == FileFormat.GENERIC_JET4) { assertNull("file format: " + fileFormat, db.getSystemTable("MSysAccessObjects")); } else if (fileFormat.ordinal() < FileFormat.V2003.ordinal()) { @@ -766,11 +768,11 @@ public class DatabaseTest extends TestCase if(fileFormat.ordinal() >= FileFormat.V2010.ordinal()) { sysTables.add("f_12D7448B56564D8AAE333BCC9B3718E5_Data"); sysTables.add("MSysResources"); - } + } } assertEquals(sysTables, db.getSystemTableNames()); - + assertNotNull(db.getSystemTable("MSysObjects")); assertNotNull(db.getSystemTable("MSysQueries")); assertNotNull(db.getSystemTable("MSysACES")); @@ -782,7 +784,7 @@ public class DatabaseTest extends TestCase assertEquals("MSysObjects", tmd.getName()); assertFalse(tmd.isLinked()); assertTrue(tmd.isSystem()); - + db.close(); } } @@ -839,7 +841,7 @@ public class DatabaseTest extends TestCase "RawData[(12) FF FE 6F 74 68 65 72 20 64 61 74 61]", null); List<String> fixVals = Arrays.asList("RawData[(4) 37 00 00 00]", - "RawData[(4) F3 FF FF FF]", + "RawData[(4) F3 FF FF FF]", "RawData[(4) 02 00 00 00]"); int idx = 0; @@ -891,7 +893,8 @@ public class DatabaseTest extends TestCase Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName); assertNotNull(linkeeDb); assertEquals(linkeeFile, linkeeDb.getFile()); - + assertEquals("linkeeTest.accdb", ((DatabaseImpl)linkeeDb).getName()); + List<? extends Map<String, Object>> expectedRows = createExpectedTable( createExpectedRow( @@ -900,7 +903,7 @@ public class DatabaseTest extends TestCase assertTable(expectedRows, t2); - db.createLinkedTable("FooTable", linkeeDbName, "Table2"); + db.createLinkedTable("FooTable", linkeeDbName, "Table2"); tmd = db.getTableMetaData("FooTable"); assertEquals("FooTable", tmd.getName()); @@ -929,7 +932,7 @@ public class DatabaseTest extends TestCase assertNull(tmd.getLinkedDbName()); Table t1 = tmd.open(db); - + assertFalse(db.isLinkedTable(null)); assertTrue(db.isLinkedTable(t2)); assertTrue(db.isLinkedTable(t3)); @@ -941,21 +944,21 @@ public class DatabaseTest extends TestCase assertTrue(tables.contains(t2)); assertTrue(tables.contains(t3)); assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - + tables = getTables(db.newIterable().setIncludeNormalTables(false)); assertEquals(2, tables.size()); assertFalse(tables.contains(t1)); assertTrue(tables.contains(t2)); assertTrue(tables.contains(t3)); assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - + tables = getTables(db.newIterable().withLocalUserTablesOnly()); assertEquals(1, tables.size()); assertTrue(tables.contains(t1)); assertFalse(tables.contains(t2)); assertFalse(tables.contains(t3)); assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - + tables = getTables(db.newIterable().withSystemTablesOnly()); assertTrue(tables.size() > 5); assertFalse(tables.contains(t1)); @@ -975,7 +978,7 @@ public class DatabaseTest extends TestCase } return tableList; } - + public void testTimeZone() throws Exception { TimeZone tz = TimeZone.getTimeZone("America/New_York"); @@ -989,7 +992,13 @@ public class DatabaseTest extends TestCase { ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { @Override - protected Calendar getCalendar() { return Calendar.getInstance(tz); } + public TimeZone getTimeZone() { return tz; } + @Override + public ZoneId getZoneId() { return null; } + @Override + public ColumnImpl.DateTimeFactory getDateTimeFactory() { + return getDateTimeFactory(DateTimeType.DATE); + } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java new file mode 100644 index 0000000..1eb11db --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -0,0 +1,275 @@ +/* +Copyright (c) 2018 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import junit.framework.TestCase; +import static com.healthmarketscience.jackcess.TestUtil.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import static com.healthmarketscience.jackcess.Database.*; + +/** + * + * @author James Ahlborn + */ +public class LocalDateTimeTest extends TestCase +{ + public LocalDateTimeTest(String name) throws Exception { + super(name); + } + + public void testWriteAndReadLocalDate() throws Exception { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + // since jackcess does not really store millis, shave them off before + // storing the current date/time + long curTimeNoMillis = (System.currentTimeMillis() / 1000L); + curTimeNoMillis *= 1000L; + + DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); + List<Date> dates = + new ArrayList<Date>( + Arrays.asList( + df.parse("19801231 00:00:00"), + df.parse("19930513 14:43:27"), + null, + df.parse("20210102 02:37:00"), + new Date(curTimeNoMillis))); + + Calendar c = Calendar.getInstance(); + for(int year = 1801; year < 2050; year +=3) { + for(int month = 0; month <= 12; ++month) { + for(int day = 1; day < 29; day += 3) { + c.clear(); + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month); + c.set(Calendar.DAY_OF_MONTH, day); + dates.add(c.getTime()); + } + } + } + + ((DatabaseImpl)db).getPageChannel().startWrite(); + try { + for(Date d : dates) { + table.addRow("row " + d, d); + } + } finally { + ((DatabaseImpl)db).getPageChannel().finishWrite(); + } + + List<LocalDateTime> foundDates = new ArrayList<LocalDateTime>(); + for(Row row : table) { + foundDates.add(row.getLocalDateTime("date")); + } + + assertEquals(dates.size(), foundDates.size()); + for(int i = 0; i < dates.size(); ++i) { + Date expected = dates.get(i); + LocalDateTime found = foundDates.get(i); + assertSameDate(expected, found); + } + + db.close(); + } + } + + public void testAncientLocalDates() throws Exception + { + ZoneId zoneId = ZoneId.of("America/New_York"); + DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu-MM-dd"); + + List<String> dates = Arrays.asList("1582-10-15", "1582-10-14", + "1492-01-10", "1392-01-10"); + + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + db.setZoneId(zoneId); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + for(String dateStr : dates) { + LocalDate ld = LocalDate.parse(dateStr, sdf); + table.addRow("row " + dateStr, ld); + } + + List<String> foundDates = new ArrayList<String>(); + for(Row row : table) { + foundDates.add(sdf.format(row.getLocalDateTime("date"))); + } + + assertEquals(dates, foundDates); + + db.close(); + } + + for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.OLD_DATES)) { + Database db = openCopy(testDB); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table t = db.getTable("Table1"); + + List<String> foundDates = new ArrayList<String>(); + for(Row row : t) { + foundDates.add(sdf.format(row.getLocalDateTime("DateField"))); + } + + assertEquals(dates, foundDates); + + db.close(); + } + + } + + public void testZoneId() throws Exception + { + ZoneId zoneId = ZoneId.of("America/New_York"); + doTestZoneId(zoneId); + + zoneId = ZoneId.of("Australia/Sydney"); + doTestZoneId(zoneId); + } + + private static void doTestZoneId(final ZoneId zoneId) throws Exception + { + final TimeZone tz = TimeZone.getTimeZone(zoneId); + ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { + @Override + public TimeZone getTimeZone() { return tz; } + @Override + public ZoneId getZoneId() { return zoneId; } + @Override + public ColumnImpl.DateTimeFactory getDateTimeFactory() { + return getDateTimeFactory(DateTimeType.LOCAL_DATE_TIME); + } + }; + + SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); + df.setTimeZone(tz); + + long startDate = df.parse("2012.01.01").getTime(); + long endDate = df.parse("2013.01.01").getTime(); + + Calendar curCal = Calendar.getInstance(tz); + curCal.setTimeInMillis(startDate); + + DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu.MM.dd HH:mm:ss"); + + while(curCal.getTimeInMillis() < endDate) { + Date curDate = curCal.getTime(); + LocalDateTime curLdt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(curDate.getTime()), zoneId); + LocalDateTime newLdt = ColumnImpl.ldtFromLocalDateDouble( + col.toDateDouble(curDate)); + if(!curLdt.equals(newLdt)) { + System.out.println("FOO " + curLdt + " " + newLdt); + assertEquals(sdf.format(curLdt), sdf.format(newLdt)); + } + curCal.add(Calendar.MINUTE, 30); + } + } + + public void testWriteAndReadTemporals() throws Exception { + ZoneId zoneId = ZoneId.of("America/New_York"); + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + db.setZoneId(zoneId); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + // since jackcess does not really store millis, shave them off before + // storing the current date/time + long curTimeNoMillis = (System.currentTimeMillis() / 1000L); + curTimeNoMillis *= 1000L; + + DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); + List<Date> tmpDates = + new ArrayList<Date>( + Arrays.asList( + df.parse("19801231 00:00:00"), + df.parse("19930513 14:43:27"), + df.parse("20210102 02:37:00"), + new Date(curTimeNoMillis))); + + List<Object> objs = new ArrayList<Object>(); + List<LocalDateTime> expected = new ArrayList<LocalDateTime>(); + for(Date d : tmpDates) { + Instant inst = Instant.ofEpochMilli(d.getTime()); + objs.add(inst); + ZonedDateTime zdt = inst.atZone(zoneId); + objs.add(zdt); + LocalDateTime ldt = zdt.toLocalDateTime(); + objs.add(ldt); + + for(int i = 0; i < 3; ++i) { + expected.add(ldt); + } + } + + ((DatabaseImpl)db).getPageChannel().startWrite(); + try { + for(Object o : objs) { + table.addRow("row " + o, o); + } + } finally { + ((DatabaseImpl)db).getPageChannel().finishWrite(); + } + + List<LocalDateTime> foundDates = new ArrayList<LocalDateTime>(); + for(Row row : table) { + foundDates.add(row.getLocalDateTime("date")); + } + + assertEquals(expected, foundDates); + + db.close(); + } + } + +} diff --git a/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java index 384386e..bb26719 100644 --- a/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java @@ -294,8 +294,8 @@ public class PropertyExpressionTest extends TestCase public static void testCustomEvalConfig() throws Exception { - TemporalConfig tempConf = new TemporalConfig("yyyy/M/d", "M/d", - "yyyy-MMM-d", + TemporalConfig tempConf = new TemporalConfig("[uuuu/]M/d", + "uuuu-MMM-d", "hh.mm.ss a", "HH.mm.ss", '/', '.', Locale.US); diff --git a/src/test/java/com/healthmarketscience/jackcess/TableTest.java b/src/test/java/com/healthmarketscience/jackcess/TableTest.java index 339ba39..eaae617 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TableTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/TableTest.java @@ -21,8 +21,8 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.List; +import java.util.TimeZone; import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.impl.JetFormat; @@ -40,8 +40,8 @@ public class TableTest extends TestCase { private TestTable _testTable; private int _varLenIdx; private int _fixedOffset; - - + + public TableTest(String name) { super(name); } @@ -52,14 +52,14 @@ public class TableTest extends TestCase { _varLenIdx = 0; _fixedOffset = 0; } - + public void testCreateRow() throws Exception { reset(); newTestColumn(DataType.INT, false); newTestColumn(DataType.TEXT, false); newTestColumn(DataType.TEXT, false); newTestTable(); - + int colCount = _columns.size(); ByteBuffer buffer = createRow(9, "Tim", "McCune"); @@ -91,10 +91,10 @@ public class TableTest extends TestCase { newTestColumn(DataType.TEXT, true); newTestColumn(DataType.TEXT, true); newTestTable(); - + ByteBuffer[] bufCmp1 = encodeColumns(small, large); ByteBuffer[] bufCmp2 = encodeColumns(smallNotAscii, largeNotAscii); - + assertEquals(buf1[0].remaining(), (bufCmp1[0].remaining() + small.length() - 2)); assertEquals(buf1[1].remaining(), @@ -111,7 +111,7 @@ public class TableTest extends TestCase { } - private ByteBuffer createRow(Object... row) + private ByteBuffer createRow(Object... row) throws IOException { return _testTable.createRow(row); @@ -146,7 +146,7 @@ public class TableTest extends TestCase { return b; } - private TableImpl newTestTable() + private TableImpl newTestTable() throws Exception { _testTable = new TestTable(); @@ -185,8 +185,8 @@ public class TableTest extends TestCase { return getFormat().CHARSET; } @Override - protected Calendar getCalendar() { - return Calendar.getInstance(); + public TimeZone getTimeZone() { + return TimeZone.getDefault(); } @Override public boolean isCompressedUnicode() { diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java index 7680fb3..83b2d7d 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java +++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java @@ -27,6 +27,10 @@ import java.io.PrintWriter; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -53,6 +57,7 @@ import org.junit.Assert; * * @author James Ahlborn */ +@SuppressWarnings("deprecation") public class TestUtil { public static final TimeZone TEST_TZ = @@ -86,7 +91,7 @@ public class TestUtil throws Exception { FileChannel channel = (inMem ? MemFileChannel.newChannel( - file, DatabaseImpl.RW_CHANNEL_MODE) + file, MemFileChannel.RW_CHANNEL_MODE) : null); final Database db = new DatabaseBuilder(file).setReadOnly(true) .setAutoSync(getTestAutoSync()).setChannel(channel).open(); @@ -378,6 +383,22 @@ public class TestUtil } } + static void assertSameDate(Date expected, LocalDateTime found) + { + if((expected == null) && (found == null)) { + return; + } + if((expected == null) || (found == null)) { + throw new AssertionError("Expected " + expected + ", found " + found); + } + + LocalDateTime expectedLdt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(expected.getTime()), + ZoneId.systemDefault()); + + Assert.assertEquals(expectedLdt, found); + } + static void copyFile(File srcFile, File dstFile) throws IOException { diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java index 561f1e8..104b266 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java @@ -16,6 +16,9 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -53,7 +56,7 @@ public class DatabaseReadWriteTest extends TestCase db.close(); } } - + public void testWriteAndReadInMem() throws Exception { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = createMem(fileFormat); @@ -61,7 +64,7 @@ public class DatabaseReadWriteTest extends TestCase db.close(); } } - + private static void doTestWriteAndRead(Database db) throws Exception { createTestTable(db); Object[] row = createTestRow(); @@ -117,7 +120,7 @@ public class DatabaseReadWriteTest extends TestCase } } - public void testUpdateRow() throws Exception + public void testUpdateRow() throws Exception { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = createMem(fileFormat); @@ -250,11 +253,18 @@ public class DatabaseReadWriteTest extends TestCase final long timeRange = 100000000L; final long timeStep = 37L; - for(long time = testTime - timeRange; time < testTime + timeRange; + for(long time = testTime - timeRange; time < testTime + timeRange; time += timeStep) { double accTime = ColumnImpl.toLocalDateDouble(time); long newTime = ColumnImpl.fromLocalDateDouble(accTime); assertEquals(time, newTime); + + Instant inst = Instant.ofEpochMilli(time); + LocalDateTime ldt = LocalDateTime.ofInstant(inst, ZoneOffset.UTC); + + accTime = ColumnImpl.toDateDouble(ldt); + LocalDateTime newLdt = ColumnImpl.ldtFromLocalDateDouble(accTime); + assertEquals(ldt, newLdt); } } } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java index 1ed17b7..66c8a71 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java @@ -98,7 +98,7 @@ public class JetFormatTest extends TestCase { } SUPPORTED_FILEFORMATS = supported.toArray(new FileFormat[0]); - SUPPORTED_FILEFORMATS_FOR_READ = + SUPPORTED_FILEFORMATS_FOR_READ = supportedForRead.toArray(new FileFormat[0]); } @@ -110,7 +110,7 @@ public class JetFormatTest extends TestCase { private final File dbFile; private final FileFormat expectedFileFormat; - private TestDB(File databaseFile, + private TestDB(File databaseFile, FileFormat expectedDBFileFormat) { dbFile = databaseFile; @@ -119,12 +119,12 @@ public class JetFormatTest extends TestCase { public final File getFile() { return dbFile; } - public final FileFormat getExpectedFileFormat() { - return expectedFileFormat; + public final FileFormat getExpectedFileFormat() { + return expectedFileFormat; } - public final JetFormat getExpectedFormat() { - return DatabaseImpl.getFileFormatDetails(expectedFileFormat).getFormat(); + public final JetFormat getExpectedFormat() { + return DatabaseImpl.getFileFormatDetails(expectedFileFormat).getFormat(); } @Override @@ -141,14 +141,14 @@ public class JetFormatTest extends TestCase { boolean readOnly) { List<TestDB> supportedTestDBs = new ArrayList<TestDB>(); - for (FileFormat fileFormat : + for (FileFormat fileFormat : (readOnly ? SUPPORTED_FILEFORMATS_FOR_READ : SUPPORTED_FILEFORMATS)) { File testFile = getFileForBasename(basename, fileFormat); if(!testFile.exists()) { continue; } - + // verify that the db is the file format expected try { Database db = new DatabaseBuilder(testFile).setReadOnly(true).open(); @@ -170,16 +170,16 @@ public class JetFormatTest extends TestCase { private static File getFileForBasename( Basename basename, FileFormat fileFormat) { - return new File(DIR_TEST_DATA, + return new File(DIR_TEST_DATA, fileFormat.name() + File.separator + - basename + fileFormat.name() + + basename + fileFormat.name() + fileFormat.getFileExtension()); } } - public static final List<TestDB> SUPPORTED_DBS_TEST = + public static final List<TestDB> SUPPORTED_DBS_TEST = TestDB.getSupportedForBasename(Basename.TEST); - public static final List<TestDB> SUPPORTED_DBS_TEST_FOR_READ = + public static final List<TestDB> SUPPORTED_DBS_TEST_FOR_READ = TestDB.getSupportedForBasename(Basename.TEST, true); @@ -193,11 +193,12 @@ public class JetFormatTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { - final FileChannel channel = DatabaseImpl.openChannel(testDB.dbFile, false); + final FileChannel channel = DatabaseImpl.openChannel( + testDB.dbFile.toPath(), false, false); try { JetFormat fmtActual = JetFormat.getFormat(channel); - assertEquals("Unexpected JetFormat for dbFile: " + + assertEquals("Unexpected JetFormat for dbFile: " + testDB.dbFile.getAbsolutePath(), testDB.getExpectedFormat(), fmtActual); @@ -221,7 +222,7 @@ public class JetFormatTest extends TestCase { PropertyMap props = db.getUserDefinedProperties(); props.put("foo", "bar"); props.save(); - } + } } catch(Exception e) { failure = e; @@ -268,7 +269,7 @@ public class JetFormatTest extends TestCase { } public void testSqlTypes() throws Exception { - + JetFormat v2000 = JetFormat.VERSION_4; for(DataType dt : DataType.values()) { if(v2000.isSupportedDataType(dt)) { diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index 84cab7c..dc4cabb 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -17,10 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.Calendar; -import java.util.Date; import com.healthmarketscience.jackcess.expr.EvalException; +import junit.framework.AssertionFailedError; import junit.framework.TestCase; import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.eval; import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.toBD; @@ -78,9 +79,9 @@ public class DefaultFunctionsTest extends TestCase eval("=CSng(\"57.12345\")")); assertEval("9786", "=CStr(9786)"); assertEval("-42", "=CStr(-42)"); - assertEval(new Date(1041483600000L), "=CDate('01/02/2003')"); - assertEval(new Date(1041508800000L), "=CDate('01/02/2003 7:00:00 AM')"); - assertEval(new Date(-1948781520000L), "=CDate(3013.45)"); + assertEval(LocalDateTime.of(2003,1,2,0,0), "=CDate('01/02/2003')"); + assertEval(LocalDateTime.of(2003,1,2,7,0), "=CDate('01/02/2003 7:00:00 AM')"); + assertEval(LocalDateTime.of(1908,3,31,10,48), "=CDate(3013.45)"); assertEval(-1, "=IsNull(Null)"); @@ -251,6 +252,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("-.123%", "=FormatPercent(-0.0012345,3,False)"); assertEval("$12,345.00", "=FormatCurrency(12345)"); + assertEval("($12,345.00)", "=FormatCurrency(-12345)"); assertEval("-$12.34", "=FormatCurrency(-12.345,-1,True,False)"); assertEval("$12", "=FormatCurrency(12.345,0,True,True)"); assertEval("($.123)", "=FormatCurrency(-0.12345,3,False)"); @@ -285,6 +287,11 @@ public class DefaultFunctionsTest extends TestCase assertEval("-12345.68", "=Format(-12345.6789, 'Fixed')"); assertEval("-0.12", "=Format(-0.12345, 'Fixed')"); + assertEval("\u20AC12,345.68", "=Format(12345.6789, 'Euro')"); + assertEval("\u20AC0.12", "=Format(0.12345, 'Euro')"); + assertEval("(\u20AC12,345.68)", "=Format(-12345.6789, 'Euro')"); + assertEval("(\u20AC0.12)", "=Format(-0.12345, 'Euro')"); + assertEval("$12,345.68", "=Format(12345.6789, 'Currency')"); assertEval("$0.12", "=Format(0.12345, 'Currency')"); assertEval("($12,345.68)", "=Format(-12345.6789, 'Currency')"); @@ -312,6 +319,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("7:00:00 AM", "=Format(#7:00:00 AM#, 'General Date')"); assertEval("1/2/2003 7:00:00 AM", "=Format('37623.2916666667', 'General Date')"); assertEval("foo", "=Format('foo', 'General Date')"); + assertEval("", "=Format('', 'General Date')"); assertEval("Thursday, January 02, 2003", "=Format(#01/02/2003 7:00:00 AM#, 'Long Date')"); assertEval("02-Jan-03", "=Format(#01/02/2003 7:00:00 AM#, 'Medium Date')"); @@ -320,7 +328,254 @@ public class DefaultFunctionsTest extends TestCase assertEval("07:00 AM", "=Format(#01/02/2003 7:00:00 AM#, 'Medium Time')"); assertEval("07:00", "=Format(#01/02/2003 7:00:00 AM#, 'Short Time')"); assertEval("19:00", "=Format(#01/02/2003 7:00:00 PM#, 'Short Time')"); + } + + public void testCustomFormat() throws Exception + { + assertEval("07:00 a", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p')"); + assertEval("07:00 p", "=Format(#01/10/2003 7:00:00 PM#, 'hh:nn a/p')"); + assertEval("07:00 a 6 2", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww')"); + assertEval("07:00 a 4 1", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww', 3, 3)"); + assertEval("1313", "=Format(#01/10/2003 7:13:00 AM#, 'nnnn; foo bar')"); + assertEval("1 1/10/2003 7:13:00 AM ttt this is text", + "=Format(#01/10/2003 7:13:00 AM#, 'q c ttt \"this is text\"')"); + assertEval("1 1/10/2003 ttt this is text", + "=Format(#01/10/2003#, 'q c ttt \"this is text\"')"); + assertEval("4 7:13:00 AM ttt this 'is' \"text\"", + "=Format(#7:13:00 AM#, \"q c ttt \"\"this 'is' \"\"\"\"text\"\"\"\"\"\"\")"); + assertEval("12/29/1899", "=Format('true', 'c')"); + assertEval("Tuesday, 00 Jan 2, 21:36:00 Y", + "=Format('3.9', '*~dddd, yy mmm d, hh:nn:ss \\Y[Yellow]')"); + assertEval("Tuesday, 00 Jan 01/2, 09:36:00 PM", + "=Format('3.9', 'dddd, yy mmm mm/d, hh:nn:ss AMPM')"); + assertEval("9:36:00 PM", + "=Format('3.9', 'ttttt')"); + assertEval("9:36:00 PM", + "=Format(3.9, 'ttttt')"); + assertEval("foo", + "=Format('foo', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); + + assertEvalFormat("';\\y;\\n'", + "foo", "'foo'", + "", "''", + "y", "True", + "n", "'0'", + "", "Null"); + + assertEvalFormat("'\\p;\"y\";!\\n;*~\\z[Blue];'", + "foo", "'foo'", + "", "''", + "y", "True", + "n", "'0'", + "p", "'10'", + "z", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\"'", + "p13.00blah", "13", + "-p13.00blah", "-13", + "p.00blah", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\")'", + "p13.00blah", "13", + "(p13.00blah)", "-13", + "p.00blah", "0", + "(p1.00blah)", "True", + "p.00blah", "'false'", + "p37623.292blah", "#01/02/2003 7:00:00 AM#", + "p37623.292blah", "'01/02/2003 7:00:00 AM'", + "NotANumber", "'NotANumber'", + "", "''", + "", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\";!(\"p\"#.00#\"blah\")[Red];\"zero\"'", + "p13.00blah", "13", + "(p13.00blah)", "-13", + "zero", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'\\p#.00#\"blah\";*~(\"p\"#.00#\"blah\");\"zero\";\"yuck\"'", + "p13.00blah", "13", + "(p13.00blah)", "-13", + "zero", "0", + "", "''", + "yuck", "Null"); + + assertEvalFormat("'0.##;(0.###);\"zero\";\"yuck\";'", + "0.03", "0.03", + "zero", "0.003", + "(0.003)", "-0.003", + "zero", "-0.0003"); + + assertEvalFormat("'0.##;(0.###E+0)'", + "0.03", "0.03", + "(3.E-4)", "-0.0003", + "0.", "0", + "34223.", "34223", + "(3.422E+4)", "-34223"); + + assertEvalFormat("'0.###E-0'", + "3.E-4", "0.0003", + "3.422E4", "34223" + ); + + assertEvalFormat("'0.###e+0'", + "3.e-4", "0.0003", + "3.422e+4", "34223" + ); + + assertEvalFormat("'0.###e-0'", + "3.e-4", "0.0003", + "3.422e4", "34223" + ); + + assertEvalFormat("'#,##0.###'", + "0.003", "0.003", + "0.", "0.0003", + "34,223.", "34223" + ); + + assertEvalFormat("'0.'", + "13.", "13", + "0.", "0.003", + "-45.", "-45", + "0.", "-0.003", + "0.", "0" + ); + + assertEvalFormat("'0.#'", + "13.", "13", + "0.3", "0.3", + "0.", "0.003", + "-45.", "-45", + "0.", "-0.003", + "0.", "0" + ); + + assertEvalFormat("'0'", + "13", "13", + "0", "0.003", + "-45", "-45", + "0", "-0.003", + "0", "0" + ); + + assertEvalFormat("'%0'", + "%13", "0.13", + "%0", "0.003", + "-%45", "-0.45", + "%0", "-0.003", + "%0", "0" + ); + + assertEvalFormat("'#'", + "13", "13", + "", "0.003", + "-45", "-45", + "", "-0.003", + "", "0" + ); + + assertEvalFormat("'\\0\\[#.#\\]\\0'", + "0[13.]0", "13", + "0[.]0", "0.003", + "0[.3]0", "0.3", + "-0[45.]0", "-45", + "0[.]0", "-0.003", + "-0[.3]0", "-0.3", + "0[.]0", "0" + ); + + assertEvalFormat("\"#;n'g;'\"", + "5", "5", + "n'g", "-5", + "'", "0"); + + assertEvalFormat("'$0.0#'", + "$213.0", "213"); + + assertEvalFormat("'@'", + "foo", "'foo'", + "-13", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'>@'", + "FOO", "'foo'", + "-13", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'<@'", + "foo", "'FOO'", + "-13", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'!>@;'", + "O", "'foo'", + "3", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'!>*~@[Red];\"empty\";'", + "O", "'foo'", + "3", "-13", + "0", "0", + "empty", "''", + "empty", "Null"); + + assertEvalFormat("'><@'", + "fOo", "'fOo'"); + + assertEvalFormat("'\\x@@@&&&\\y'", + "x fy", "'f'", + "x fooy", "'foo'", + "x foobay", "'fooba'", + "xfoobarybaz", "'foobarbaz'" + ); + + assertEvalFormat("'!\\x@@@&&&\\y'", + "xf y", "'f'", + "xfooy", "'foo'", + "xfoobay", "'fooba'", + "xbarbazy", "'foobarbaz'" + ); + + assertEvalFormat("'\\x&&&@@@\\y'", + "x fy", "'f'", + "xfooy", "'foo'", + "xfoobay", "'fooba'", + "xfoobarybaz", "'foobarbaz'" + ); + + assertEvalFormat("'!\\x&&&@@@\\y'", + "xf y", "'f'", + "xfoo y", "'foo'", + "xfooba y", "'fooba'", + "xbarbazy", "'foobarbaz'" + ); + } + private static void assertEvalFormat(String fmtStr, String... testStrs) { + for(int i = 0; i < testStrs.length; i+=2) { + String expected = testStrs[i]; + String val = testStrs[i + 1]; + + try { + assertEval(expected, + "=Format(" + val + ", " + fmtStr + ")"); + } catch(AssertionFailedError afe) { + throw new AssertionFailedError("Input " + val + ": " + + afe.getMessage()); + } + } } public void testNumberFuncs() throws Exception @@ -590,7 +845,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("409.090909090909", "=CStr(SYD(30000,7500,10,10))"); assertEval("-1.63048347266756E-02", "=CStr(Rate(3,200,-610,0,-20,0.1))"); - assertEval("7.70147248820175E-03", "=CStr(Rate(4*12,-200,8000))"); + assertEval("7.70147248820165E-03", "=CStr(Rate(4*12,-200,8000))"); assertEval("-1.09802980531205", "=CStr(Rate(60,93.22,5000,0.1))"); } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java index c67dfe7..67ad20b 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java @@ -20,14 +20,13 @@ import java.io.BufferedReader; import java.io.FileReader; import java.math.BigDecimal; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import javax.script.Bindings; import javax.script.SimpleBindings; import com.healthmarketscience.jackcess.DataType; -import com.healthmarketscience.jackcess.DatabaseBuilder; import com.healthmarketscience.jackcess.TestUtil; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.Expression; @@ -38,7 +37,6 @@ import com.healthmarketscience.jackcess.expr.ParseException; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.BaseEvalContext; -import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; import junit.framework.TestCase; /** @@ -318,11 +316,11 @@ public class ExpressionatorTest extends TestCase public void testDateArith() throws Exception { - assertEquals(new Date(1041508800000L), eval("=#01/02/2003# + #7:00:00 AM#")); - assertEquals(new Date(1041458400000L), eval("=#01/02/2003# - #7:00:00 AM#")); - assertEquals(new Date(1044680400000L), eval("=#01/02/2003# + '37'")); - assertEquals(new Date(1044680400000L), eval("='37' + #01/02/2003#")); - assertEquals(new Date(1041508800000L), eval("=#01/02/2003 7:00:00 AM#")); + assertEquals(LocalDateTime.of(2003,1,2,7,0), eval("=#01/02/2003# + #7:00:00 AM#")); + assertEquals(LocalDateTime.of(2003,1,1,17,0), eval("=#01/02/2003# - #7:00:00 AM#")); + assertEquals(LocalDateTime.of(2003,2,8,0,0), eval("=#01/02/2003# + '37'")); + assertEquals(LocalDateTime.of(2003,2,8,0,0), eval("='37' + #01/02/2003#")); + assertEquals(LocalDateTime.of(2003,1,2,7,0), eval("=#01/02/2003 7:00:00 AM#")); assertEquals("2/8/2003", eval("=CStr(#01/02/2003# + '37')")); assertEquals("9:24:00 AM", eval("=CStr(#7:00:00 AM# + 0.1)")); @@ -404,7 +402,7 @@ public class ExpressionatorTest extends TestCase assertEquals("foo37", eval("=\"foo\" + (12 + 25)")); assertEquals("25foo12", eval("=\"25foo\" + 12")); - assertEquals(new Date(1485579600000L), eval("=#1/1/2017# + 27")); + assertEquals(LocalDateTime.of(2017,1,28,0,0), eval("=#1/1/2017# + 27")); assertEquals(128208, eval("=#1/1/2017# * 3")); } @@ -590,15 +588,14 @@ public class ExpressionatorTest extends TestCase return TemporalConfig.US_TEMPORAL_CONFIG; } - public SimpleDateFormat createDateFormat(String formatStr) { - SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr); - sdf.setTimeZone(TestUtil.TEST_TZ); - return sdf; + public DateTimeFormatter createDateFormatter(String formatStr) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern( + formatStr, TemporalConfig.US_TEMPORAL_CONFIG.getLocale()); + return dtf; } - public Calendar getCalendar() { - return createDateFormat(getTemporalConfig().getDefaultDateTimeFormat()) - .getCalendar(); + public ZoneId getZoneId() { + return TestUtil.TEST_TZ.toZoneId(); } public NumericConfig getNumericConfig() { diff --git a/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java b/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java index f7e48f4..f2fe675 100644 --- a/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java @@ -29,9 +29,8 @@ import com.healthmarketscience.jackcess.TestUtil; import com.healthmarketscience.jackcess.impl.query.QueryImpl; import com.healthmarketscience.jackcess.impl.query.QueryImpl.Row; import junit.framework.TestCase; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; -import static org.apache.commons.lang.SystemUtils.LINE_SEPARATOR; import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; @@ -53,22 +52,22 @@ public class QueryTest extends TestCase String expr2 = "Select * from Table2"; UnionQuery query = (UnionQuery)newQuery( - Query.Type.UNION, + Query.Type.UNION, newRow(TABLE_ATTRIBUTE, expr1, null, UNION_PART1), newRow(TABLE_ATTRIBUTE, expr2, null, UNION_PART2)); setFlag(query, 3); assertEquals(multiline("Select * from Table1", - "UNION Select * from Table2;"), + "UNION Select * from Table2;"), query.toSQLString()); setFlag(query, 1); assertEquals(multiline("Select * from Table1", - "UNION ALL Select * from Table2;"), + "UNION ALL Select * from Table2;"), query.toSQLString()); - addRows(query, newRow(ORDERBY_ATTRIBUTE, "Table1.id", + addRows(query, newRow(ORDERBY_ATTRIBUTE, "Table1.id", null, null)); assertEquals(multiline("Select * from Table1", @@ -112,7 +111,7 @@ public class QueryTest extends TestCase public void testUpdateQuery() throws Exception { UpdateQuery query = (UpdateQuery)newQuery( - Query.Type.UPDATE, + Query.Type.UPDATE, newRow(TABLE_ATTRIBUTE, null, "Table1", null), newRow(COLUMN_ATTRIBUTE, "\"some string\"", null, "Table1.id"), newRow(COLUMN_ATTRIBUTE, "42", null, "Table1.col1")); @@ -122,7 +121,7 @@ public class QueryTest extends TestCase "SET Table1.id = \"some string\", Table1.col1 = 42;"), query.toSQLString()); - addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", + addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", null, null)); assertEquals( @@ -135,14 +134,14 @@ public class QueryTest extends TestCase public void testSelectQuery() throws Exception { SelectQuery query = (SelectQuery)newQuery( - Query.Type.SELECT, + Query.Type.SELECT, newRow(TABLE_ATTRIBUTE, null, "Table1", null)); setFlag(query, 1); assertEquals(multiline("SELECT *", "FROM Table1;"), query.toSQLString()); - + doTestColumns(query); doTestSelectFlags(query); doTestParameters(query); @@ -168,7 +167,7 @@ public class QueryTest extends TestCase } addRows(query, newRow(TYPE_ATTRIBUTE, null, -1, null, null)); - + try { query.getTypeRow(); fail("IllegalStateException should have been thrown"); @@ -265,7 +264,7 @@ public class QueryTest extends TestCase newRow(COLUMN_ATTRIBUTE, "54", APPEND_VALUE_FLAG, null, null), newRow(COLUMN_ATTRIBUTE, "'hello'", APPEND_VALUE_FLAG, null, null)); - assertEquals(multiline("INSERT INTO Table2", + assertEquals(multiline("INSERT INTO Table2", "VALUES (54, 'hello');"), query.toSQLString()); query = (AppendQuery)newQuery( @@ -274,7 +273,7 @@ public class QueryTest extends TestCase newRow(COLUMN_ATTRIBUTE, "54", APPEND_VALUE_FLAG, null, "ID"), newRow(COLUMN_ATTRIBUTE, "'hello'", APPEND_VALUE_FLAG, null, "Field 3")); - assertEquals(multiline("INSERT INTO Table2 (ID, [Field 3])", + assertEquals(multiline("INSERT INTO Table2 (ID, [Field 3])", "VALUES (54, 'hello');"), query.toSQLString()); } @@ -291,27 +290,27 @@ public class QueryTest extends TestCase private void doTestSelectFlags(SelectQuery query) throws Exception { setFlag(query, 3); - + assertEquals(multiline("SELECT DISTINCT Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1;"), query.toSQLString()); setFlag(query, 9); - + assertEquals(multiline("SELECT DISTINCTROW Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1;"), query.toSQLString()); setFlag(query, 7); - + assertEquals(multiline("SELECT DISTINCT Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1", "WITH OWNERACCESS OPTION;"), query.toSQLString()); - replaceRows(query, + replaceRows(query, newRow(FLAG_ATTRIBUTE, null, 49, null, "5", null)); - + assertEquals(multiline("SELECT TOP 5 PERCENT Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1;"), query.toSQLString()); @@ -354,7 +353,7 @@ public class QueryTest extends TestCase assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", "FROM Table1, Table2 AS [Another Table], [Select val from Table3].val AS Table3Val;"), - query.toSQLString()); + query.toSQLString()); } private void doTestRemoteDb(SelectQuery query) throws Exception @@ -387,7 +386,7 @@ public class QueryTest extends TestCase assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", "FROM [Select val from Table3].val AS Table3Val, Table1 INNER JOIN Table2 AS [Another Table] ON (Table1.id = [Another Table].id);"), query.toSQLString()); - + addRows(query, newRow(JOIN_ATTRIBUTE, "(Table1.id = Table3Val.id)", 2, "Table1", "Table3Val")); assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", @@ -417,13 +416,13 @@ public class QueryTest extends TestCase private void doTestWhereExpression(SelectQuery query) throws Exception { - addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", + addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", null, null)); assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", "FROM Table1, Table2 AS [Another Table], [Select val from Table3].val AS Table3Val", "WHERE (Table1.col2 < 13);"), - query.toSQLString()); + query.toSQLString()); } private void doTestGroupings(SelectQuery query) throws Exception @@ -435,7 +434,7 @@ public class QueryTest extends TestCase "FROM Table1, Table2 AS [Another Table], [Select val from Table3].val AS Table3Val", "WHERE (Table1.col2 < 13)", "GROUP BY Table1.id, SUM(Table1.val);"), - query.toSQLString()); + query.toSQLString()); } private void doTestHavingExpression(SelectQuery query) throws Exception @@ -447,7 +446,7 @@ public class QueryTest extends TestCase "WHERE (Table1.col2 < 13)", "GROUP BY Table1.id, SUM(Table1.val)", "HAVING (SUM(Table1.val) = 500);"), - query.toSQLString()); + query.toSQLString()); } private void doTestOrderings(SelectQuery query) throws Exception @@ -461,7 +460,7 @@ public class QueryTest extends TestCase "GROUP BY Table1.id, SUM(Table1.val)", "HAVING (SUM(Table1.val) = 500)", "ORDER BY Table1.id, Table2.val DESC;"), - query.toSQLString()); + query.toSQLString()); } public void testComplexJoins() throws Exception @@ -484,7 +483,7 @@ public class QueryTest extends TestCase addJoinRows(query, 1, 2, 1, 2, 1, 1); - + assertEquals(multiline("SELECT *", "FROM Table3, Table4, Table5, Table6, Table7, Table8, Table9, Table10, Table1 INNER JOIN Table2 ON (Table2.f3 = Table1.f3) AND (Table1.f0 = Table2.f0);"), query.toSQLString()); @@ -498,7 +497,7 @@ public class QueryTest extends TestCase } catch(IllegalStateException e) { // success } - + addJoinRows(query, 1, 2, 1, 3, 4, 1, 5, 6, 1, @@ -613,7 +612,7 @@ public class QueryTest extends TestCase private static void setFlag(Query query, Number newFlagNum) { - replaceRows(query, + replaceRows(query, newRow(FLAG_ATTRIBUTE, null, newFlagNum, null, null, null)); } @@ -646,7 +645,7 @@ public class QueryTest extends TestCase private static String multiline(String... strs) { - return StringUtils.join(strs, LINE_SEPARATOR); + return StringUtils.join(strs, System.lineSeparator()); } } diff --git a/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java b/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java index 87f8e6f..d0446b2 100644 --- a/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java @@ -16,9 +16,9 @@ limitations under the License. package com.healthmarketscience.jackcess.util; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Path; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.DataType; @@ -149,7 +149,7 @@ public class CustomLinkResolverTest extends TestCase @Override protected Database createTempDb(Object customFile, FileFormat format, - boolean inMemory, File tempDir, + boolean inMemory, Path tempDir, boolean readOnly) throws IOException { diff --git a/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java b/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java index 754be06..5ffdbd9 100644 --- a/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java @@ -30,7 +30,6 @@ import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.TableBuilder; import com.healthmarketscience.jackcess.impl.JetFormatTest; import junit.framework.TestCase; -import org.apache.commons.lang.SystemUtils; import static com.healthmarketscience.jackcess.TestUtil.*; /** @@ -39,7 +38,7 @@ import static com.healthmarketscience.jackcess.TestUtil.*; */ public class ExportTest extends TestCase { - private static final String NL = SystemUtils.LINE_SEPARATOR; + private static final String NL = System.lineSeparator(); public ExportTest(String name) { @@ -78,7 +77,7 @@ public class ExportTest extends TestCase new ExportUtil.Builder(db, "test") .exportWriter(new BufferedWriter(out)); - String expected = + String expected = "some text||some more,13,13.25,\"61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78\n79 7A 61 62 63 64\",true," + testDate + NL + "\"crazy'data\"\"here\",-345,-3.45E-4,61 62 63 64 65 66 67,true," + NL + "C:\\temp\\some_file.txt,25,0.0,,false," + NL; @@ -86,14 +85,14 @@ public class ExportTest extends TestCase assertEquals(expected, out.toString()); out = new StringWriter(); - + new ExportUtil.Builder(db, "test") .setHeader(true) .setDelimiter("||") .setQuote('\'') .exportWriter(new BufferedWriter(out)); - expected = + expected = "col1||col2||col3||col4||col5||col6" + NL + "'some text||some more'||13||13.25||'61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78\n79 7A 61 62 63 64'||true||" + testDate + NL + "'crazy''data\"here'||-345||-3.45E-4||61 62 63 64 65 66 67||true||" + NL + @@ -117,7 +116,7 @@ public class ExportTest extends TestCase .setFilter(oddFilter) .exportWriter(new BufferedWriter(out)); - expected = + expected = "some text||some more,13,13.25,\"61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78\n79 7A 61 62 63 64\",true," + testDate + NL + "C:\\temp\\some_file.txt,25,0.0,,false," + NL; diff --git a/src/test/java/com/healthmarketscience/jackcess/util/OleBlobTest.java b/src/test/java/com/healthmarketscience/jackcess/util/OleBlobTest.java index be3a0a9..725daa0 100644 --- a/src/test/java/com/healthmarketscience/jackcess/util/OleBlobTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/util/OleBlobTest.java @@ -34,7 +34,7 @@ import com.healthmarketscience.jackcess.impl.CompoundOleUtil; import junit.framework.TestCase; 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; import static com.healthmarketscience.jackcess.TestUtil.*; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; @@ -42,7 +42,7 @@ import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; * * @author James Ahlborn */ -public class OleBlobTest extends TestCase +public class OleBlobTest extends TestCase { public OleBlobTest(String name) { @@ -73,7 +73,7 @@ public class OleBlobTest extends TestCase } finally { ByteUtil.closeQuietly(blob); } - + try { blob = new OleBlob.Builder() .setLink(sampleFile) @@ -82,7 +82,7 @@ public class OleBlobTest extends TestCase } finally { ByteUtil.closeQuietly(blob); } - + try { blob = new OleBlob.Builder() .setPackagePrettyName("Text File") @@ -109,14 +109,14 @@ public class OleBlobTest extends TestCase assertEquals(sampleFilePath, spc.getFilePath()); assertEquals(sampleFilePath, spc.getLocalFilePath()); assertEquals(sampleFileName, spc.getFileName()); - assertEquals(OleBlob.Builder.PACKAGE_PRETTY_NAME, + assertEquals(OleBlob.Builder.PACKAGE_PRETTY_NAME, spc.getPrettyName()); - assertEquals(OleBlob.Builder.PACKAGE_TYPE_NAME, + assertEquals(OleBlob.Builder.PACKAGE_TYPE_NAME, spc.getTypeName()); - assertEquals(OleBlob.Builder.PACKAGE_TYPE_NAME, + assertEquals(OleBlob.Builder.PACKAGE_TYPE_NAME, spc.getClassName()); assertEquals(sampleFileBytes.length, spc.length()); - assertTrue(Arrays.equals(sampleFileBytes, + assertTrue(Arrays.equals(sampleFileBytes, toByteArray(spc.getStream(), spc.length()))); break; @@ -130,7 +130,7 @@ public class OleBlobTest extends TestCase assertEquals(OleBlob.Builder.PACKAGE_TYPE_NAME, lc.getTypeName()); assertEquals(OleBlob.Builder.PACKAGE_TYPE_NAME, lc.getClassName()); break; - + case 3: OleBlob.OtherContent oc = (OleBlob.OtherContent)content; assertEquals(OleBlob.ContentType.OTHER, oc.getType()); @@ -138,7 +138,7 @@ public class OleBlobTest extends TestCase assertEquals("Text.File", oc.getClassName()); assertEquals("TextFile", oc.getTypeName()); assertEquals(sampleFileBytes.length, oc.length()); - assertTrue(Arrays.equals(sampleFileBytes, + assertTrue(Arrays.equals(sampleFileBytes, toByteArray(oc.getStream(), oc.length()))); break; default: @@ -149,8 +149,8 @@ public class OleBlobTest extends TestCase } } - db.close(); - } + db.close(); + } } public void testReadBlob() throws Exception @@ -198,7 +198,7 @@ public class OleBlobTest extends TestCase } else { if("test_word.doc".equals(name)) { - checkCompoundEntries(cc, + checkCompoundEntries(cc, "/%02OlePres000", 466, "/WordDocument", 4096, "/%05SummaryInformation", 4096, @@ -210,7 +210,7 @@ public class OleBlobTest extends TestCase "/%01Ole", 20); checkCompoundStorage(cc, attach); } else if("test_excel.xls".equals(name)) { - checkCompoundEntries(cc, + checkCompoundEntries(cc, "/%02OlePres000", 1326, "/%03AccessObjSiteData", 56, "/%05SummaryInformation", 200, @@ -243,10 +243,10 @@ public class OleBlobTest extends TestCase } db.close(); - } + } } - private static void checkCompoundEntries(OleBlob.CompoundContent cc, + private static void checkCompoundEntries(OleBlob.CompoundContent cc, Object... entryInfo) throws Exception { @@ -262,7 +262,7 @@ public class OleBlobTest extends TestCase } } - private static void checkCompoundStorage(OleBlob.CompoundContent cc, + private static void checkCompoundStorage(OleBlob.CompoundContent cc, Attachment attach) throws Exception { @@ -273,7 +273,7 @@ public class OleBlobTest extends TestCase fout.write(attach.getFileData()); fout.close(); - NPOIFSFileSystem attachFs = new NPOIFSFileSystem(tmpData, true); + POIFSFileSystem attachFs = new POIFSFileSystem(tmpData, true); for(OleBlob.CompoundContent.Entry e : cc) { DocumentEntry attachE = null; @@ -284,7 +284,7 @@ public class OleBlobTest extends TestCase continue; } - byte[] attachEBytes = toByteArray(new DocumentInputStream(attachE), + byte[] attachEBytes = toByteArray(new DocumentInputStream(attachE), attachE.getSize()); byte[] entryBytes = toByteArray(e.getStream(), e.length()); @@ -292,9 +292,9 @@ public class OleBlobTest extends TestCase } ByteUtil.closeQuietly(attachFs); - + } finally { tmpData.delete(); - } + } } } diff --git a/src/test/resources/log4j_test.properties b/src/test/resources/log4j_test.properties deleted file mode 100644 index 6647228..0000000 --- a/src/test/resources/log4j_test.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.rootCategory=INFO, stdout -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=**** %-5p %d{MMM d HH:mm:ss} [%F] - %m%n - -log4j.category.com.healthmarketscience.jackcess=ERROR diff --git a/src/test/resources/logging_test.properties b/src/test/resources/logging_test.properties new file mode 100644 index 0000000..1d5324b --- /dev/null +++ b/src/test/resources/logging_test.properties @@ -0,0 +1 @@ +com.healthmarketscience.jackcess.level=SEVERE |