"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_
*/
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_
* @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);
return getDatabase().getZoneId();
}
- protected DateTimeFactory getDateTimeFactory() {
+ @Override
+ public DateTimeFactory getDateTimeFactory() {
return getDatabase().getDateTimeFactory();
}
* 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) {
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);
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();
}
}
+ 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;
}
}
- 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);
}
/**
* 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() {
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 :
/**
* Factory impl for LocalDateTime handling.
*/
- static final class LDTDateTimeFactory extends DateTimeFactory
+ private static final class LDTDateTimeFactory extends DateTimeFactory
{
@Override
public DateTimeType getType() {
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) {
* @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);
/** 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
_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;
}
_dtf = ColumnImpl.getDateTimeFactory(dateTimeType);
}
- protected ColumnImpl.DateTimeFactory getDateTimeFactory() {
+ @Override
+ public ColumnImpl.DateTimeFactory getDateTimeFactory() {
return _dtf;
}
*/
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);
}
/**
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.
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.
*/
--- /dev/null
+/*
+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();
+}
+++ /dev/null
-/*
-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();
-}
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");
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");
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");
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;
}
}
+ 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
{