]> source.dussan.org Git - jackcess.git/commitdiff
add ability to determine fileformat for existing db files
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 19 Mar 2010 12:33:59 +0000 (12:33 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 19 Mar 2010 12:33:59 +0000 (12:33 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/newformats@453 f203690c-595d-4dc9-a70b-905162fa7fd2

src/java/com/healthmarketscience/jackcess/ByteUtil.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java

index 701369561294471af443328adb9e7ecf2a4276bd..fbb152138adffd3f6419c93cb527aef8a4c740f9 100644 (file)
@@ -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
index 75e5911c547b2ab4563e55729f8231174eca15e8..5044de72bce625f6ccf0c5423ec146173f852a38 100644 (file)
@@ -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
index 62ce99205c5480cafe8d2179db74aed7c2807632..f5cc175f0bfd946dff0784c8324995e2224c333a 100644 (file)
@@ -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;
+    }
   }
 
 }
index cd8732375ccb147cfdd3ad17c1acc3a50945e263..828f76d7bf5b72d43334aa82e6f1b0937fb80821 100644 (file)
@@ -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;
   }