git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@475 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-1.2.1
@@ -17,6 +17,13 @@ | |||
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"> |
@@ -256,6 +256,41 @@ public final class ByteUtil { | |||
} | |||
} | |||
/** | |||
* 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. | |||
*/ |
@@ -39,12 +39,14 @@ import java.math.BigInteger; | |||
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; | |||
@@ -345,6 +347,14 @@ public class Column implements Comparable<Column> { | |||
return _fixedDataOffset; | |||
} | |||
protected Charset getCharset() { | |||
return getTable().getDatabase().getCharset(); | |||
} | |||
protected TimeZone getTimeZone() { | |||
return getTable().getDatabase().getTimeZone(); | |||
} | |||
private void setAutoNumberGenerator() | |||
{ | |||
if(!_autoNumber || (_type == null)) { | |||
@@ -732,7 +742,7 @@ public class Column implements Comparable<Column> { | |||
/** | |||
* 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; | |||
@@ -763,7 +773,7 @@ public class Column implements Comparable<Column> { | |||
* 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! | |||
@@ -780,9 +790,9 @@ public class Column implements Comparable<Column> { | |||
/** | |||
* 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)); | |||
} | |||
@@ -1239,7 +1249,7 @@ public class Column implements Comparable<Column> { | |||
} | |||
return decodeUncompressedText(data, getFormat()); | |||
return decodeUncompressedText(data, getCharset()); | |||
} catch (IllegalInputException e) { | |||
throw (IOException) | |||
@@ -1273,7 +1283,7 @@ public class Column implements Comparable<Column> { | |||
} else { | |||
// handle uncompressed data | |||
textBuf.append(decodeUncompressedText(data, dataStart, dataLength, | |||
getFormat())); | |||
getCharset())); | |||
} | |||
} | |||
@@ -1282,9 +1292,9 @@ public class Column implements Comparable<Column> { | |||
* @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)); | |||
} | |||
/** | |||
@@ -1320,7 +1330,7 @@ public class Column implements Comparable<Column> { | |||
} | |||
} | |||
return encodeUncompressedText(text, getFormat()); | |||
return encodeUncompressedText(text, getCharset()); | |||
} | |||
/** | |||
@@ -1364,25 +1374,25 @@ public class Column implements Comparable<Column> { | |||
/** | |||
* @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)); | |||
} | |||
@@ -37,6 +37,7 @@ import java.io.RandomAccessFile; | |||
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; | |||
@@ -52,6 +53,7 @@ import java.util.List; | |||
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; | |||
@@ -100,6 +102,16 @@ public class Database | |||
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, | |||
@@ -173,6 +185,7 @@ public class Database | |||
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"); | |||
@@ -301,6 +314,10 @@ public class Database | |||
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 | |||
@@ -355,13 +372,58 @@ public class Database | |||
*/ | |||
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); | |||
} | |||
/** | |||
@@ -438,12 +500,40 @@ public class Database | |||
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); | |||
} | |||
/** | |||
@@ -471,17 +561,30 @@ public class Database | |||
* @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 | |||
@@ -556,6 +659,44 @@ public class Database | |||
_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). | |||
@@ -747,7 +888,7 @@ public class Database | |||
//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)); | |||
@@ -1255,6 +1396,47 @@ public class Database | |||
} | |||
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. |
@@ -366,9 +366,9 @@ public abstract class Index implements Comparable<Index> { | |||
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 | |||
} | |||
/** |
@@ -70,6 +70,9 @@ public abstract class JetFormat { | |||
// 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); | |||
@@ -89,6 +92,9 @@ public abstract class JetFormat { | |||
/** 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; | |||
@@ -120,6 +126,7 @@ public abstract class JetFormat { | |||
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; | |||
@@ -150,6 +157,14 @@ public abstract class JetFormat { | |||
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; | |||
@@ -162,6 +177,7 @@ public abstract class JetFormat { | |||
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(); | |||
@@ -178,19 +194,23 @@ public abstract class JetFormat { | |||
} | |||
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(); | |||
@@ -221,6 +241,7 @@ public abstract class JetFormat { | |||
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(); | |||
@@ -251,7 +272,15 @@ public abstract class JetFormat { | |||
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(); | |||
@@ -264,6 +293,8 @@ public abstract class JetFormat { | |||
CHARSET = defineCharset(); | |||
} | |||
protected abstract boolean defineReadOnly(); | |||
protected abstract int definePageSize(); | |||
protected abstract long defineMaxDatabaseSize(); | |||
@@ -294,6 +325,7 @@ public abstract class JetFormat { | |||
protected abstract int defineOffsetColumnLength(); | |||
protected abstract int defineOffsetColumnVariableTableIndex(); | |||
protected abstract int defineOffsetColumnFixedDataOffset(); | |||
protected abstract int defineOffsetColumnFixedDataRowOffset(); | |||
protected abstract int defineOffsetTableDefLocation(); | |||
@@ -324,7 +356,15 @@ public abstract class JetFormat { | |||
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(); | |||
@@ -343,6 +383,175 @@ public abstract class JetFormat { | |||
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() { | |||
@@ -353,6 +562,9 @@ public abstract class JetFormat { | |||
super(name); | |||
} | |||
@Override | |||
protected boolean defineReadOnly() { return false; } | |||
@Override | |||
protected int definePageSize() { return 4096; } | |||
@@ -412,6 +624,8 @@ public abstract class JetFormat { | |||
protected int defineOffsetColumnVariableTableIndex() { return 7; } | |||
@Override | |||
protected int defineOffsetColumnFixedDataOffset() { return 21; } | |||
@Override | |||
protected int defineOffsetColumnFixedDataRowOffset() { return 2; } | |||
@Override | |||
protected int defineOffsetTableDefLocation() { return 4; } | |||
@@ -465,6 +679,22 @@ public abstract class JetFormat { | |||
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; } |
@@ -29,6 +29,7 @@ package com.healthmarketscience.jackcess; | |||
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; | |||
@@ -61,6 +62,8 @@ public class Table | |||
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 */ | |||
@@ -403,7 +406,7 @@ public class Table | |||
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); | |||
requireNonDeletedRow(rowState, rowId); | |||
return getRowColumn(rowBuffer, getRowNullMask(rowBuffer), column, | |||
return getRowColumn(getFormat(), rowBuffer, getRowNullMask(rowBuffer), column, | |||
rowState); | |||
} | |||
@@ -421,7 +424,7 @@ public class Table | |||
ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); | |||
requireNonDeletedRow(rowState, rowId); | |||
return getRow(rowState, rowBuffer, getRowNullMask(rowBuffer), _columns, | |||
return getRow(getFormat(), rowState, rowBuffer, getRowNullMask(rowBuffer), _columns, | |||
columnNames); | |||
} | |||
@@ -430,6 +433,7 @@ public class Table | |||
* Saves parsed row values to the given rowState. | |||
*/ | |||
private static Map<String, Object> getRow( | |||
JetFormat format, | |||
RowState rowState, | |||
ByteBuffer rowBuffer, | |||
NullMask nullMask, | |||
@@ -444,7 +448,7 @@ public class Table | |||
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; | |||
@@ -454,7 +458,8 @@ public class Table | |||
* 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) | |||
@@ -484,19 +489,34 @@ public class Table | |||
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; | |||
} | |||
@@ -523,16 +543,61 @@ public class Table | |||
} | |||
} | |||
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); | |||
@@ -685,7 +750,8 @@ public class Table | |||
* @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 | |||
@@ -710,7 +776,7 @@ public class Table | |||
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); | |||
@@ -834,7 +900,8 @@ public class Table | |||
* @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; | |||
@@ -896,7 +963,7 @@ public class Table | |||
} | |||
} | |||
for (Column col : columns) { | |||
writeName(buffer, col.getName(), format); | |||
writeName(buffer, col.getName(), charset); | |||
} | |||
} | |||
@@ -905,10 +972,9 @@ public class Table | |||
* {@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); | |||
} | |||
@@ -1041,12 +1107,12 @@ public class Table | |||
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 | |||
@@ -1077,7 +1143,7 @@ public class Table | |||
// 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); | |||
} | |||
@@ -1115,15 +1181,16 @@ public class Table | |||
} | |||
/** | |||
* 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()); | |||
} | |||
/** | |||
@@ -1131,10 +1198,17 @@ public class Table | |||
* 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...)}. | |||
@@ -1307,7 +1381,7 @@ public class Table | |||
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); | |||
} | |||
} | |||
@@ -1929,6 +2003,8 @@ public class Table | |||
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); | |||
@@ -1955,6 +2031,7 @@ public class Table | |||
_rowsOnHeaderPage = 0; | |||
_status = RowStateStatus.INIT; | |||
_rowStatus = RowStatus.INIT; | |||
_varColOffsets = null; | |||
if(_haveRowValues) { | |||
Arrays.fill(_rowValues, null); | |||
_haveRowValues = false; | |||
@@ -2036,6 +2113,14 @@ public class Table | |||
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; |
@@ -67,7 +67,7 @@ public class BigIndexTest extends TestCase { | |||
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"); |
@@ -176,7 +176,7 @@ public class DatabaseTest extends TestCase { | |||
} | |||
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) { | |||
@@ -187,7 +187,7 @@ public class DatabaseTest extends TestCase { | |||
} | |||
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()); | |||
@@ -213,7 +213,7 @@ public class DatabaseTest extends TestCase { | |||
} | |||
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"); | |||
@@ -272,7 +272,7 @@ public class DatabaseTest extends TestCase { | |||
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()); | |||
@@ -396,7 +396,7 @@ public class DatabaseTest extends TestCase { | |||
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(); | |||
@@ -528,7 +528,7 @@ public class DatabaseTest extends TestCase { | |||
} | |||
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>(); | |||
@@ -733,7 +733,7 @@ public class DatabaseTest extends TestCase { | |||
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()); | |||
} |
@@ -84,7 +84,7 @@ public class IndexTest extends TestCase { | |||
} | |||
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()) { | |||
@@ -100,7 +100,7 @@ public class IndexTest extends TestCase { | |||
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"); |
@@ -4,7 +4,6 @@ import java.io.File; | |||
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; | |||
@@ -12,6 +11,7 @@ import java.util.Set; | |||
import junit.framework.TestCase; | |||
import static com.healthmarketscience.jackcess.Database.*; | |||
import static com.healthmarketscience.jackcess.DatabaseTest.*; | |||
/** | |||
* @author Dan Rollo | |||
@@ -28,21 +28,20 @@ public class JetFormatTest extends TestCase { | |||
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; | |||
@@ -58,6 +57,7 @@ public class JetFormatTest extends TestCase { | |||
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"); | |||
@@ -70,15 +70,21 @@ public class JetFormatTest extends TestCase { | |||
} | |||
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]); | |||
} | |||
/** | |||
@@ -113,12 +119,36 @@ public class JetFormatTest extends TestCase { | |||
} | |||
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; | |||
} | |||
@@ -133,12 +163,10 @@ public class JetFormatTest extends TestCase { | |||
} | |||
} | |||
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 { | |||
@@ -149,41 +177,61 @@ public class JetFormatTest extends TestCase { | |||
// 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(); | |||
} | |||
} | |||
} | |||
} | |||
@@ -45,7 +45,7 @@ public class RelationshipTest extends TestCase { | |||
} | |||
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"); |
@@ -29,9 +29,11 @@ package com.healthmarketscience.jackcess; | |||
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; | |||
@@ -173,6 +175,14 @@ public class TableTest extends TestCase { | |||
public Table getTable() { | |||
return _testTable; | |||
} | |||
@Override | |||
protected Charset getCharset() { | |||
return getFormat().CHARSET; | |||
} | |||
@Override | |||
protected TimeZone getTimeZone() { | |||
return TimeZone.getDefault(); | |||
} | |||
}; | |||
} | |||