From b288c8bae282f0023f3b257d36b0c6f82782fed4 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 19 Mar 2010 12:33:59 +0000 Subject: [PATCH] add ability to determine fileformat for existing db files git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/newformats@453 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/ByteUtil.java | 17 +++ .../jackcess/Database.java | 104 ++++++++++++++++-- .../jackcess/JetFormat.java | 51 ++++++++- .../jackcess/DatabaseTest.java | 2 + 4 files changed, 164 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 *

* 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. If this file * already exists, it will be overwritten. @@ -375,7 +381,29 @@ public class Database } /** - * Create a new Database + * Create a new Database for the given fileFormat + *

+ * Equivalent to: + * {@code create(fileFormat, mdbFile, DEFAULT_AUTO_SYNC);} + * + * @param fileFormat version of new database. + * @param mdbFile Location to write the new database to. If this file + * already exists, it will be overwritten. + * + * @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 + *

+ * Equivalent to: + * {@code create(FileFormat.V2000, mdbFile, DEFAULT_AUTO_SYNC);} + * * @param mdbFile Location to write the new database to. If this file * already exists, it will be overwritten. * @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. If this file * already exists, it will be overwritten. @@ -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 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 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 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 POSSIBLE_VERSION_3 = + Collections.singletonMap(Database.FileFormat.V1997, (byte[])null); + + private static final Map POSSIBLE_VERSION_4 = + new EnumMap(Database.FileFormat.class); + + private static final Map 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 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 getPossibleFileFormats() + { + return PossibleFileFormats.POSSIBLE_VERSION_4; + } + } private static final class Jet3Format extends Jet4Format { private Jet3Format() { super("VERSION_3"); } + + @Override + protected Map 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 getPossibleFileFormats() { + return PossibleFileFormats.POSSIBLE_VERSION_5; + } } } diff --git a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java index cd87323..828f76d 100644 --- a/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -73,6 +73,8 @@ public class DatabaseTest extends TestCase { public static Database open(final TestDB testDB) throws Exception { final Database db = Database.open(testDB.getFile(), true, _autoSync); assertEquals("Wrong JetFormat.", testDB.getExpectedFormat(), db.getFormat()); + assertEquals("Wrong FileFormat.", testDB.getExpectedFileFormat(), + db.getFileFormat()); return db; } -- 2.39.5