aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/DataType.java56
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/Database.java36
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/DateTimeType.java31
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java4
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/Row.java7
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java356
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java46
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java5
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java5
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/TableTest.java22
10 files changed, 474 insertions, 94 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/DataType.java b/src/main/java/com/healthmarketscience/jackcess/DataType.java
index 6850ab6..11483b1 100644
--- a/src/main/java/com/healthmarketscience/jackcess/DataType.java
+++ b/src/main/java/com/healthmarketscience/jackcess/DataType.java
@@ -24,13 +24,14 @@ 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;
/**
* Supported access data types.
- *
+ *
* @author Tim McCune
* @usage _general_class_
*/
@@ -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),
/**
@@ -104,7 +106,7 @@ public enum DataType {
* null}. Equivalent to SQL {@link Types#VARCHAR}, {@link Types#CHAR}.
*/
TEXT((byte) 0x0A, Types.VARCHAR, null, true, false, 0,
- JetFormat.TEXT_FIELD_MAX_LENGTH, JetFormat.TEXT_FIELD_MAX_LENGTH,
+ JetFormat.TEXT_FIELD_MAX_LENGTH, JetFormat.TEXT_FIELD_MAX_LENGTH,
JetFormat.TEXT_FIELD_UNIT_SIZE),
/**
* Corresponds to a java {@code byte[]} of max length 16777215 bytes.
@@ -151,7 +153,7 @@ public enum DataType {
* Complex type corresponds to a special {@link #LONG} autonumber field
* which is the key for a secondary table which holds the "real" data.
*/
- COMPLEX_TYPE((byte) 0x12, null, 4),
+ COMPLEX_TYPE((byte) 0x12, null, 4),
/**
* Corresponds to a java {@link Long}. Accepts any {@link Number} (using
* {@link Number#longValue}), Boolean as 1 or 0, any Object converted to a
@@ -206,7 +208,7 @@ public enum DataType {
addNewSqlType("TIME_WITH_TIMEZONE", SHORT_DATE_TIME, null);
addNewSqlType("TIMESTAMP_WITH_TIMEZONE", SHORT_DATE_TIME, null);
}
-
+
private static Map<Byte, DataType> DATA_TYPES = new HashMap<Byte, DataType>();
static {
for (DataType type : DataType.values()) {
@@ -249,11 +251,11 @@ public enum DataType {
private final int _maxPrecision;
/** the number of bytes per "unit" for this data type */
private final int _unitSize;
-
+
private DataType(byte value) {
this(value, null, null);
}
-
+
private DataType(byte value, Integer sqlType, Integer fixedSize) {
this(value, sqlType, fixedSize, false, false, 0, 0, 0, 1);
}
@@ -269,7 +271,7 @@ public enum DataType {
minSize, defaultSize, maxSize,
false, 0, 0, 0, 0, 0, 0, unitSize);
}
-
+
private DataType(byte value, Integer sqlType, Integer fixedSize,
boolean variableLength,
boolean longValue,
@@ -301,11 +303,11 @@ public enum DataType {
_maxPrecision = maxPrecision;
_unitSize = unitSize;
}
-
+
public byte getValue() {
return _value;
}
-
+
public boolean isVariableLength() {
return _variableLength;
}
@@ -315,7 +317,7 @@ public enum DataType {
// e.g. NUMERIC
return (isVariableLength() && (getMinSize() != getMaxSize()));
}
-
+
public boolean isLongValue() {
return _longValue;
}
@@ -327,7 +329,7 @@ public enum DataType {
public int getFixedSize() {
return getFixedSize(null);
}
-
+
public int getFixedSize(Short colLength) {
if(_fixedSize != null) {
if(colLength != null) {
@@ -338,7 +340,7 @@ public enum DataType {
if(colLength != null) {
return colLength;
}
- throw new IllegalArgumentException("Unexpected fixed length column " +
+ throw new IllegalArgumentException("Unexpected fixed length column " +
this);
}
@@ -353,7 +355,7 @@ public enum DataType {
public int getMaxSize() {
return _maxSize;
}
-
+
public int getSQLType() throws SQLException {
if (_sqlType != null) {
return _sqlType;
@@ -368,19 +370,19 @@ public enum DataType {
public int getDefaultScale() {
return _defaultScale;
}
-
+
public int getMaxScale() {
return _maxScale;
}
-
+
public int getMinPrecision() {
return _minPrecision;
}
-
+
public int getDefaultPrecision() {
return _defaultPrecision;
}
-
+
public int getMaxPrecision() {
return _maxPrecision;
}
@@ -414,7 +416,7 @@ public enum DataType {
private static boolean isWithinRange(int value, int minValue, int maxValue) {
return((value >= minValue) && (value <= maxValue));
}
-
+
public int toValidSize(int size) {
return toValidRange(size, getMinSize(), getMaxSize());
}
@@ -442,12 +444,12 @@ public enum DataType {
public boolean isUnsupported() {
return((this == UNSUPPORTED_FIXEDLEN) || (this == UNSUPPORTED_VARLEN));
}
-
+
private static int toValidRange(int value, int minValue, int maxValue) {
return((value > maxValue) ? maxValue :
((value < minValue) ? minValue : value));
}
-
+
public static DataType fromByte(byte b) throws IOException {
DataType rtn = DATA_TYPES.get(b);
if (rtn != null) {
@@ -455,13 +457,13 @@ public enum DataType {
}
throw new IOException("Unrecognized data type: " + b);
}
-
+
public static DataType fromSQLType(int sqlType)
throws SQLException
{
return fromSQLType(sqlType, 0, null);
}
-
+
public static DataType fromSQLType(int sqlType, int lengthInUnits)
throws SQLException
{
@@ -504,7 +506,7 @@ public enum DataType {
rtn = altRtn;
}
}
-
+
return rtn;
}
@@ -512,7 +514,7 @@ public enum DataType {
* Adds mappings for a sql type which was added after jdk 1.5 (using
* reflection).
*/
- private static void addNewSqlType(String typeName, DataType type,
+ private static void addNewSqlType(String typeName, DataType type,
DataType altType)
{
try {
diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java
index 6a7bcc4..3a7b65a 100644
--- a/src/main/java/com/healthmarketscience/jackcess/Database.java
+++ b/src/main/java/com/healthmarketscience/jackcess/Database.java
@@ -22,6 +22,7 @@ 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;
@@ -207,6 +208,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable
* database while an Iterator is in use.
* @usage _general_method_
*/
+ @Override
public Iterator<Table> iterator();
/**
@@ -325,6 +327,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable
* databases) to disk.
* @usage _general_method_
*/
+ @Override
public void flush() throws IOException;
/**
@@ -335,6 +338,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable
* OutputStream or jdbc Connection).
* @usage _general_method_
*/
+ @Override
public void close() throws IOException;
/**
@@ -383,18 +387,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_
*/
@@ -498,4 +518,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/DateTimeType.java b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java
new file mode 100644
index 0000000..7f5cdb1
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java
@@ -0,0 +1,31 @@
+/*
+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.
+ *
+ * @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/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/Row.java b/src/main/java/com/healthmarketscience/jackcess/Row.java
index 4e43541..6917628 100644
--- a/src/main/java/com/healthmarketscience/jackcess/Row.java
+++ b/src/main/java/com/healthmarketscience/jackcess/Row.java
@@ -20,6 +20,7 @@ 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;
@@ -95,6 +96,12 @@ public interface Row extends Map<String,Object>
/**
* Convenience method which gets the value for the row with the given name,
+ * casting it to a LocalDateTime (DataType SHORT_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);
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
index 8fa1906..f5b7d5b 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
@@ -32,11 +32,23 @@ 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.Temporal;
+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 +56,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;
@@ -79,8 +92,9 @@ 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;
/**
* Access starts counting dates at Dec 30, 1899 (note, this strange date
@@ -91,6 +105,16 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
static final long MILLIS_BETWEEN_EPOCH_AND_1900 =
25569L * MILLISECONDS_PER_DAY;
+ static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30);
+ static final LocalTime BASE_LT = LocalTime.of(0, 0);
+ static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT);
+
+ private static final DateTimeFactory DEF_DATE_TIME_FACTORY =
+ new DefaultDateTimeFactory();
+
+ private static final DateTimeFactory LDT_DATE_TIME_FACTORY =
+ new LDTDateTimeFactory();
+
/**
* mask for the fixed len bit
* @usage _advanced_field_
@@ -455,8 +479,16 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return getDatabase().getCharset();
}
- protected Calendar getCalendar() {
- return getDatabase().getCalendar();
+ protected TimeZone getTimeZone() {
+ return getDatabase().getTimeZone();
+ }
+
+ protected ZoneId getZoneId() {
+ return getDatabase().getZoneId();
+ }
+
+ protected DateTimeFactory getDateTimeFactory() {
+ return getDatabase().getDateTimeFactory();
}
public boolean isAppendOnly() {
@@ -881,45 +913,43 @@ 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());
+ public static long fromDateDouble(double value, DatabaseImpl db) {
+ return fromDateDouble(value, db.getTimeZone());
}
/**
* Returns a java long time value converted from an access date double.
* @usage _advanced_method_
*/
- public static long fromDateDouble(double value, Calendar c)
- {
+ @Deprecated
+ public static long fromDateDouble(double value, Calendar c) {
+ // FIXME, remove me
+ return fromDateDouble(value, c.getTimeZone());
+ }
+
+ public 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)
- {
+ public static long fromLocalDateDouble(double value) {
long datePart = ((long)value) * MILLISECONDS_PER_DAY;
// the fractional part of the double represents the time. it is always
@@ -927,29 +957,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 = durationFromLocalDateDouble1900(value);
+ return BASE_LDT.plus(dateTimeOffset);
+ }
+
+ private static Duration durationFromLocalDateDouble1900(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 timeNanos = Math.round((secondsDouble % 1.0d) * NANOS_PER_SECOND);
+
+ return Duration.ofSeconds(dateSeconds + timeSeconds, timeNanos);
+ }
+
+
+
/**
* 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));
}
}
@@ -960,8 +1010,13 @@ 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, getTimeZone(), getZoneId());
+ } catch(IllegalArgumentException iae) {
+ throw new InvalidValueException(withErrorContext(iae.getMessage()), iae);
+ }
}
/**
@@ -971,25 +1026,100 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
*/
public static double toDateDouble(Object value, DatabaseImpl db)
{
- return toDateDouble(value, db.getCalendar());
+ return toDateDouble(value, db.getTimeZone(), db.getZoneId());
}
/**
- * 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, Calendar c)
+ @Deprecated
+ public static double toDateDouble(Object value, Calendar c) {
+ // FIXME remove me
+ return toDateDouble(value, c.getTimeZone());
+ }
+
+ public static double toDateDouble(Object value, TimeZone tz)
{
+ return toDateDouble(value, tz, null);
+ }
+
+ /**
+ * Returns an access date double converted from a java
+ * Date/Calendar/Number/Temporal time value.
+ * @usage _advanced_method_
+ */
+ public static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId)
+ {
+ if(value instanceof TemporalAccessor) {
+ return toDateDouble(toLocalDateTime((Temporal)value, tz, zoneId));
+ }
+
// 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);
+ time += getToLocalTimeZoneOffset(time, tz);
return toLocalDateDouble(time);
}
- static double toLocalDateDouble(long time)
- {
+ private static LocalDateTime toLocalDateTime(
+ TemporalAccessor value, TimeZone tz, ZoneId zoneId) {
+
+ // 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(
+ getZoneId(tz, zoneId)).toLocalDateTime();
+ }
+ if(value instanceof Instant) {
+ return LocalDateTime.ofInstant((Instant)value, getZoneId(tz, zoneId));
+ }
+ 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 = getZoneId(tz, zoneId);
+ 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);
+ }
+ }
+
+ private static ZoneId getZoneId(TimeZone tz, ZoneId zoneId) {
+ return ((zoneId != null) ? zoneId : tz.toZoneId());
+ }
+
+ static double toLocalDateDouble(long time) {
time += MILLIS_BETWEEN_EPOCH_AND_1900;
if(time < 0L) {
@@ -1003,11 +1133,36 @@ 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 toLocalDateDouble1900(dateTimeOffset);
+ }
+
+ private static double toLocalDateDouble1900(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();
+
+ double timeDouble = ((((double)timeNanos / NANOS_PER_SECOND) + timeSeconds)
+ / SECONDS_PER_DAY);
+
+ double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY);
+
+ if(dateSeconds < 0) {
+ timeDouble = -timeDouble;
+ }
+
+ return dateDouble + timeDouble;
+ }
+
/**
* @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) ?
@@ -1019,24 +1174,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());
}
/**
@@ -1990,8 +2140,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return ((value instanceof Double) ? value :
toNumber(value, db).doubleValue());
case SHORT_DATE_TIME:
- return ((value instanceof DateExt) ? value :
- new Date(toDateLong(value)));
+ return db.getDateTimeFactory().toInternalValue(db, value);
case TEXT:
case MEMO:
case GUID:
@@ -2011,6 +2160,11 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
}
}
+ 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());
}
@@ -2028,8 +2182,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;
@@ -2046,6 +2202,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)
@@ -2450,4 +2641,65 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
sb.append("allowZeroLength=false");
}
}
+
+ /**
+ * Factory which handles date/time values appropriately for a DateTimeType.
+ */
+ static abstract class DateTimeFactory
+ {
+ public abstract DateTimeType getType();
+
+ public abstract Object fromDateBits(ColumnImpl col, long dateBits);
+
+ public abstract Object toInternalValue(DatabaseImpl db, Object value);
+ }
+
+ /**
+ * Factory impl for legacy Date handling.
+ */
+ 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 Object toInternalValue(DatabaseImpl db, Object value) {
+ return ((value instanceof Date) ? value :
+ new Date(toDateLong(value)));
+ }
+ }
+
+ /**
+ * Factory impl for LocalDateTime handling.
+ */
+ 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 Object toInternalValue(DatabaseImpl db, Object value) {
+ if(value instanceof TemporalAccessor) {
+ return toLocalDateTime((TemporalAccessor)value, null, db.getZoneId());
+ }
+ Instant inst = Instant.ofEpochMilli(toDateLong(value));
+ return LocalDateTime.ofInstant(inst, db.getZoneId());
+ }
+ }
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
index 03e32c8..f132a99 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
@@ -32,6 +32,7 @@ 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;
@@ -56,6 +57,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;
@@ -303,6 +305,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) */
@@ -342,6 +346,9 @@ public class DatabaseImpl implements Database
private Calendar _calendar;
/** shared context for evaluating expressions */
private DBEvalContext _evalCtx;
+ /** factory for the appropriate date/time type */
+ private ColumnImpl.DateTimeFactory _dtf =
+ ColumnImpl.getDateTimeFactory(DateTimeType.DATE);
/**
* Open an existing Database. If the existing file is not writeable or the
@@ -644,15 +651,38 @@ 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;
+ }
+
+ 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;
+ _zoneId = newZoneId;
+
// clear cached calendar(s) when timezone is changed
_calendar = null;
if(_evalCtx != null) {
@@ -660,6 +690,20 @@ public class DatabaseImpl implements Database
}
}
+ @Override
+ public DateTimeType getDateTimeType() {
+ return _dtf.getType();
+ }
+
+ @Override
+ public void setDateTimeType(DateTimeType dateTimeType) {
+ _dtf = ColumnImpl.getDateTimeFactory(dateTimeType);
+ }
+
+ protected ColumnImpl.DateTimeFactory getDateTimeFactory() {
+ return _dtf;
+ }
+
public Charset getCharset()
{
return _charset;
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java
index 66d9c80..0e6fe6e 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;
@@ -94,6 +95,10 @@ public class RowImpl extends LinkedHashMap<String,Object> implements Row
return (Date)get(name);
}
+ public LocalDateTime getLocalDateTime(String name) {
+ return (LocalDateTime)get(name);
+ }
+
public byte[] getBytes(String name) {
return (byte[])get(name);
}
diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
index 2ce343b..de6bd94 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;
@@ -990,7 +991,9 @@ 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); }
+ protected TimeZone getTimeZone() { return tz; }
+ @Override
+ protected ZoneId getZoneId() { return null; }
};
SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd");
diff --git a/src/test/java/com/healthmarketscience/jackcess/TableTest.java b/src/test/java/com/healthmarketscience/jackcess/TableTest.java
index 339ba39..3bc2dbd 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();
+ protected TimeZone getTimeZone() {
+ return TimeZone.getDefault();
}
@Override
public boolean isCompressedUnicode() {