git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1235 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-3.0.0
@@ -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 { |
@@ -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); | |||
} |
@@ -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; | |||
} |
@@ -29,4 +29,8 @@ public class InvalidValueException extends JackcessException | |||
public InvalidValueException(String msg) { | |||
super(msg); | |||
} | |||
public InvalidValueException(String msg, Throwable cause) { | |||
super(msg, cause); | |||
} | |||
} |
@@ -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). |
@@ -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()); | |||
} | |||
} | |||
} |
@@ -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; |
@@ -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); | |||
} |
@@ -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"); |
@@ -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() { |