From: James Ahlborn Date: Tue, 11 Dec 2018 02:07:56 +0000 (+0000) Subject: initial support for LocalDateTime and Temporal types X-Git-Tag: jackcess-3.0.0~7^2~26 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=abf32c90b101a0c1f76d89e8cdf80cd24e72f6c8;p=jackcess.git initial support for LocalDateTime and Temporal types git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1235 f203690c-595d-4dc9-a70b-905162fa7fd2 --- 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 DATA_TYPES = new HashMap(); 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, Closeable, Flushable * database while an Iterator is in use. * @usage _general_method_ */ + @Override public Iterator
iterator(); /** @@ -325,6 +327,7 @@ public interface Database extends Iterable
, Closeable, Flushable * databases) to disk. * @usage _general_method_ */ + @Override public void flush() throws IOException; /** @@ -335,6 +338,7 @@ public interface Database extends Iterable
, Closeable, Flushable * OutputStream or jdbc Connection). * @usage _general_method_ */ + @Override public void close() throws IOException; /** @@ -383,17 +387,33 @@ public interface Database extends Iterable
, 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
, 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; @@ -93,6 +94,12 @@ public interface Row extends Map */ 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). + */ + 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). 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 { /** * 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 { 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 { 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 { /** * 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 { // _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 { * @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 { */ 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 { 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 { * 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 { 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 { } } + 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 { /** * 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 { 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 { 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 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() {