summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/java/com/healthmarketscience/jackcess/ByteUtil.java17
-rw-r--r--src/java/com/healthmarketscience/jackcess/Database.java104
-rw-r--r--src/java/com/healthmarketscience/jackcess/JetFormat.java51
3 files changed, 162 insertions, 10 deletions
diff --git a/src/java/com/healthmarketscience/jackcess/ByteUtil.java b/src/java/com/healthmarketscience/jackcess/ByteUtil.java
index 7013695..fbb1521 100644
--- a/src/java/com/healthmarketscience/jackcess/ByteUtil.java
+++ b/src/java/com/healthmarketscience/jackcess/ByteUtil.java
@@ -308,6 +308,23 @@ public final class ByteUtil {
}
return true;
}
+
+ /**
+ * Searches for a pattern of bytes in the given buffer starting at the
+ * given offset.
+ * @return the offset of the pattern if a match is found, -1 otherwise
+ */
+ public static int findRange(ByteBuffer buffer, int start, byte[] pattern)
+ {
+ byte firstByte = pattern[0];
+ int limit = buffer.limit() - pattern.length;
+ for(int i = start; i < limit; ++i) {
+ if((firstByte == buffer.get(i)) && matchesRange(buffer, i, pattern)) {
+ return i;
+ }
+ }
+ return -1;
+ }
/**
* Convert a byte buffer to a hexadecimal string for display
diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java
index 75e5911..5044de7 100644
--- a/src/java/com/healthmarketscience/jackcess/Database.java
+++ b/src/java/com/healthmarketscience/jackcess/Database.java
@@ -168,13 +168,15 @@ public class Database
private static final String CAT_COL_DATE_UPDATE = "DateUpdate";
/** System catalog column name of the flags column */
private static final String CAT_COL_FLAGS = "Flags";
+ /** System catalog column name of the properties column */
+ private static final String CAT_COL_PROPS = "LvProp";
public static enum FileFormat {
V1997(null, JetFormat.VERSION_3), // v97 is not supported, so no empty template is provided
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");
+ V2007("com/healthmarketscience/jackcess/empty2007.accdb", JetFormat.VERSION_5, ".accdb");
private final String _emptyFile;
private final JetFormat _format;
@@ -210,6 +212,8 @@ public class Database
private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships";
/** Name of the table that contains queries */
private static final String TABLE_SYSTEM_QUERIES = "MSysQueries";
+ /** Name of the table that contains queries */
+ private static final String OBJECT_NAME_DBPROPS = "MSysDb";
/** System object type for table definitions */
private static final Short TYPE_TABLE = (short) 1;
/** System object type for query definitions */
@@ -296,6 +300,8 @@ public class Database
private boolean _useBigIndex;
/** optional error handler to use when row errors are encountered */
private ErrorHandler _dbErrorHandler;
+ /** the file format of the database */
+ private FileFormat _fileFormat;
/**
* Open an existing Database. If the existing file is not writeable, the
@@ -356,14 +362,14 @@ public class Database
}
return new Database(openChannel(mdbFile,
(!mdbFile.canWrite() || readOnly)),
- autoSync);
+ autoSync, null);
}
/**
- * Create a new Database
+ * Create a new Access 2000 Database
* <p>
* Equivalent to:
- * {@code create(mdbFile, DEFAULT_AUTO_SYNC);}
+ * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);}
*
* @param mdbFile Location to write the new database to. <b>If this file
* already exists, it will be overwritten.</b>
@@ -375,7 +381,29 @@ public class Database
}
/**
- * Create a new Database
+ * Create a new Database for the given fileFormat
+ * <p>
+ * Equivalent to:
+ * {@code create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);}
+ *
+ * @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>
+ *
+ * @see #create(File,boolean)
+ */
+ public static Database create(FileFormat fileFormat, File mdbFile)
+ throws IOException
+ {
+ return create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);
+ }
+
+ /**
+ * Create a new Access 2000 Database
+ * <p>
+ * Equivalent to:
+ * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);}
+ *
* @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
@@ -392,8 +420,9 @@ public class Database
{
return create(FileFormat.V2000, mdbFile, autoSync);
}
+
/**
- * Create a new Database
+ * 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>
@@ -406,7 +435,8 @@ public class Database
* leave the database in an inconsistent state if failures
* are encountered during writing.
*/
- public static Database create(FileFormat fileFormat, File mdbFile, boolean autoSync)
+ public static Database create(FileFormat fileFormat, File mdbFile,
+ boolean autoSync)
throws IOException
{
FileChannel channel = openChannel(mdbFile, false);
@@ -414,7 +444,7 @@ public class Database
channel.transferFrom(Channels.newChannel(
Thread.currentThread().getContextClassLoader().getResourceAsStream(
fileFormat._emptyFile)), 0, Integer.MAX_VALUE);
- return new Database(channel, autoSync);
+ return new Database(channel, autoSync, fileFormat);
}
/**
@@ -443,9 +473,12 @@ public class Database
* FileChannel instead of a ReadableByteChannel because we need to
* randomly jump around to various points in the file.
*/
- protected Database(FileChannel channel, boolean autoSync) throws IOException
+ protected Database(FileChannel channel, boolean autoSync,
+ FileFormat fileFormat)
+ throws IOException
{
_format = JetFormat.getFormat(channel);
+ _fileFormat = fileFormat;
_pageChannel = new PageChannel(channel, _format, autoSync);
// note, it's slighly sketchy to pass ourselves along partially
// constructed, but only our _format and _pageChannel refs should be
@@ -508,6 +541,59 @@ public class Database
public void setErrorHandler(ErrorHandler newErrorHandler) {
_dbErrorHandler = newErrorHandler;
}
+
+ /**
+ * Returns the FileFormat of this database (which may involve inspecting the
+ * database itself).
+ * @throws IllegalStateException if the file format cannot be determined
+ */
+ public FileFormat getFileFormat()
+ {
+ if(_fileFormat == null) {
+
+ Map<Database.FileFormat,byte[]> possibleFileFormats =
+ getFormat().getPossibleFileFormats();
+
+ if(possibleFileFormats.size() == 1) {
+
+ // single possible format, easy enough
+ _fileFormat = possibleFileFormats.keySet().iterator().next();
+
+ } else {
+
+ // need to check the "AccessVersion" property
+ byte[] dbProps = null;
+ for(Map<String,Object> row :
+ Cursor.createCursor(_systemCatalog).iterable(
+ Arrays.asList(CAT_COL_NAME, CAT_COL_PROPS))) {
+ if(OBJECT_NAME_DBPROPS.equals(row.get(CAT_COL_NAME))) {
+ dbProps = (byte[])row.get(CAT_COL_PROPS);
+ break;
+ }
+ }
+
+ if(dbProps != null) {
+
+ // search for certain "version strings" in the properties (we
+ // can't fully parse the properties objects, but we can still
+ // find the byte pattern)
+ ByteBuffer dbPropBuf = ByteBuffer.wrap(dbProps);
+ for(Map.Entry<Database.FileFormat,byte[]> possible :
+ possibleFileFormats.entrySet()) {
+ if(ByteUtil.findRange(dbPropBuf, 0, possible.getValue()) >= 0) {
+ _fileFormat = possible.getKey();
+ break;
+ }
+ }
+ }
+
+ if(_fileFormat == null) {
+ throw new IllegalStateException("Could not determine FileFormat");
+ }
+ }
+ }
+ return _fileFormat;
+ }
/**
* Read the system catalog
diff --git a/src/java/com/healthmarketscience/jackcess/JetFormat.java b/src/java/com/healthmarketscience/jackcess/JetFormat.java
index 62ce992..f5cc175 100644
--- a/src/java/com/healthmarketscience/jackcess/JetFormat.java
+++ b/src/java/com/healthmarketscience/jackcess/JetFormat.java
@@ -31,6 +31,9 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.Map;
/**
* Encapsulates constants describing a specific version of the Access Jet format
@@ -46,7 +49,8 @@ public abstract class JetFormat {
/** Maximum size of a text field */
public static final short TEXT_FIELD_MAX_LENGTH = 255 * TEXT_FIELD_UNIT_SIZE;
- /** Offset in the file that holds the byte describing the Jet format version */
+ /** Offset in the file that holds the byte describing the Jet format
+ version */
private static final long OFFSET_VERSION = 20L;
/** Version code for Jet version 3 */
private static final byte CODE_VERSION_3 = 0x0;
@@ -55,6 +59,32 @@ public abstract class JetFormat {
/** Version code for Jet version 5 */
private static final byte CODE_VERSION_5 = 0x2;
+ /** value of the "AccessVersion" property for access 2000 dbs:
+ {@code "08.50"} */
+ private static final byte[] ACCESS_VERSION_2000 = new byte[] {
+ '0', 0, '8', 0, '.', 0, '5', 0, '0', 0};
+ /** value of the "AccessVersion" property for access 2002/2003 dbs
+ {@code "09.50"} */
+ private static final byte[] ACCESS_VERSION_2003 = new byte[] {
+ '0', 0, '9', 0, '.', 0, '5', 0, '0', 0};
+
+ // 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);
+
+ private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_5 =
+ Collections.singletonMap(Database.FileFormat.V2007, (byte[])null);
+
+ static {
+ POSSIBLE_VERSION_4.put(Database.FileFormat.V2000, ACCESS_VERSION_2000);
+ POSSIBLE_VERSION_4.put(Database.FileFormat.V2003, ACCESS_VERSION_2003);
+ }
+ }
+
//These constants are populated by this class's constructor. They can't be
//populated by the subclass's constructor because they are final, and Java
//doesn't allow this; hence all the abstract defineXXX() methods.
@@ -310,6 +340,8 @@ public abstract class JetFormat {
protected abstract boolean defineReverseFirstByteInDescNumericIndexes();
+ protected abstract Map<Database.FileFormat,byte[]> getPossibleFileFormats();
+
@Override
public String toString() {
return _name;
@@ -458,12 +490,25 @@ public abstract class JetFormat {
@Override
protected Charset defineCharset() { return Charset.forName("UTF-16LE"); }
+
+ @Override
+ protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+ {
+ return PossibleFileFormats.POSSIBLE_VERSION_4;
+ }
+
}
private static final class Jet3Format extends Jet4Format {
private Jet3Format() {
super("VERSION_3");
}
+
+ @Override
+ protected Map<Database.FileFormat,byte[]> getPossibleFileFormats() {
+ return PossibleFileFormats.POSSIBLE_VERSION_3;
+ }
+
}
private static final class Jet5Format extends Jet4Format {
@@ -474,6 +519,10 @@ public abstract class JetFormat {
@Override
protected boolean defineReverseFirstByteInDescNumericIndexes() { return true; }
+ @Override
+ protected Map<Database.FileFormat,byte[]> getPossibleFileFormats() {
+ return PossibleFileFormats.POSSIBLE_VERSION_5;
+ }
}
}