Browse Source

add system prop for date/time type; rework how date/times are written based on date/time type

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1253 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-3.0.0
James Ahlborn 5 years ago
parent
commit
0f568a4620

+ 8
- 1
src/main/java/com/healthmarketscience/jackcess/Database.java View 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_

+ 54
- 21
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java View 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) {

+ 32
- 15
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java View 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.
*/

src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java → src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java View File

@@ -24,9 +24,11 @@ import java.util.TimeZone;
*
* @author James Ahlborn
*/
interface ZoneContext
interface DateTimeContext
{
public ZoneId getZoneId();

public TimeZone getTimeZone();

public ColumnImpl.DateTimeFactory getDateTimeFactory();
}

+ 4
- 0
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java View 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");

+ 69
- 1
src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java View 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");

+ 20
- 0
src/test/java/com/healthmarketscience/jackcess/TestUtil.java View 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
{

Loading…
Cancel
Save