]> source.dussan.org Git - jackcess.git/commitdiff
add system prop for date/time type; rework how date/times are written based on date...
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 28 Dec 2018 04:19:21 +0000 (04:19 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 28 Dec 2018 04:19:21 +0000 (04:19 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1253 f203690c-595d-4dc9-a70b-905162fa7fd2

src/main/java/com/healthmarketscience/jackcess/Database.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/DateTimeContext.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java [deleted file]
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java
src/test/java/com/healthmarketscience/jackcess/TestUtil.java

index 13e45a50200c961f4aa7f47babe5058cd3ef854e..4fa874118998cc6108728b5b46565cdbf7d8eb60 100644 (file)
@@ -106,7 +106,7 @@ public interface Database extends Iterable<Table>, Closeable, Flushable
     "com.healthmarketscience.jackcess.brokenNio";
 
   /** system property which can be used to set the default sort order for
-   *  table columns.  Value should be one {@link Table.ColumnOrder} enum
+   *  table columns.  Value should be one of {@link Table.ColumnOrder} enum
    *  values.
    * @usage _intermediate_field_
    */
@@ -134,6 +134,13 @@ public interface Database extends Iterable<Table>, Closeable, Flushable
   public static final String ENABLE_EXPRESSION_EVALUATION_PROPERTY =
     "com.healthmarketscience.jackcess.enableExpressionEvaluation";
 
+  /** system property which can be used to set the default date/Time type.
+   * Value should be one of {@link DateTimeType} enum values.
+   * @usage _general_field_
+   */
+  public static final String DATE_TIME_TYPE_PROPERTY =
+    "com.healthmarketscience.jackcess.dateTimeType";
+
   /**
    * Enum which indicates which version of Access created the database.
    * @usage _general_class_
index 6ab88ab3fedfd7b09d580ca65b281071a6796e33..41728746ce3fa88c7d746eae5835fd3a82d4f300 100644 (file)
@@ -76,7 +76,7 @@ import org.apache.commons.logging.LogFactory;
  * @author Tim McCune
  * @usage _intermediate_class_
  */
-public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
+public class ColumnImpl implements Column, Comparable<ColumnImpl>, DateTimeContext
 {
 
   protected static final Log LOG = LogFactory.getLog(ColumnImpl.class);
@@ -506,7 +506,8 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
     return getDatabase().getZoneId();
   }
 
-  protected DateTimeFactory getDateTimeFactory() {
+  @Override
+  public DateTimeFactory getDateTimeFactory() {
     return getDatabase().getDateTimeFactory();
   }
 
@@ -1042,21 +1043,12 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
    * Date/Calendar/Number/Temporal time value.
    * @usage _advanced_method_
    */
-  private static double toDateDouble(Object value, ZoneContext zc)
-  {
-    if(value instanceof TemporalAccessor) {
-      return toDateDouble(toLocalDateTime((TemporalAccessor)value, zc));
-    }
-
-    // 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, zc.getTimeZone());
-    return toLocalDateDouble(time);
+  private static double toDateDouble(Object value, DateTimeContext dtc) {
+    return dtc.getDateTimeFactory().toDateDouble(value, dtc);
   }
 
   private static LocalDateTime toLocalDateTime(
-      TemporalAccessor value, ZoneContext zc) {
+      TemporalAccessor value, DateTimeContext dtc) {
 
     // handle some common Temporal types
     if(value instanceof LocalDateTime) {
@@ -1065,10 +1057,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
     if(value instanceof ZonedDateTime) {
       // if the temporal value has a timezone, convert it to this db's timezone
       return ((ZonedDateTime)value).withZoneSameInstant(
-          zc.getZoneId()).toLocalDateTime();
+          dtc.getZoneId()).toLocalDateTime();
     }
     if(value instanceof Instant) {
-      return LocalDateTime.ofInstant((Instant)value, zc.getZoneId());
+      return LocalDateTime.ofInstant((Instant)value, dtc.getZoneId());
     }
     if(value instanceof LocalDate) {
       return ((LocalDate)value).atTime(BASE_LT);
@@ -1092,7 +1084,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
       if(zone != null) {
         // the Temporal has a zone, see if it is the right zone.  if not,
         // adjust it
-        ZoneId zoneId = zc.getZoneId();
+        ZoneId zoneId = dtc.getZoneId();
         if(!zoneId.equals(zone)) {
           return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId)
             .toLocalDateTime();
@@ -1107,6 +1099,16 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
     }
   }
 
+  private static Instant toInstant(TemporalAccessor value, DateTimeContext dtc) {
+    if(value instanceof ZonedDateTime) {
+      return ((ZonedDateTime)value).toInstant();
+    }
+    if(value instanceof Instant) {
+      return (Instant)value;
+    }
+    return toLocalDateTime(value, dtc).atZone(dtc.getZoneId()).toInstant();
+  }
+
   static double toLocalDateDouble(long time) {
     time += MILLIS_BETWEEN_EPOCH_AND_1900;
 
@@ -2191,7 +2193,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
     }
   }
 
-  static DateTimeFactory getDateTimeFactory(DateTimeType type) {
+  protected static DateTimeFactory getDateTimeFactory(DateTimeType type) {
     return ((type == DateTimeType.LOCAL_DATE_TIME) ?
             LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY);
   }
@@ -2676,19 +2678,21 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
   /**
    * Factory which handles date/time values appropriately for a DateTimeType.
    */
-  static abstract class DateTimeFactory
+  protected static abstract class DateTimeFactory
   {
     public abstract DateTimeType getType();
 
     public abstract Object fromDateBits(ColumnImpl col, long dateBits);
 
+    public abstract double toDateDouble(Object value, DateTimeContext dtc);
+
     public abstract Object toInternalValue(DatabaseImpl db, Object value);
   }
 
   /**
    * Factory impl for legacy Date handling.
    */
-  static final class DefaultDateTimeFactory extends DateTimeFactory
+  private static final class DefaultDateTimeFactory extends DateTimeFactory
   {
     @Override
     public DateTimeType getType() {
@@ -2702,6 +2706,23 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
       return new DateExt(time, dateBits);
     }
 
+    @Override
+    public double toDateDouble(Object value, DateTimeContext dtc) {
+      // ZoneId and TimeZone have different rules for older timezones, so we
+      // need to consistently use one or the other depending on the date/time
+      // type
+      long time = 0L;
+      if(value instanceof TemporalAccessor) {
+        time = toInstant((TemporalAccessor)value, dtc).toEpochMilli();
+      } else {
+        time = toDateLong(value);
+      }
+      // seems access stores dates in the local timezone.  guess you just
+      // hope you read it in the same timezone in which it was written!
+      time += getToLocalTimeZoneOffset(time, dtc.getTimeZone());
+      return toLocalDateDouble(time);
+    }
+
     @Override
     public Object toInternalValue(DatabaseImpl db, Object value) {
       return ((value instanceof Date) ? value :
@@ -2712,7 +2733,7 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
   /**
    * Factory impl for LocalDateTime handling.
    */
-  static final class LDTDateTimeFactory extends DateTimeFactory
+  private static final class LDTDateTimeFactory extends DateTimeFactory
   {
     @Override
     public DateTimeType getType() {
@@ -2724,6 +2745,18 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, ZoneContext
       return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits));
     }
 
+    @Override
+    public double toDateDouble(Object value, DateTimeContext dtc) {
+      // ZoneId and TimeZone have different rules for older timezones, so we
+      // need to consistently use one or the other depending on the date/time
+      // type
+      if(!(value instanceof TemporalAccessor)) {
+        value = Instant.ofEpochMilli(toDateLong(value));
+      }
+      return ColumnImpl.toDateDouble(
+          toLocalDateTime((TemporalAccessor)value, dtc));
+    }
+
     @Override
     public Object toInternalValue(DatabaseImpl db, Object value) {
       if(value instanceof TemporalAccessor) {
index 1a34dbd65da49c94746721d209991df604315094..c005651456ed3055e0f9efd2341e78112ff3da6a 100644 (file)
@@ -88,7 +88,7 @@ import org.apache.commons.logging.LogFactory;
  * @author Tim McCune
  * @usage _intermediate_class_
  */
-public class DatabaseImpl implements Database, ZoneContext
+public class DatabaseImpl implements Database, DateTimeContext
 {
   private static final Log LOG = LogFactory.getLog(DatabaseImpl.class);
 
@@ -346,8 +346,7 @@ public class DatabaseImpl implements Database, ZoneContext
   /** shared context for evaluating expressions */
   private DBEvalContext _evalCtx;
   /** factory for the appropriate date/time type */
-  private ColumnImpl.DateTimeFactory _dtf =
-    ColumnImpl.getDateTimeFactory(DateTimeType.DATE);
+  private ColumnImpl.DateTimeFactory _dtf;
 
   /**
    * Open an existing Database.  If the existing file is not writeable or the
@@ -537,8 +536,9 @@ public class DatabaseImpl implements Database, ZoneContext
     _allowAutoNumInsert = getDefaultAllowAutoNumberInsert();
     _evaluateExpressions = getDefaultEvaluateExpressions();
     _fileFormat = fileFormat;
-    _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync);
     setZoneInfo(timeZone, null);
+    _dtf = ColumnImpl.getDateTimeFactory(getDefaultDateTimeType());
+    _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync);
     if(provider == null) {
       provider = DefaultCodecProvider.INSTANCE;
     }
@@ -710,7 +710,8 @@ public class DatabaseImpl implements Database, ZoneContext
     _dtf = ColumnImpl.getDateTimeFactory(dateTimeType);
   }
 
-  protected ColumnImpl.DateTimeFactory getDateTimeFactory() {
+  @Override
+  public ColumnImpl.DateTimeFactory getDateTimeFactory() {
     return _dtf;
   }
 
@@ -2043,16 +2044,8 @@ public class DatabaseImpl implements Database, ZoneContext
    */
   public static Table.ColumnOrder getDefaultColumnOrder()
   {
-    String coProp = System.getProperty(COLUMN_ORDER_PROPERTY);
-    if(coProp != null) {
-      coProp = coProp.trim();
-      if(coProp.length() > 0) {
-        return Table.ColumnOrder.valueOf(coProp);
-      }
-    }
-
-    // use default order
-    return DEFAULT_COLUMN_ORDER;
+    return getEnumSystemProperty(Table.ColumnOrder.class, COLUMN_ORDER_PROPERTY,
+                                 DEFAULT_COLUMN_ORDER);
   }
 
   /**
@@ -2100,6 +2093,17 @@ public class DatabaseImpl implements Database, ZoneContext
     return false;
   }
 
+  /**
+   * Returns the default DateTimeType.  This defaults to
+   * {@link DateTimeType#DATE}, but can be overridden using the system
+   * property {@value com.healthmarketscience.jackcess.Database#DATE_TIME_TYPE_PROPERTY}.
+   * @usage _advanced_method_
+   */
+  public static DateTimeType getDefaultDateTimeType() {
+    return getEnumSystemProperty(DateTimeType.class, DATE_TIME_TYPE_PROPERTY,
+                                 DateTimeType.DATE);
+  }
+
   /**
    * Copies the given db InputStream to the given channel using the most
    * efficient means possible.
@@ -2196,6 +2200,19 @@ public class DatabaseImpl implements Database, ZoneContext
     return msg + " (Db=" + dbName + ")";
   }
 
+  private static <E extends Enum<E>> E getEnumSystemProperty(
+      Class<E> enumClass, String propName, E defaultValue)
+  {
+    String prop = System.getProperty(propName);
+    if(prop != null) {
+      prop = prop.trim().toUpperCase();
+      if(!prop.isEmpty()) {
+        return Enum.valueOf(enumClass, prop);
+      }
+    }
+    return defaultValue;
+  }
+
   /**
    * Utility class for storing table page number and actual name.
    */
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java
new file mode 100644 (file)
index 0000000..8045755
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+Copyright (c) 2018 James Ahlborn
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package com.healthmarketscience.jackcess.impl;
+
+import java.time.ZoneId;
+import java.util.TimeZone;
+
+/**
+ * Provider of zone related info for date/time conversions.
+ *
+ * @author James Ahlborn
+ */
+interface DateTimeContext
+{
+  public ZoneId getZoneId();
+
+  public TimeZone getTimeZone();
+
+  public ColumnImpl.DateTimeFactory getDateTimeFactory();
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java
deleted file mode 100644 (file)
index 0134e1f..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
-Copyright (c) 2018 James Ahlborn
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package com.healthmarketscience.jackcess.impl;
-
-import java.time.ZoneId;
-import java.util.TimeZone;
-
-/**
- * Provider of zone related info for date/time conversions.
- *
- * @author James Ahlborn
- */
-interface ZoneContext
-{
-  public ZoneId getZoneId();
-
-  public TimeZone getTimeZone();
-}
index 1aa24833d069874aeaf03b02384742c1357e05b8..0a33a9997d7da26cddb69da2e9a3d17cfe6ba901 100644 (file)
@@ -995,6 +995,10 @@ public class DatabaseTest extends TestCase
       public TimeZone getTimeZone() { return tz; }
       @Override
       public ZoneId getZoneId() { return null; }
+      @Override
+      public ColumnImpl.DateTimeFactory getDateTimeFactory() {
+        return getDateTimeFactory(DateTimeType.DATE);
+      }
     };
 
     SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd");
index 637629aaf8a2d4d5bc8087a17188a0a27c1ffbfc..e17bbfb7e438c0c0f13b55c46df60203018572a4 100644 (file)
@@ -62,7 +62,71 @@ public class LocalDateTimeTest extends TestCase
     super(name);
   }
 
-  public void testAncientDates() throws Exception
+  public void testWriteAndReadLocalDate() throws Exception {
+    for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+      Database db = createMem(fileFormat);
+
+      db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME);
+
+      Table table = new TableBuilder("test")
+        .addColumn(new ColumnBuilder("name", DataType.TEXT))
+        .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME))
+        .toTable(db);
+
+      // since jackcess does not really store millis, shave them off before
+      // storing the current date/time
+      long curTimeNoMillis = (System.currentTimeMillis() / 1000L);
+      curTimeNoMillis *= 1000L;
+
+      DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
+      List<Date> dates =
+        new ArrayList<Date>(
+            Arrays.asList(
+                df.parse("19801231 00:00:00"),
+                df.parse("19930513 14:43:27"),
+                null,
+                df.parse("20210102 02:37:00"),
+                new Date(curTimeNoMillis)));
+
+      Calendar c = Calendar.getInstance();
+      for(int year = 1801; year < 2050; year +=3) {
+        for(int month = 0; month <= 12; ++month) {
+          for(int day = 1; day < 29; day += 3) {
+            c.clear();
+            c.set(Calendar.YEAR, year);
+            c.set(Calendar.MONTH, month);
+            c.set(Calendar.DAY_OF_MONTH, day);
+            dates.add(c.getTime());
+          }
+        }
+      }
+
+      ((DatabaseImpl)db).getPageChannel().startWrite();
+      try {
+        for(Date d : dates) {
+          table.addRow("row " + d, d);
+        }
+      } finally {
+        ((DatabaseImpl)db).getPageChannel().finishWrite();
+      }
+
+      List<LocalDateTime> foundDates = new ArrayList<LocalDateTime>();
+      for(Row row : table) {
+        foundDates.add(row.getLocalDateTime("date"));
+      }
+
+      assertEquals(dates.size(), foundDates.size());
+      for(int i = 0; i < dates.size(); ++i) {
+        Date expected = dates.get(i);
+        LocalDateTime found = foundDates.get(i);
+        assertSameDate(expected, found);
+      }
+
+      db.close();
+    }
+  }
+
+  public void testAncientLocalDates() throws Exception
   {
     ZoneId zoneId = ZoneId.of("America/New_York");
     DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu-MM-dd");
@@ -130,6 +194,10 @@ public class LocalDateTimeTest extends TestCase
       public TimeZone getTimeZone() { return tz; }
       @Override
       public ZoneId getZoneId() { return zoneId; }
+      @Override
+      public ColumnImpl.DateTimeFactory getDateTimeFactory() {
+        return getDateTimeFactory(DateTimeType.LOCAL_DATE_TIME);
+      }
     };
 
     SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd");
index b5db277724287c526444772d8a4eff78d36a89d1..83b2d7df2425ff6559f6c53f7486a04460f8046f 100644 (file)
@@ -27,6 +27,10 @@ import java.io.PrintWriter;
 import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -379,6 +383,22 @@ public class TestUtil
     }
   }
 
+  static void assertSameDate(Date expected, LocalDateTime found)
+  {
+    if((expected == null) && (found == null)) {
+      return;
+    }
+    if((expected == null) || (found == null)) {
+      throw new AssertionError("Expected " + expected + ", found " + found);
+    }
+
+    LocalDateTime expectedLdt = LocalDateTime.ofInstant(
+        Instant.ofEpochMilli(expected.getTime()),
+        ZoneId.systemDefault());
+
+    Assert.assertEquals(expectedLdt, found);
+  }
+
   static void copyFile(File srcFile, File dstFile)
     throws IOException
   {