Browse Source

initial support for LocalDateTime and Temporal types

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

+ 29
- 27
src/main/java/com/healthmarketscience/jackcess/DataType.java View 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 {

+ 34
- 2
src/main/java/com/healthmarketscience/jackcess/Database.java View 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);
}

+ 31
- 0
src/main/java/com/healthmarketscience/jackcess/DateTimeType.java View File

@@ -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;
}

+ 4
- 0
src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java View 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);
}
}

+ 7
- 0
src/main/java/com/healthmarketscience/jackcess/Row.java View 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).

+ 304
- 52
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java View 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());
}
}
}

+ 45
- 1
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java View 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;

+ 5
- 0
src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java View 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);
}

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

+ 11
- 11
src/test/java/com/healthmarketscience/jackcess/TableTest.java View 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() {

Loading…
Cancel
Save