git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1253 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-3.0.0
@@ -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_ |
@@ -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) { |
@@ -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. | |||
*/ |
@@ -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(); | |||
} |
@@ -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"); |
@@ -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"); |
@@ -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 | |||
{ |