Make Database.escapeIdentifier public; add methods to TableBuilder and
ColumnBuilder to optionally escape names.
</action>
+ <action dev="jahlborn" type="update" issue="2997751">
+ Add support for overriding charset and tiemzone used when
+ reading/writing database.
+ </action>
+ <action dev="jahlborn" type="add" issue="3003375">
+ Add support for reading Access 97 (jet format 3) databases.
+ </action>
</release>
<release version="1.2.0" date="2010-04-18">
<action dev="bhamail" type="update" issue="1451628">
}
}
+ /**
+ * Read an unsigned variable length int from a buffer
+ * @param buffer Buffer containing the variable length int
+ * @return The unsigned int
+ */
+ public static int getUnsignedVarInt(ByteBuffer buffer, int numBytes) {
+ int pos = buffer.position();
+ int rtn = getUnsignedVarInt(buffer, pos, numBytes);
+ buffer.position(pos + numBytes);
+ return rtn;
+ }
+
+ /**
+ * Read an unsigned variable length int from a buffer
+ * @param buffer Buffer containing the variable length int
+ * @param offset Offset at which to read the value
+ * @return The unsigned int
+ */
+ public static int getUnsignedVarInt(ByteBuffer buffer, int offset,
+ int numBytes) {
+ switch(numBytes) {
+ case 1:
+ return getUnsignedByte(buffer, offset);
+ case 2:
+ return getUnsignedShort(buffer, offset);
+ case 3:
+ return get3ByteInt(buffer, offset);
+ case 4:
+ return buffer.getInt(offset);
+ default:
+ throw new IllegalArgumentException("Invalid num bytes " + numBytes);
+ }
+ }
+
+
/**
* Sets all bits in the given remaining byte range to 0.
*/
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
+import java.nio.charset.Charset;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
+import java.util.TimeZone;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
return _fixedDataOffset;
}
+ protected Charset getCharset() {
+ return getTable().getDatabase().getCharset();
+ }
+
+ protected TimeZone getTimeZone() {
+ return getTable().getDatabase().getTimeZone();
+ }
+
private void setAutoNumberGenerator()
{
if(!_autoNumber || (_type == null)) {
/**
* Returns a java long time value converted from an access date double.
*/
- private static long fromDateDouble(double value)
+ private long fromDateDouble(double value)
{
long time = Math.round(value * MILLISECONDS_PER_DAY);
time -= MILLIS_BETWEEN_EPOCH_AND_1900;
* Returns an access date double converted from a java Date/Calendar/Number
* time value.
*/
- private static double toDateDouble(Object value)
+ private double toDateDouble(Object 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!
/**
* Gets the timezone offset from UTC for the given time (including DST).
*/
- private static long getTimeZoneOffset(long time)
+ private long getTimeZoneOffset(long time)
{
- Calendar c = Calendar.getInstance();
+ Calendar c = Calendar.getInstance(getTimeZone());
c.setTimeInMillis(time);
return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET));
}
}
- return decodeUncompressedText(data, getFormat());
+ return decodeUncompressedText(data, getCharset());
} catch (IllegalInputException e) {
throw (IOException)
} else {
// handle uncompressed data
textBuf.append(decodeUncompressedText(data, dataStart, dataLength,
- getFormat()));
+ getCharset()));
}
}
* @return the decoded string
*/
private static CharBuffer decodeUncompressedText(
- byte[] textBytes, int startPos, int length, JetFormat format)
+ byte[] textBytes, int startPos, int length, Charset charset)
{
- return format.CHARSET.decode(ByteBuffer.wrap(textBytes, startPos, length));
+ return charset.decode(ByteBuffer.wrap(textBytes, startPos, length));
}
/**
}
}
- return encodeUncompressedText(text, getFormat());
+ return encodeUncompressedText(text, getCharset());
}
/**
/**
* @param textBytes bytes of text to decode
- * @param format relevant db format
+ * @param charset relevant charset
* @return the decoded string
*/
- public static String decodeUncompressedText(byte[] textBytes,
- JetFormat format)
+ public static String decodeUncompressedText(byte[] textBytes,
+ Charset charset)
{
- return decodeUncompressedText(textBytes, 0, textBytes.length, format)
+ return decodeUncompressedText(textBytes, 0, textBytes.length, charset)
.toString();
}
/**
* @param text Text to encode
- * @param format relevant db format
+ * @param db relevant db
* @return A buffer with the text encoded
*/
public static ByteBuffer encodeUncompressedText(CharSequence text,
- JetFormat format)
+ Charset charset)
{
- return format.CHARSET.encode(CharBuffer.wrap(text));
+ return charset.encode(CharBuffer.wrap(text));
}
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
+import java.util.TimeZone;
import java.util.TreeSet;
import com.healthmarketscience.jackcess.query.Query;
public static final String USE_BIG_INDEX_PROPERTY =
"com.healthmarketscience.jackcess.bigIndex";
+ /** system property which can be used to set the default TimeZone used for
+ date calculations. */
+ public static final String TIMEZONE_PROPERTY =
+ "com.healthmarketscience.jackcess.timeZone";
+
+ /** system property prefix which can be used to set the default Charset
+ used for text data (full property includes the JetFormat version). */
+ public static final String CHARSET_PROPERTY_PREFIX =
+ "com.healthmarketscience.jackcess.charset.";
+
/** default error handler used if none provided (just rethrows exception) */
public static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() {
public Object handleRowError(Column column,
public static enum FileFormat {
+ V1997(null, JetFormat.VERSION_3),
V2000("com/healthmarketscience/jackcess/empty.mdb", JetFormat.VERSION_4),
V2003("com/healthmarketscience/jackcess/empty2003.mdb", JetFormat.VERSION_4),
V2007("com/healthmarketscience/jackcess/empty2007.accdb", JetFormat.VERSION_5, ".accdb");
private ErrorHandler _dbErrorHandler;
/** the file format of the database */
private FileFormat _fileFormat;
+ /** charset to use when handling text */
+ private Charset _charset;
+ /** timezone to use when handling dates */
+ private TimeZone _timeZone;
/**
* Open an existing Database. If the existing file is not writeable, the
*/
public static Database open(File mdbFile, boolean readOnly, boolean autoSync)
throws IOException
+ {
+ return open(mdbFile, readOnly, autoSync, null, null);
+ }
+
+ /**
+ * Open an existing Database. If the existing file is not writeable or the
+ * readOnly flag is <code>true</code>, the file will be opened read-only.
+ * @param mdbFile File containing the database
+ * @param readOnly iff <code>true</code>, force opening file in read-only
+ * mode
+ * @param autoSync whether or not to enable auto-syncing on write. if
+ * {@code true}, writes will be immediately flushed to disk.
+ * This leaves the database in a (fairly) consistent state
+ * on each write, but can be very inefficient for many
+ * updates. if {@code false}, flushing to disk happens at
+ * the jvm's leisure, which can be much faster, but may
+ * leave the database in an inconsistent state if failures
+ * are encountered during writing.
+ * @param charset Charset to use, if {@code null}, uses default
+ * @param timeZone TimeZone to use, if {@code null}, uses default
+ */
+ public static Database open(File mdbFile, boolean readOnly, boolean autoSync,
+ Charset charset, TimeZone timeZone)
+ throws IOException
{
if(!mdbFile.exists() || !mdbFile.canRead()) {
throw new FileNotFoundException("given file does not exist: " + mdbFile);
}
- return new Database(openChannel(mdbFile,
- (!mdbFile.canWrite() || readOnly)),
- autoSync, null);
+
+ // force read-only for non-writable files
+ readOnly |= !mdbFile.canWrite();
+
+ // open file channel
+ FileChannel channel = openChannel(mdbFile, readOnly);
+
+ if(!readOnly) {
+
+ // verify that format supports writing
+ JetFormat jetFormat = JetFormat.getFormat(channel);
+
+ if(jetFormat.READ_ONLY) {
+ // shutdown the channel (quietly)
+ try {
+ channel.close();
+ } catch(Exception ignored) {
+ // we don't care
+ }
+ throw new IOException("jet format '" + jetFormat + "' does not support writing");
+ }
+ }
+
+ return new Database(channel, autoSync, null, charset, timeZone);
}
/**
boolean autoSync)
throws IOException
{
+ return create(fileFormat, mdbFile, autoSync, null, null);
+ }
+
+ /**
+ * Create a new Database for the given fileFormat
+ * @param fileFormat version of new database.
+ * @param mdbFile Location to write the new database to. <b>If this file
+ * already exists, it will be overwritten.</b>
+ * @param autoSync whether or not to enable auto-syncing on write. if
+ * {@code true}, writes will be immediately flushed to disk.
+ * This leaves the database in a (fairly) consistent state
+ * on each write, but can be very inefficient for many
+ * updates. if {@code false}, flushing to disk happens at
+ * the jvm's leisure, which can be much faster, but may
+ * leave the database in an inconsistent state if failures
+ * are encountered during writing.
+ * @param charset Charset to use, if {@code null}, uses default
+ * @param timeZone TimeZone to use, if {@code null}, uses default
+ */
+ public static Database create(FileFormat fileFormat, File mdbFile,
+ boolean autoSync, Charset charset,
+ TimeZone timeZone)
+ throws IOException
+ {
+ if (fileFormat.getJetFormat().READ_ONLY) {
+ throw new IOException("jet format '" + fileFormat.getJetFormat() + "' does not support writing");
+ }
+
FileChannel channel = openChannel(mdbFile, false);
channel.truncate(0);
channel.transferFrom(Channels.newChannel(
Thread.currentThread().getContextClassLoader().getResourceAsStream(
fileFormat._emptyFile)), 0, Integer.MAX_VALUE);
- return new Database(channel, autoSync, fileFormat);
+ return new Database(channel, autoSync, fileFormat, charset, timeZone);
}
/**
* @param channel File channel of the database. This needs to be a
* FileChannel instead of a ReadableByteChannel because we need to
* randomly jump around to various points in the file.
+ * @param autoSync whether or not to enable auto-syncing on write. if
+ * {@code true}, writes will be immediately flushed to disk.
+ * This leaves the database in a (fairly) consistent state
+ * on each write, but can be very inefficient for many
+ * updates. if {@code false}, flushing to disk happens at
+ * the jvm's leisure, which can be much faster, but may
+ * leave the database in an inconsistent state if failures
+ * are encountered during writing.
+ * @param fileFormat version of new database (if known)
+ * @param charset Charset to use, if {@code null}, uses default
+ * @param timeZone TimeZone to use, if {@code null}, uses default
*/
- protected Database(FileChannel channel, boolean autoSync,
- FileFormat fileFormat)
+ protected Database(FileChannel channel, boolean autoSync,
+ FileFormat fileFormat, Charset charset, TimeZone timeZone)
throws IOException
{
boolean success = false;
try {
_format = JetFormat.getFormat(channel);
+ _charset = ((charset == null) ? getDefaultCharset(_format) : charset);
_fileFormat = fileFormat;
_pageChannel = new PageChannel(channel, _format, autoSync);
+ _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone);
// note, it's slighly sketchy to pass ourselves along partially
// constructed, but only our _format and _pageChannel refs should be
// needed
_dbErrorHandler = newErrorHandler;
}
+ /**
+ * Gets currently configured TimeZone (always non-{@code null}).
+ */
+ public TimeZone getTimeZone()
+ {
+ return _timeZone;
+ }
+
+ /**
+ * Sets a new TimeZone. If {@code null}, resets to the value returned by
+ * {@link #getDefaultTimeZone}.
+ */
+ public void setTimeZone(TimeZone newTimeZone) {
+ if(newTimeZone == null) {
+ newTimeZone = getDefaultTimeZone();
+ }
+ _timeZone = newTimeZone;
+ }
+
+ /**
+ * Gets currently configured Charset (always non-{@code null}).
+ */
+ public Charset getCharset()
+ {
+ return _charset;
+ }
+
+ /**
+ * Sets a new Charset. If {@code null}, resets to the value returned by
+ * {@link #getDefaultCharset}.
+ */
+ public void setCharset(Charset newCharset) {
+ if(newCharset == null) {
+ newCharset = getDefaultCharset(getFormat());
+ }
+ _charset = newCharset;
+ }
+
/**
* Returns the FileFormat of this database (which may involve inspecting the
* database itself).
//Write the tdef page to disk.
int tdefPageNumber = Table.writeTableDefinition(columns, _pageChannel,
- _format);
+ _format, getCharset());
//Add this table to our internal list.
addTable(name, Integer.valueOf(tdefPageNumber));
}
return true;
}
+
+ /**
+ * Returns the default TimeZone. This is normally the platform default
+ * TimeZone as returned by {@link TimeZone#getDefault}, but can be
+ * overridden using the system property {@value #TIMEZONE_PROPERTY}.
+ */
+ public static TimeZone getDefaultTimeZone()
+ {
+ String tzProp = System.getProperty(TIMEZONE_PROPERTY);
+ if(tzProp != null) {
+ tzProp = tzProp.trim();
+ if(tzProp.length() > 0) {
+ return TimeZone.getTimeZone(tzProp);
+ }
+ }
+
+ // use system default
+ return TimeZone.getDefault();
+ }
+
+ /**
+ * Returns the default Charset for the given JetFormat. This may or may not
+ * be platform specific, depending on the format, but can be overridden
+ * using a system property composed of the prefix
+ * {@value #CHARSET_PROPERTY_PREFIX} followed by the JetFormat version to
+ * which the charset should apply, e.g. {@code
+ * "com.healthmarketscience.jackcess.charset.VERSION_3"}.
+ */
+ public static Charset getDefaultCharset(JetFormat format)
+ {
+ String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format);
+ if(csProp != null) {
+ csProp = csProp.trim();
+ if(csProp.length() > 0) {
+ return Charset.forName(csProp);
+ }
+ }
+
+ // use format default
+ return format.CHARSET;
+ }
/**
* Utility class for storing table page number and actual name.
umapRowNum, false);
_rootPageNumber = tableBuffer.getInt();
- tableBuffer.getInt(); //Forward past Unknown
+ ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_FLAGS); //Forward past Unknown
_indexFlags = tableBuffer.get();
- ByteUtil.forward(tableBuffer, 5); //Forward past other stuff
+ ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_FLAGS); //Forward past other stuff
}
/**
// use nested inner class to avoid problematic static init loops
private static final class PossibleFileFormats {
+ private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_3 =
+ Collections.singletonMap(Database.FileFormat.V1997, (byte[])null);
+
private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_4 =
new EnumMap<Database.FileFormat,byte[]>(Database.FileFormat.class);
/** the name of this format */
private final String _name;
+ /** the read/write mode of this format */
+ public final boolean READ_ONLY;
+
/** Database page size in bytes */
public final int PAGE_SIZE;
public final long MAX_DATABASE_SIZE;
public final int OFFSET_COLUMN_LENGTH;
public final int OFFSET_COLUMN_VARIABLE_TABLE_INDEX;
public final int OFFSET_COLUMN_FIXED_DATA_OFFSET;
+ public final int OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET;
public final int OFFSET_TABLE_DEF_LOCATION;
public final int SIZE_TDEF_TRAILER;
public final int SIZE_COLUMN_DEF_BLOCK;
public final int SIZE_INDEX_ENTRY_MASK;
+ public final int SKIP_BEFORE_INDEX_FLAGS;
+ public final int SKIP_AFTER_INDEX_FLAGS;
+ public final int SKIP_BEFORE_INDEX_SLOT;
+ public final int SKIP_AFTER_INDEX_SLOT;
+ public final int SKIP_BEFORE_INDEX;
+ public final int SIZE_NAME_LENGTH;
+ public final int SIZE_ROW_COLUMN_COUNT;
+ public final int SIZE_ROW_VAR_COL_OFFSET;
public final int USAGE_MAP_TABLE_BYTE_LENGTH;
public final Charset CHARSET;
+ public static final JetFormat VERSION_3 = new Jet3Format();
public static final JetFormat VERSION_4 = new Jet4Format();
public static final JetFormat VERSION_5 = new Jet5Format();
}
buffer.flip();
byte version = buffer.get();
- if (version == CODE_VERSION_4) {
+ if (version == CODE_VERSION_3) {
+ return VERSION_3;
+ } else if (version == CODE_VERSION_4) {
return VERSION_4;
} else if (version == CODE_VERSION_5) {
return VERSION_5;
}
throw new IOException("Unsupported " +
- ((version < CODE_VERSION_4) ? "older" : "newer") +
+ ((version < CODE_VERSION_3) ? "older" : "newer") +
" version: " + version);
}
private JetFormat(String name) {
_name = name;
+ READ_ONLY = defineReadOnly();
+
PAGE_SIZE = definePageSize();
MAX_DATABASE_SIZE = defineMaxDatabaseSize();
OFFSET_COLUMN_LENGTH = defineOffsetColumnLength();
OFFSET_COLUMN_VARIABLE_TABLE_INDEX = defineOffsetColumnVariableTableIndex();
OFFSET_COLUMN_FIXED_DATA_OFFSET = defineOffsetColumnFixedDataOffset();
+ OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET = defineOffsetColumnFixedDataRowOffset();
OFFSET_TABLE_DEF_LOCATION = defineOffsetTableDefLocation();
SIZE_TDEF_TRAILER = defineSizeTdefTrailer();
SIZE_COLUMN_DEF_BLOCK = defineSizeColumnDefBlock();
SIZE_INDEX_ENTRY_MASK = defineSizeIndexEntryMask();
-
+ SKIP_BEFORE_INDEX_FLAGS = defineSkipBeforeIndexFlags();
+ SKIP_AFTER_INDEX_FLAGS = defineSkipAfterIndexFlags();
+ SKIP_BEFORE_INDEX_SLOT = defineSkipBeforeIndexSlot();
+ SKIP_AFTER_INDEX_SLOT = defineSkipAfterIndexSlot();
+ SKIP_BEFORE_INDEX = defineSkipBeforeIndex();
+ SIZE_NAME_LENGTH = defineSizeNameLength();
+ SIZE_ROW_COLUMN_COUNT = defineSizeRowColumnCount();
+ SIZE_ROW_VAR_COL_OFFSET = defineSizeRowVarColOffset();
+
USAGE_MAP_TABLE_BYTE_LENGTH = defineUsageMapTableByteLength();
MAX_COLUMNS_PER_TABLE = defineMaxColumnsPerTable();
CHARSET = defineCharset();
}
+ protected abstract boolean defineReadOnly();
+
protected abstract int definePageSize();
protected abstract long defineMaxDatabaseSize();
protected abstract int defineOffsetColumnLength();
protected abstract int defineOffsetColumnVariableTableIndex();
protected abstract int defineOffsetColumnFixedDataOffset();
+ protected abstract int defineOffsetColumnFixedDataRowOffset();
protected abstract int defineOffsetTableDefLocation();
protected abstract int defineSizeTdefTrailer();
protected abstract int defineSizeColumnDefBlock();
protected abstract int defineSizeIndexEntryMask();
-
+ protected abstract int defineSkipBeforeIndexFlags();
+ protected abstract int defineSkipAfterIndexFlags();
+ protected abstract int defineSkipBeforeIndexSlot();
+ protected abstract int defineSkipAfterIndexSlot();
+ protected abstract int defineSkipBeforeIndex();
+ protected abstract int defineSizeNameLength();
+ protected abstract int defineSizeRowColumnCount();
+ protected abstract int defineSizeRowVarColOffset();
+
protected abstract int defineUsageMapTableByteLength();
protected abstract int defineMaxColumnsPerTable();
return _name;
}
+ private static class Jet3Format extends JetFormat {
+
+ private Jet3Format() {
+ super("VERSION_3");
+ }
+
+ @Override
+ protected boolean defineReadOnly() { return true; }
+
+ @Override
+ protected int definePageSize() { return 2048; }
+
+ @Override
+ protected long defineMaxDatabaseSize() {
+ return (1L * 1024L * 1024L * 1024L);
+ }
+
+ @Override
+ protected int defineMaxRowSize() { return 2012; }
+ @Override
+ protected int definePageInitialFreeSpace() { return PAGE_SIZE - 14; }
+
+ @Override
+ protected int defineOffsetNextTableDefPage() { return 4; }
+ @Override
+ protected int defineOffsetNumRows() { return 12; }
+ @Override
+ protected int defineOffsetNextAutoNumber() { return 20; }
+ @Override
+ protected int defineOffsetTableType() { return 20; }
+ @Override
+ protected int defineOffsetMaxCols() { return 21; }
+ @Override
+ protected int defineOffsetNumVarCols() { return 23; }
+ @Override
+ protected int defineOffsetNumCols() { return 25; }
+ @Override
+ protected int defineOffsetNumIndexSlots() { return 27; }
+ @Override
+ protected int defineOffsetNumIndexes() { return 31; }
+ @Override
+ protected int defineOffsetOwnedPages() { return 35; }
+ @Override
+ protected int defineOffsetFreeSpacePages() { return 39; }
+ @Override
+ protected int defineOffsetIndexDefBlock() { return 43; }
+
+ @Override
+ protected int defineOffsetIndexNumberBlock() { return 39; }
+
+ @Override
+ protected int defineOffsetColumnType() { return 0; }
+ @Override
+ protected int defineOffsetColumnNumber() { return 1; }
+ @Override
+ protected int defineOffsetColumnPrecision() { return 11; }
+ @Override
+ protected int defineOffsetColumnScale() { return 12; }
+ @Override
+ protected int defineOffsetColumnFlags() { return 13; }
+ @Override
+ protected int defineOffsetColumnCompressedUnicode() { return 16; }
+ @Override
+ protected int defineOffsetColumnLength() { return 16; }
+ @Override
+ protected int defineOffsetColumnVariableTableIndex() { return 3; }
+ @Override
+ protected int defineOffsetColumnFixedDataOffset() { return 14; }
+ @Override
+ protected int defineOffsetColumnFixedDataRowOffset() { return 1; }
+
+ @Override
+ protected int defineOffsetTableDefLocation() { return 4; }
+
+ @Override
+ protected int defineOffsetRowStart() { return 10; }
+ @Override
+ protected int defineOffsetUsageMapStart() { return 5; }
+
+ @Override
+ protected int defineOffsetUsageMapPageData() { return 4; }
+
+ @Override
+ protected int defineOffsetReferenceMapPageNumbers() { return 1; }
+
+ @Override
+ protected int defineOffsetFreeSpace() { return 2; }
+ @Override
+ protected int defineOffsetNumRowsOnDataPage() { return 8; }
+ @Override
+ protected int defineMaxNumRowsOnDataPage() { return 255; }
+
+ @Override
+ protected int defineOffsetIndexCompressedByteCount() { return 20; }
+ @Override
+ protected int defineOffsetIndexEntryMask() { return 22; }
+ @Override
+ protected int defineOffsetPrevIndexPage() { return 8; }
+ @Override
+ protected int defineOffsetNextIndexPage() { return 12; }
+ @Override
+ protected int defineOffsetChildTailIndexPage() { return 16; }
+
+ @Override
+ protected int defineSizeIndexDefinition() { return 8; }
+ @Override
+ protected int defineSizeColumnHeader() { return 18; }
+ @Override
+ protected int defineSizeRowLocation() { return 2; }
+ @Override
+ protected int defineSizeLongValueDef() { return 12; }
+ @Override
+ protected int defineMaxInlineLongValueSize() { return 64; }
+ @Override
+ protected int defineMaxLongValueRowSize() { return 2032; }
+ @Override
+ protected int defineSizeTdefHeader() { return 63; }
+ @Override
+ protected int defineSizeTdefTrailer() { return 2; }
+ @Override
+ protected int defineSizeColumnDefBlock() { return 25; }
+ @Override
+ protected int defineSizeIndexEntryMask() { return 226; }
+ @Override
+ protected int defineSkipBeforeIndexFlags() { return 0; }
+ @Override
+ protected int defineSkipAfterIndexFlags() { return 0; }
+ @Override
+ protected int defineSkipBeforeIndexSlot() { return 0; }
+ @Override
+ protected int defineSkipAfterIndexSlot() { return 0; }
+ @Override
+ protected int defineSkipBeforeIndex() { return 0; }
+ @Override
+ protected int defineSizeNameLength() { return 1; }
+ @Override
+ protected int defineSizeRowColumnCount() { return 1; }
+ @Override
+ protected int defineSizeRowVarColOffset() { return 1; }
+
+ @Override
+ protected int defineUsageMapTableByteLength() { return 128; }
+
+ @Override
+ protected int defineMaxColumnsPerTable() { return 255; }
+
+ @Override
+ protected int defineMaxTableNameLength() { return 64; }
+
+ @Override
+ protected int defineMaxColumnNameLength() { return 64; }
+
+ @Override
+ protected int defineMaxIndexNameLength() { return 64; }
+
+ @Override
+ protected boolean defineReverseFirstByteInDescNumericIndexes() { return false; }
+
+ @Override
+ protected Charset defineCharset() { return Charset.defaultCharset(); }
+
+ @Override
+ protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+ {
+ return PossibleFileFormats.POSSIBLE_VERSION_3;
+ }
+
+ }
+
private static class Jet4Format extends JetFormat {
private Jet4Format() {
super(name);
}
+ @Override
+ protected boolean defineReadOnly() { return false; }
+
@Override
protected int definePageSize() { return 4096; }
protected int defineOffsetColumnVariableTableIndex() { return 7; }
@Override
protected int defineOffsetColumnFixedDataOffset() { return 21; }
+ @Override
+ protected int defineOffsetColumnFixedDataRowOffset() { return 2; }
@Override
protected int defineOffsetTableDefLocation() { return 4; }
protected int defineSizeColumnDefBlock() { return 25; }
@Override
protected int defineSizeIndexEntryMask() { return 453; }
+ @Override
+ protected int defineSkipBeforeIndexFlags() { return 4; }
+ @Override
+ protected int defineSkipAfterIndexFlags() { return 5; }
+ @Override
+ protected int defineSkipBeforeIndexSlot() { return 4; }
+ @Override
+ protected int defineSkipAfterIndexSlot() { return 4; }
+ @Override
+ protected int defineSkipBeforeIndex() { return 4; }
+ @Override
+ protected int defineSizeNameLength() { return 2; }
+ @Override
+ protected int defineSizeRowColumnCount() { return 2; }
+ @Override
+ protected int defineSizeRowVarColOffset() { return 2; }
@Override
protected int defineUsageMapTableByteLength() { return 64; }
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
private static final short OVERFLOW_ROW_MASK = (short)0x4000;
+ private static final int MAX_BYTE = 256;
+
/** Table type code for system tables */
public static final byte TYPE_SYSTEM = 0x53;
/** Table type code for user tables */
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
requireNonDeletedRow(rowState, rowId);
- return getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column,
+ return getRowColumn(getFormat(), rowBuffer, getRowNullMask(rowBuffer), column,
rowState);
}
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId);
requireNonDeletedRow(rowState, rowId);
- return getRow(rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
+ return getRow(getFormat(), rowState, rowBuffer, getRowNullMask(rowBuffer), _columns,
columnNames);
}
* Saves parsed row values to the given rowState.
*/
private static Map<String, Object> getRow(
+ JetFormat format,
RowState rowState,
ByteBuffer rowBuffer,
NullMask nullMask,
if((columnNames == null) || (columnNames.contains(column.getName()))) {
// Add the value to the row data
rtn.put(column.getName(),
- getRowColumn(rowBuffer, nullMask, column, rowState));
+ getRowColumn(format, rowBuffer, nullMask, column, rowState));
}
}
return rtn;
* Reads the column data from the given row buffer. Leaves limit unchanged.
* Caches the returned value in the rowState.
*/
- private static Object getRowColumn(ByteBuffer rowBuffer,
+ private static Object getRowColumn(JetFormat format,
+ ByteBuffer rowBuffer,
NullMask nullMask,
Column column,
RowState rowState)
if(!column.isVariableLength()) {
// read fixed length value (non-boolean at this point)
- int dataStart = rowStart + 2;
+ int dataStart = rowStart + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET;
colDataPos = dataStart + column.getFixedDataOffset();
colDataLen = column.getType().getFixedSize(column.getLength());
} else {
+ int varDataStart;
+ int varDataEnd;
+
+ if(format.SIZE_ROW_VAR_COL_OFFSET == 2) {
+
+ // read simple var length value
+ int varColumnOffsetPos =
+ (rowBuffer.limit() - nullMask.byteSize() - 4) -
+ (column.getVarLenTableIndex() * 2);
+
+ varDataStart = rowBuffer.getShort(varColumnOffsetPos);
+ varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);
+
+ } else {
- // read var length value
- int varColumnOffsetPos =
- (rowBuffer.limit() - nullMask.byteSize() - 4) -
- (column.getVarLenTableIndex() * 2);
+ // read jump-table based var length values
+ short[] varColumnOffsets = readJumpTableVarColOffsets(
+ rowState, rowBuffer, rowStart, nullMask);
+
+ varDataStart = varColumnOffsets[column.getVarLenTableIndex()];
+ varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1];
+ }
- short varDataStart = rowBuffer.getShort(varColumnOffsetPos);
- short varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2);
colDataPos = rowStart + varDataStart;
colDataLen = varDataEnd - varDataStart;
}
}
}
+ static short[] readJumpTableVarColOffsets(
+ RowState rowState, ByteBuffer rowBuffer, int rowStart,
+ NullMask nullMask)
+ {
+ short[] varColOffsets = rowState.getVarColOffsets();
+ if(varColOffsets != null) {
+ return varColOffsets;
+ }
+
+ // calculate offsets using jump-table info
+ int nullMaskSize = nullMask.byteSize();
+ int rowEnd = rowStart + rowBuffer.remaining() - 1;
+ int numVarCols = ByteUtil.getUnsignedByte(rowBuffer,
+ rowEnd - nullMaskSize);
+ varColOffsets = new short[numVarCols + 1];
+
+ int rowLen = rowEnd - rowStart + 1;
+ int numJumps = (rowLen - 1) / MAX_BYTE;
+ int colOffset = rowEnd - nullMaskSize - numJumps - 1;
+
+ // If last jump is a dummy value, ignore it
+ if(((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) {
+ numJumps--;
+ }
+
+ int jumpsUsed = 0;
+ for(int i = 0; i < numVarCols + 1; i++) {
+
+ if((jumpsUsed < numJumps) &&
+ (i == ByteUtil.getUnsignedByte(
+ rowBuffer, rowEnd - nullMaskSize-jumpsUsed - 1))) {
+ jumpsUsed++;
+ }
+
+ varColOffsets[i] = (short)
+ (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i)
+ + (jumpsUsed * MAX_BYTE));
+ }
+
+ rowState.setVarColOffsets(varColOffsets);
+ return varColOffsets;
+ }
+
/**
* Reads the null mask from the given row buffer. Leaves limit unchanged.
*/
- private static NullMask getRowNullMask(ByteBuffer rowBuffer)
+ private NullMask getRowNullMask(ByteBuffer rowBuffer)
throws IOException
{
// reset position to row start
rowBuffer.reset();
-
- short columnCount = rowBuffer.getShort(); // Number of columns in this row
+
+ // Number of columns in this row
+ int columnCount = ByteUtil.getUnsignedVarInt(
+ rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT);
// read null mask
NullMask nullMask = new NullMask(columnCount);
* @return the first page of the new table's definition
*/
public static int writeTableDefinition(
- List<Column> columns, PageChannel pageChannel, JetFormat format)
+ List<Column> columns, PageChannel pageChannel, JetFormat format,
+ Charset charset)
throws IOException
{
// first, create the usage map page
format.PAGE_SIZE));
writeTableDefinitionHeader(buffer, columns, usageMapPageNumber,
totalTableDefSize, format);
- writeColumnDefinitions(buffer, columns, format);
+ writeColumnDefinitions(buffer, columns, format, charset);
//End of tabledef
buffer.put((byte) 0xff);
* @param columns List of Columns to write definitions for
*/
private static void writeColumnDefinitions(
- ByteBuffer buffer, List<Column> columns, JetFormat format)
+ ByteBuffer buffer, List<Column> columns, JetFormat format,
+ Charset charset)
throws IOException
{
short columnNumber = (short) 0;
}
}
for (Column col : columns) {
- writeName(buffer, col.getName(), format);
+ writeName(buffer, col.getName(), charset);
}
}
* {@link #readName}.
*/
private static void writeName(ByteBuffer buffer, String name,
- JetFormat format)
+ Charset charset)
{
- ByteBuffer encName = Column.encodeUncompressedText(
- name, format);
+ ByteBuffer encName = Column.encodeUncompressedText(name, charset);
buffer.putShort((short) encName.remaining());
buffer.put(encName);
}
for (int i = 0; i < _indexSlotCount; i++) {
- tableBuffer.getInt(); //Forward past Unknown
+ ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
tableBuffer.getInt(); //Forward past alternate index number
int indexNumber = tableBuffer.getInt();
ByteUtil.forward(tableBuffer, 11);
byte indexType = tableBuffer.get();
- ByteUtil.forward(tableBuffer, 4);
+ ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_SLOT); //Skip past Unknown
if(i < firstRealIdx) {
// ignore this info
// go back to index column info after sorting
tableBuffer.position(idxOffset);
for (int i = 0; i < _indexCount; i++) {
- tableBuffer.getInt(); //Forward past Unknown
+ ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown
_indexes.get(i).read(tableBuffer, _columns);
}
}
/**
- * Returns a name read from the buffer at the current position. The
- * expected name format is the name length as a short followed by (length *
- * 2) bytes encoded using the {@link JetFormat#CHARSET}
+ * Returns a name read from the buffer at the current position. The
+ * expected name format is the name length followed by the name
+ * encoded using the {@link JetFormat#CHARSET}
*/
- private String readName(ByteBuffer buffer) {
- int nameLength = ByteUtil.getUnsignedShort(buffer);
+ private String readName(ByteBuffer buffer) {
+ int nameLength = readNameLength(buffer);
byte[] nameBytes = new byte[nameLength];
buffer.get(nameBytes);
- return Column.decodeUncompressedText(nameBytes, getFormat());
+ return Column.decodeUncompressedText(nameBytes,
+ getDatabase().getCharset());
}
/**
* expected name format is the same as that for {@link #readName}.
*/
private void skipName(ByteBuffer buffer) {
- int nameLength = ByteUtil.getUnsignedShort(buffer);
+ int nameLength = readNameLength(buffer);
ByteUtil.forward(buffer, nameLength);
}
+ /**
+ * Returns a name length read from the buffer at the current position.
+ */
+ private int readNameLength(ByteBuffer buffer) {
+ return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH);
+ }
+
/**
* Converts a map of columnName -> columnValue to an array of row values
* appropriate for a call to {@link #addRow(Object...)}.
for(Column column : _columns) {
if(column.isAutoNumber() ||
(row[column.getColumnIndex()] == Column.KEEP_VALUE)) {
- row[column.getColumnIndex()] = getRowColumn(rowBuffer, nullMask,
+ row[column.getColumnIndex()] = getRowColumn(getFormat(), rowBuffer, nullMask,
column, rowState);
}
}
private int _lastModCount;
/** optional error handler to use when row errors are encountered */
private ErrorHandler _errorHandler;
+ /** cached variable column offsets for jump-table based rows */
+ private short[] _varColOffsets;
private RowState(TempBufferHolder.Type headerType) {
_headerRowBufferH = TempPageHolder.newHolder(headerType);
_rowsOnHeaderPage = 0;
_status = RowStateStatus.INIT;
_rowStatus = RowStatus.INIT;
+ _varColOffsets = null;
if(_haveRowValues) {
Arrays.fill(_rowValues, null);
_haveRowValues = false;
public Object[] getRowValues() {
return dupeRow(_rowValues, _rowValues.length);
}
+
+ private short[] getVarColOffsets() {
+ return _varColOffsets;
+ }
+
+ private void setVarColOffsets(short[] varColOffsets) {
+ _varColOffsets = varColOffsets;
+ }
public RowId getHeaderRowId() {
return _headerRowId;
public void testComplexIndex() throws Exception
{
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX)) {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX, true)) {
// this file has an index with "compressed" entries and node pages
Database db = open(testDB);
Table t = db.getTable("Table1");
}
public void testReadDeletedRows() throws Exception {
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL)) {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL, true)) {
Table table = open(testDB).getTable("Table");
int rows = 0;
while (table.getNextRow() != null) {
}
public void testGetColumns() throws Exception {
- for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
List<Column> columns = open(testDB).getTable("Table1").getColumns();
assertEquals(9, columns.size());
}
public void testGetNextRow() throws Exception {
- for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
final Database db = open(testDB);
assertEquals(4, db.getTableNames().size());
final Table table = db.getTable("Table1");
assertEquals(Boolean.FALSE, row.get("I"));
}
- public void testCreate() throws Exception {
+ public void testCreate() throws Exception {
for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
Database db = create(fileFormat);
assertEquals(0, db.getTableNames().size());
public void testReadLongValue() throws Exception {
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.TEST2)) {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.TEST2, true)) {
Database db = open(testDB);
Table table = db.getTable("MSP_PROJECTS");
Map<String, Object> row = table.getNextRow();
}
public void testReadWithDeletedCols() throws Exception {
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL_COL)) {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL_COL, true)) {
Table table = open(testDB).getTable("Table1");
Map<String, Object> expectedRow0 = new LinkedHashMap<String, Object>();
public void testMultiPageTableDef() throws Exception
{
- for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
List<Column> columns = open(testDB).getTable("Table2").getColumns();
assertEquals(89, columns.size());
}
}
public void testPrimaryKey() throws Exception {
- for (final TestDB testDB : SUPPORTED_DBS_TEST) {
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
Table table = open(testDB).getTable("Table1");
Map<String, Boolean> foundPKs = new HashMap<String, Boolean>();
for(Index index : table.getIndexes()) {
public void testIndexSlots() throws Exception
{
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX)) {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) {
Database mdb = open(testDB);
Table table = mdb.getTable("Table1");
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import junit.framework.TestCase;
import static com.healthmarketscience.jackcess.Database.*;
+import static com.healthmarketscience.jackcess.DatabaseTest.*;
/**
* @author Dan Rollo
public static enum Basename {
BIG_INDEX("bigIndexTest"),
- COMP_INDEX("compIndexTest"),
- DEL_COL("delColTest"),
- DEL("delTest"),
- FIXED_NUMERIC("fixedNumericTest"),
- FIXED_TEXT("fixedTextTest"),
- INDEX_CURSOR("indexCursorTest"),
- INDEX("indexTest"),
- OVERFLOW("overflowTest"),
- QUERY("queryTest"),
- TEST("test"),
- TEST2("test2"),
- INDEX_CODES("testIndexCodes"),
- INDEX_PROPERTIES("testIndexProperties"),
- PROMOTION("testPromotion"),
- ;
+ COMP_INDEX("compIndexTest"),
+ DEL_COL("delColTest"),
+ DEL("delTest"),
+ FIXED_NUMERIC("fixedNumericTest"),
+ FIXED_TEXT("fixedTextTest"),
+ INDEX_CURSOR("indexCursorTest"),
+ INDEX("indexTest"),
+ OVERFLOW("overflowTest"),
+ QUERY("queryTest"),
+ TEST("test"),
+ TEST2("test2"),
+ INDEX_CODES("testIndexCodes"),
+ INDEX_PROPERTIES("testIndexProperties"),
+ PROMOTION("testPromotion");
private final String _basename;
runtime via the system property
"com.healthmarketscience.jackcess.testFormats") */
final static FileFormat[] SUPPORTED_FILEFORMATS;
+ final static FileFormat[] SUPPORTED_FILEFORMATS_FOR_READ;
static {
String testFormatStr = System.getProperty("com.healthmarketscience.jackcess.testFormats");
}
List<FileFormat> supported = new ArrayList<FileFormat>();
- for(FileFormat ff : Arrays.asList(FileFormat.V2000, FileFormat.V2003,
- FileFormat.V2007)) {
+ List<FileFormat> supportedForRead = new ArrayList<FileFormat>();
+ for(FileFormat ff : FileFormat.values()) {
if(!testFormats.contains(ff)) {
continue;
}
+ supportedForRead.add(ff);
+ if(ff.getJetFormat().READ_ONLY) {
+ continue;
+ }
supported.add(ff);
}
SUPPORTED_FILEFORMATS = supported.toArray(new FileFormat[0]);
+ SUPPORTED_FILEFORMATS_FOR_READ =
+ supportedForRead.toArray(new FileFormat[0]);
}
/**
}
public static List<TestDB> getSupportedForBasename(Basename basename) {
+ return getSupportedForBasename(basename, false);
+ }
+
+ public static List<TestDB> getSupportedForBasename(Basename basename,
+ boolean readOnly) {
List<TestDB> supportedTestDBs = new ArrayList<TestDB>();
- for (FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
- supportedTestDBs.add(new TestDB(
- getFileForBasename(basename, fileFormat),
- fileFormat));
+ for (FileFormat fileFormat :
+ (readOnly ? SUPPORTED_FILEFORMATS_FOR_READ :
+ SUPPORTED_FILEFORMATS)) {
+ File testFile = getFileForBasename(basename, fileFormat);
+ if(!testFile.exists()) {
+ continue;
+ }
+
+ // verify that the db is the file format expected
+ try {
+// System.out.println("FOO checking " + testFile);
+ Database db = Database.open(testFile, true);
+ FileFormat dbFileFormat = db.getFileFormat();
+ db.close();
+ if(dbFileFormat != fileFormat) {
+ throw new IllegalStateException("Expected " + fileFormat +
+ " was " + dbFileFormat);
+ }
+ } catch(Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ supportedTestDBs.add(new TestDB(testFile, fileFormat));
}
return supportedTestDBs;
}
}
}
- private static final File UNSUPPORTED_TEST_V1997 =
- new File(DIR_TEST_DATA, "V1997" + File.separator +
- Basename.TEST + "V1997.mdb");
-
static final List<TestDB> SUPPORTED_DBS_TEST =
TestDB.getSupportedForBasename(Basename.TEST);
+ static final List<TestDB> SUPPORTED_DBS_TEST_FOR_READ =
+ TestDB.getSupportedForBasename(Basename.TEST, true);
public void testGetFormat() throws Exception {
// success
}
- checkUnsupportedJetFormat(UNSUPPORTED_TEST_V1997);
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
+
+ final FileChannel channel = Database.openChannel(testDB.dbFile, false);
+ try {
+
+ JetFormat fmtActual = JetFormat.getFormat(channel);
+ assertEquals("Unexpected JetFormat for dbFile: " +
+ testDB.dbFile.getAbsolutePath(),
+ testDB.expectedFileFormat.getJetFormat(), fmtActual);
+
+ } finally {
+ channel.close();
+ }
- for (final TestDB testDB : SUPPORTED_DBS_TEST) {
- checkJetFormat(testDB);
}
}
- private static void checkJetFormat(final TestDB testDB)
- throws IOException {
+ public void testReadOnlyFormat() throws Exception {
- final FileChannel channel = Database.openChannel(testDB.dbFile, false);
- try {
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
+
+ Database db = null;
+ IOException failure = null;
+ try {
+ db = openCopy(testDB);
+ } catch(IOException e) {
+ failure = e;
+ } finally {
+ if(db != null) {
+ db.close();
+ }
+ }
- JetFormat fmtActual = JetFormat.getFormat(channel);
- assertEquals("Unexpected JetFormat for dbFile: " +
- testDB.dbFile.getAbsolutePath(),
- testDB.expectedFileFormat.getJetFormat(), fmtActual);
+ if(!testDB.getExpectedFormat().READ_ONLY) {
+ assertNull(failure);
+ } else {
+ assertTrue(failure.getMessage().contains("does not support writing"));
+ }
- } finally {
- channel.close();
}
}
- private static void checkUnsupportedJetFormat(File testDB)
- throws IOException {
+ public void testFileFormat() throws Exception {
- final FileChannel channel = Database.openChannel(testDB, false);
- try {
- JetFormat.getFormat(channel);
- fail("Unexpected JetFormat for dbFile: " +
- testDB.getAbsolutePath());
- } catch(IOException ignored) {
- // success
- } finally {
- channel.close();
+ for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) {
+
+ Database db = null;
+ try {
+ db = open(testDB);
+ assertEquals(testDB.getExpectedFileFormat(), db.getFileFormat());
+ } finally {
+ if(db != null) {
+ db.close();
+ }
+ }
}
}
}
public void testSimple() throws Exception {
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX)) {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) {
Database db = open(testDB);
Table t1 = db.getTable("Table1");
Table t2 = db.getTable("Table2");
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.TimeZone;
import junit.framework.TestCase;
public Table getTable() {
return _testTable;
}
+ @Override
+ protected Charset getCharset() {
+ return getFormat().CHARSET;
+ }
+ @Override
+ protected TimeZone getTimeZone() {
+ return TimeZone.getDefault();
+ }
};
}