]> source.dussan.org Git - jackcess.git/commitdiff
initial support for LocalDateTime and Temporal types
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 11 Dec 2018 02:07:56 +0000 (02:07 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 11 Dec 2018 02:07:56 +0000 (02:07 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1235 f203690c-595d-4dc9-a70b-905162fa7fd2

src/main/java/com/healthmarketscience/jackcess/DataType.java
src/main/java/com/healthmarketscience/jackcess/Database.java
src/main/java/com/healthmarketscience/jackcess/DateTimeType.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java
src/main/java/com/healthmarketscience/jackcess/Row.java
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
src/test/java/com/healthmarketscience/jackcess/TableTest.java

index 6850ab681df0d6e9e5cfb0232174e7720f903ab6..11483b143bb38ef27c6aa44c0cdee06249f9940b 100644 (file)
@@ -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 {
index 6a7bcc4b4c1054824b13d02b59cfdd8789565669..3a7b65afa72bd68cb942d1064829ff38ee9accdb 100644 (file)
@@ -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,17 +387,33 @@ 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 (file)
index 0000000..7f5cdb1
--- /dev/null
@@ -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;
+}
index adffc0f36a710cee22892c775b0810f5b143eba3..2e161d236f67bfda8f1e361e61a1623f7c267b48 100644 (file)
@@ -29,4 +29,8 @@ public class InvalidValueException extends JackcessException
   public InvalidValueException(String msg) {
     super(msg);
   }
+
+  public InvalidValueException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
 }
index 4e43541c78f5dc7de8bab41ee4451ed8755cc441..69176285d61879e80e5ef56654aac8c158e696e6 100644 (file)
@@ -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<String,Object>
    */
   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).
index 8fa1906cab11317dd5a4a3ccbbde8e0a747302f7..f5b7d5b1e590a59395cad17f8d6971db141a9eba 100644 (file)
@@ -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());
+    }
+  }
 }
index 03e32c8553767b3f14d135eabde56e12fc67c6c6..f132a9927e1e44e35501d234ab00ab7d44e271ff 100644 (file)
@@ -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;
index 66d9c80127f183514964f6895d7c875280821b97..0e6fe6e41ce44077d62082fad9e68459b804bb72 100644 (file)
@@ -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);
   }
index 2ce343bba35e66f5c54f87f39770a68f60d722b6..de6bd949b11d0931e20f6bcc6794ce59d2769683 100644 (file)
@@ -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");
index 339ba390407984d487c4a474b360c98cb288978b..3bc2dbd6d4dec44905f6af451c5045d3d3da935a 100644 (file)
@@ -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() {