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_
*/
*/
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),
/**
* 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.
* 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
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()) {
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);
}
minSize, defaultSize, maxSize,
false, 0, 0, 0, 0, 0, 0, unitSize);
}
-
+
private DataType(byte value, Integer sqlType, Integer fixedSize,
boolean variableLength,
boolean longValue,
_maxPrecision = maxPrecision;
_unitSize = unitSize;
}
-
+
public byte getValue() {
return _value;
}
-
+
public boolean isVariableLength() {
return _variableLength;
}
// e.g. NUMERIC
return (isVariableLength() && (getMinSize() != getMaxSize()));
}
-
+
public boolean isLongValue() {
return _longValue;
}
public int getFixedSize() {
return getFixedSize(null);
}
-
+
public int getFixedSize(Short colLength) {
if(_fixedSize != null) {
if(colLength != null) {
if(colLength != null) {
return colLength;
}
- throw new IllegalArgumentException("Unexpected fixed length column " +
+ throw new IllegalArgumentException("Unexpected fixed length column " +
this);
}
public int getMaxSize() {
return _maxSize;
}
-
+
public int getSQLType() throws SQLException {
if (_sqlType != null) {
return _sqlType;
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;
}
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());
}
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) {
}
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
{
rtn = altRtn;
}
}
-
+
return rtn;
}
* 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 {
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;
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;
/**
* 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
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_
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() {
/**
* 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
// _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));
}
}
* @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);
+ }
}
/**
*/
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) {
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) ?
* 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());
}
/**
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:
}
}
+ 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());
}
/**
* 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;
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)
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());
+ }
+ }
}