]> source.dussan.org Git - jackcess.git/commitdiff
add support for reading properties blobs; add accessors to various database objects...
authorJames Ahlborn <jtahlborn@yahoo.com>
Sun, 20 Mar 2011 21:19:32 +0000 (21:19 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Sun, 20 Mar 2011 21:19:32 +0000 (21:19 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@533 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
src/java/com/healthmarketscience/jackcess/PropertyMap.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/PropertyMaps.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/Table.java
test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java
test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java [new file with mode: 0644]
test/src/java/com/healthmarketscience/jackcess/TableTest.java

index df709337c5e39c7a90d5b8d527a1dddbb276a243..59529aacaa6fb7a82d27704a4a2bf4c34096fef5 100644 (file)
@@ -4,6 +4,17 @@
     <author email="javajedi@users.sf.net">Tim McCune</author>
   </properties>
   <body>
+    <release version="1.2.4" date="TBD">
+      <action dev="jahlborn" type="update">
+        Refactor table loading to use indexes.  Do not load all table names at
+        database startup (should make startup faster).
+      </action>
+      <action dev="jahlborn" type="add">
+        Add support for reading properties blobs.  Add methods for accessing
+        database, summary, and user-defined properties from the Database.  Add
+        methods to Table and Column for accessing their respective properties.
+      </action>
+    </release>
     <release version="1.2.3" date="2011-03-05">
       <action dev="jahlborn" type="fix" issue="3181334">
         Add support for writing all fixed length column types into variable
index e4b7ecdb96744145f4511aa8a04737abc5b478e7..bb0ffd493ca5bc6e0bd4d8f69d5cd4a7b1585e49 100644 (file)
@@ -172,9 +172,11 @@ public class Column implements Comparable<Column> {
   private short _textSortOrder = GENERAL_SORT_ORDER;
   /** the auto number generator for this column (if autonumber column) */
   private AutoNumberGenerator _autoNumberGenerator;
+  /** properties for this column, if any */
+  private PropertyMap _props;  
   
   public Column() {
-    this(JetFormat.VERSION_4);
+    this(null);
   }
   
   public Column(JetFormat format) {
@@ -238,13 +240,17 @@ public class Column implements Comparable<Column> {
   public Table getTable() {
     return _table;
   }
+
+  public Database getDatabase() {       
+    return getTable().getDatabase();
+  }
   
   public JetFormat getFormat() {
-    return getTable().getFormat();
+    return getDatabase().getFormat();
   }
 
   public PageChannel getPageChannel() {
-    return getTable().getPageChannel();
+    return getDatabase().getPageChannel();
   }
   
   public String getName() {
@@ -388,11 +394,11 @@ public class Column implements Comparable<Column> {
   }
 
   protected Charset getCharset() {
-    return getTable().getDatabase().getCharset();
+    return getDatabase().getCharset();
   }
 
   protected TimeZone getTimeZone() {
-    return getTable().getDatabase().getTimeZone();
+    return getDatabase().getTimeZone();
   }
 
   private void setAutoNumberGenerator()
@@ -428,6 +434,16 @@ public class Column implements Comparable<Column> {
     return _autoNumberGenerator;
   }
 
+  /**
+   * @return the properties for this column
+   */
+  public PropertyMap getProperties() throws IOException {
+    if(_props == null) {
+      _props = getTable().getPropertyMaps().get(getName());
+    }
+    return _props;
+  }
+
   /**
    * Checks that this column definition is valid.
    *
@@ -1603,7 +1619,7 @@ public class Column implements Comparable<Column> {
   /**
    * Treat booleans as integers (C-style).
    */
-  private static Object booleanToInteger(Object obj) {
+  protected static Object booleanToInteger(Object obj) {
     if (obj instanceof Boolean) {
       obj = ((Boolean) obj) ? 1 : 0;
     }
index 1c6e5b67eb8390c5a5e9d6199909daf495f7ff10..f4bb69a82b3cdd741eb2921f892edaaac8a68ac7 100644 (file)
@@ -275,14 +275,22 @@ public class Database
   private static final String ESCAPE_PREFIX = "x";
   /** Name of the system object that is the parent of all tables */
   private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables";
+  /** Name of the system object that is the parent of all databases */
+  private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases";
+  /** Name of the system object that is the parent of all relationships */
+  private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = "Relationships";
   /** Name of the table that contains system access control entries */
   private static final String TABLE_SYSTEM_ACES = "MSysACEs";
   /** Name of the table that contains table relationships */
   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";
+  /** Name of the main database properties object */
+  private static final String OBJECT_NAME_DB_PROPS = "MSysDb";
+  /** Name of the summary properties object */
+  private static final String OBJECT_NAME_SUMMARY_PROPS = "SummaryInfo";
+  /** Name of the user-defined properties object */
+  private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined";
   /** System object type for table definitions */
   private static final Short TYPE_TABLE = (short) 1;
   /** System object type for query definitions */
@@ -299,6 +307,9 @@ public class Database
   private static Collection<String> SYSTEM_CATALOG_TABLE_NAME_COLUMNS =
     new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, 
                                       CAT_COL_FLAGS, CAT_COL_PARENT_ID));
+  /** the columns to read when getting object propertyes */
+  private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS =
+    new HashSet<String>(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS));
   
   
   /**
@@ -391,7 +402,18 @@ public class Database
   private Table.ColumnOrder _columnOrder;
   /** cache of in-use tables */
   private final TableCache _tableCache = new TableCache();
-  
+  /** handler for reading/writing properteies */
+  private PropertyMaps.Handler _propsHandler;
+  /** ID of the Databases system object */
+  private Integer _dbParentId;
+  /** core database properties */
+  private PropertyMaps _dbPropMaps;
+  /** summary properties */
+  private PropertyMaps _summaryPropMaps;
+  /** user-defined properties */
+  private PropertyMaps _userDefPropMaps;
+
+
   /**
    * Open an existing Database.  If the existing file is not writeable, the
    * file will be opened read-only.  Auto-syncing is enabled for the returned
@@ -806,8 +828,7 @@ public class Database
    * Gets currently configured {@link Table.ColumnOrder} (always non-{@code
    * null}).
    */
-  public Table.ColumnOrder getColumnOrder()
-  {
+  public Table.ColumnOrder getColumnOrder() {
     return _columnOrder;
   }
 
@@ -822,50 +843,41 @@ public class Database
     _columnOrder = newColumnOrder;
   }
 
+  /**
+   * @returns the current handler for reading/writing properties, creating if
+   * necessary
+   */
+  private PropertyMaps.Handler getPropsHandler() {
+    if(_propsHandler == null) {
+      _propsHandler = new PropertyMaps.Handler(this);
+    }
+    return _propsHandler;
+  }
+
   /**
    * 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()
-  {
+  public FileFormat getFileFormat() throws IOException {
+
     if(_fileFormat == null) {
 
-      Map<Database.FileFormat,byte[]> possibleFileFormats =
+      Map<String,Database.FileFormat> possibleFileFormats =
         getFormat().getPossibleFileFormats();
 
       if(possibleFileFormats.size() == 1) {
 
-        // single possible format, easy enough
-        _fileFormat = possibleFileFormats.keySet().iterator().next();
+        // single possible format (null key), easy enough
+        _fileFormat = possibleFileFormats.get(null);
 
       } 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;
-          }
-        }
+        String accessVersion = (String)getDatabaseProperties().getValue(
+            PropertyMap.ACCESS_VERSION_PROP);
         
-        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;
-            }
-          }
-        }
+        _fileFormat = possibleFileFormats.get(accessVersion);
         
         if(_fileFormat == null) {
           throw new IllegalStateException("Could not determine FileFormat");
@@ -874,6 +886,16 @@ public class Database
     }
     return _fileFormat;
   }
+
+  /**
+   * @return a PropertyMaps instance decoded from the given bytes (always
+   *         returns non-{@code null} result).
+   */
+  public PropertyMaps readProperties(byte[] propsBytes, int objectId)
+    throws IOException 
+  {
+    return getPropsHandler().read(propsBytes, objectId);
+  }
   
   /**
    * Read the system catalog
@@ -1180,6 +1202,77 @@ public class Database
     return getTable(tableName, true, defaultUseBigIndex());
   }
 
+  /**
+   * @return the core properties for the database
+   */
+  public PropertyMap getDatabaseProperties() throws IOException {
+    if(_dbPropMaps == null) {
+      _dbPropMaps = getPropertiesForDbObject(OBJECT_NAME_DB_PROPS);
+    }
+    return _dbPropMaps.getDefault();
+  }
+
+  /**
+   * @return the summary properties for the database
+   */
+  public PropertyMap getSummaryProperties() throws IOException {
+    if(_summaryPropMaps == null) {
+      _summaryPropMaps = getPropertiesForDbObject(OBJECT_NAME_SUMMARY_PROPS);
+    }
+    return _summaryPropMaps.getDefault();
+  }
+
+  /**
+   * @return the user-defined properties for the database
+   */
+  public PropertyMap getUserDefinedProperties() throws IOException {
+    if(_userDefPropMaps == null) {
+      _userDefPropMaps = getPropertiesForDbObject(OBJECT_NAME_USERDEF_PROPS);
+    }
+    return _userDefPropMaps.getDefault();
+  }
+
+  /**
+   * @return the PropertyMaps for the object with the given id
+   */
+  public PropertyMaps getPropertiesForObject(int objectId)
+    throws IOException
+  {
+    Map<String,Object> objectRow = _tableFinder.getObjectRow(
+        objectId, SYSTEM_CATALOG_PROPS_COLUMNS);
+    byte[] propsBytes = null;
+    if(objectRow != null) {
+      propsBytes = (byte[])objectRow.get(CAT_COL_PROPS);
+    }
+    return readProperties(propsBytes, objectId);
+  }
+
+  /**
+   * @return property group for the given "database" object
+   */
+  private PropertyMaps getPropertiesForDbObject(String dbName)
+    throws IOException
+  {
+    if(_dbParentId == null) {
+      // need the parent if of the databases objects
+      _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, 
+                                              SYSTEM_OBJECT_NAME_DATABASES);
+      if(_dbParentId == null) {  
+        throw new IOException("Did not find required parent db id");
+      }
+    }
+
+    Map<String,Object> objectRow = _tableFinder.getObjectRow(
+        _dbParentId, dbName, SYSTEM_CATALOG_PROPS_COLUMNS);
+    byte[] propsBytes = null;
+    int objectId = -1;
+    if(objectRow != null) {
+      propsBytes = (byte[])objectRow.get(CAT_COL_PROPS);
+      objectId = (Integer)objectRow.get(CAT_COL_ID);
+    }
+    return readProperties(propsBytes, objectId);
+  }
+
   /**
    * @return the current database password, or {@code null} if none set.
    */
@@ -1572,7 +1665,7 @@ public class Database
    */
   private void addTable(String tableName, Integer pageNumber)
   {
-    _tableLookup.put(toLookupTableName(tableName),
+    _tableLookup.put(toLookupName(tableName),
                      new TableInfo(pageNumber, tableName, 0));
     // clear this, will be created next time needed
     _tableNames = null;
@@ -1583,7 +1676,7 @@ public class Database
    */
   private TableInfo lookupTable(String tableName) throws IOException {
 
-    String lookupTableName = toLookupTableName(tableName);
+    String lookupTableName = toLookupName(tableName);
     TableInfo tableInfo = _tableLookup.get(lookupTableName);
     if(tableInfo != null) {
       return tableInfo;
@@ -1602,8 +1695,8 @@ public class Database
   /**
    * @return a string usable in the _tableLookup map.
    */
-  private String toLookupTableName(String tableName) {
-    return ((tableName != null) ? tableName.toUpperCase() : null);
+  static String toLookupName(String name) {
+    return ((name != null) ? name.toUpperCase() : null);
   }
 
   /**
@@ -1793,14 +1886,44 @@ public class Database
    */
   private abstract class TableFinder
   {
-    public abstract Integer findObjectId(Integer parentId, String name)
+    protected abstract Cursor findRow(Integer parentId, String name)
+      throws IOException;
+
+    protected abstract Cursor findRow(Integer objectId) 
       throws IOException;
 
+    public Integer findObjectId(Integer parentId, String name) 
+      throws IOException 
+    {
+      Cursor cur = findRow(parentId, name);
+      if(cur == null) {  
+        return null;
+      }
+      Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
+      return (Integer)cur.getCurrentRowValue(idCol);
+    }
+
     public abstract TableInfo lookupTable(String tableName)
       throws IOException;
     
     public abstract void getTableNames(Set<String> tableNames) 
       throws IOException;
+
+    public Map<String,Object> getObjectRow(Integer parentId, String name,
+                                           Collection<String> columns) 
+      throws IOException 
+    {
+      Cursor cur = findRow(parentId, name);
+      return ((cur != null) ? cur.getCurrentRow(columns) : null);
+    }
+
+    public Map<String,Object> getObjectRow(
+        Integer objectId, Collection<String> columns)
+      throws IOException
+    {
+      Cursor cur = findRow(objectId);
+      return ((cur != null) ? cur.getCurrentRow(columns) : null);
+    }
   }
 
   /**
@@ -1809,26 +1932,37 @@ public class Database
   private final class DefaultTableFinder extends TableFinder
   {
     private final IndexCursor _systemCatalogCursor;
+    private IndexCursor _systemCatalogIdCursor;
 
     private DefaultTableFinder(IndexCursor systemCatalogCursor) {
       _systemCatalogCursor = systemCatalogCursor;
     }
 
     @Override
-    public Integer findObjectId(Integer parentId, String name) 
+    protected Cursor findRow(Integer parentId, String name) 
       throws IOException 
     {
-      if(!_systemCatalogCursor.findRowByEntry(parentId, name)) {  
-        return null;
+      return (_systemCatalogCursor.findRowByEntry(parentId, name) ?
+              _systemCatalogCursor : null);
+    }
+
+    @Override
+    protected Cursor findRow(Integer objectId) throws IOException 
+    {
+      if(_systemCatalogIdCursor == null) {
+        _systemCatalogIdCursor = new CursorBuilder(_systemCatalog)
+          .setIndexByColumnNames(CAT_COL_ID)
+          .toIndexCursor();
       }
-      Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
-      return (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
+
+      return (_systemCatalogIdCursor.findRowByEntry(objectId) ?
+              _systemCatalogIdCursor : null);
     }
 
     @Override
     public TableInfo lookupTable(String tableName) throws IOException {
 
-      if(!_systemCatalogCursor.findRowByEntry(_tableParentId, tableName)) {
+      if(findRow(_tableParentId, tableName) == null) {
         return null;
       }
 
@@ -1881,18 +2015,22 @@ public class Database
     }
 
     @Override
-    public Integer findObjectId(Integer parentId, String name) 
+    protected Cursor findRow(Integer parentId, String name) 
       throws IOException 
     {
       Map<String,Object> rowPat = new HashMap<String,Object>();
       rowPat.put(CAT_COL_PARENT_ID, parentId);  
       rowPat.put(CAT_COL_NAME, name);
-      if(!_systemCatalogCursor.findRow(rowPat)) {  
-        return null;
-      }
-      
+      return (_systemCatalogCursor.findRow(rowPat) ?
+              _systemCatalogCursor : null);
+    }
+
+    @Override
+    protected Cursor findRow(Integer objectId) throws IOException 
+    {
       Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
-      return (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
+      return (_systemCatalogCursor.findRow(idCol, objectId) ?
+              _systemCatalogCursor : null);
     }
 
     @Override
@@ -1935,12 +2073,8 @@ public class Database
         Short type = (Short)row.get(CAT_COL_TYPE);
         int parentId = (Integer)row.get(CAT_COL_PARENT_ID);
 
-        if(parentId != _tableParentId) {
-          // no more tables
-          continue;
-        }
-
-        if(TYPE_TABLE.equals(type) && !isSystemObject(flags)) {
+        if((parentId == _tableParentId) && TYPE_TABLE.equals(type) && 
+           !isSystemObject(flags)) {
           tableNames.add(tableName);
         }
       }
index 08db1e056070072c949f86ce11766f989a3fb0dd..dbe3e47b9d669047caf9654b37eed5f911429190 100644 (file)
@@ -32,7 +32,7 @@ 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.HashMap;
 import java.util.Map;
 
 /**
@@ -103,33 +103,36 @@ public abstract class JetFormat {
 
   /** 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};
+  private static final String ACCESS_VERSION_2000 = "08.50";
   /** 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};
+  private static final String ACCESS_VERSION_2003 = "09.50";
+
+  /** known intro bytes for property maps */
+  static final byte[][] PROPERTY_MAP_TYPES = {
+    new byte[]{'M', 'R', '2', '\0'}, // access 2000+
+    new byte[]{'K', 'K', 'D', '\0'}};  // access 97
 
   // 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<String,Database.FileFormat> POSSIBLE_VERSION_3 = 
+      Collections.singletonMap((String)null, Database.FileFormat.V1997);
 
-    private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_4 = 
-      new EnumMap<Database.FileFormat,byte[]>(Database.FileFormat.class);
+    private static final Map<String,Database.FileFormat> POSSIBLE_VERSION_4 = 
+      new HashMap<String,Database.FileFormat>();
 
-    private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_12 = 
-      Collections.singletonMap(Database.FileFormat.V2007, (byte[])null);
+    private static final Map<String,Database.FileFormat> POSSIBLE_VERSION_12 = 
+      Collections.singletonMap((String)null, Database.FileFormat.V2007);
 
-    private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_14 = 
-      Collections.singletonMap(Database.FileFormat.V2010, (byte[])null);
+    private static final Map<String,Database.FileFormat> POSSIBLE_VERSION_14 = 
+      Collections.singletonMap((String)null, Database.FileFormat.V2010);
 
-    private static final Map<Database.FileFormat,byte[]> POSSIBLE_VERSION_MSISAM = 
-      Collections.singletonMap(Database.FileFormat.MSISAM, (byte[])null);
+    private static final Map<String,Database.FileFormat> POSSIBLE_VERSION_MSISAM = 
+      Collections.singletonMap((String)null, Database.FileFormat.MSISAM);
 
     static {
-      POSSIBLE_VERSION_4.put(Database.FileFormat.V2000, ACCESS_VERSION_2000);
-      POSSIBLE_VERSION_4.put(Database.FileFormat.V2003, ACCESS_VERSION_2003);
+      POSSIBLE_VERSION_4.put(ACCESS_VERSION_2000, Database.FileFormat.V2000);
+      POSSIBLE_VERSION_4.put(ACCESS_VERSION_2003, Database.FileFormat.V2003);
     }
   }
 
@@ -467,7 +470,7 @@ public abstract class JetFormat {
 
   protected abstract boolean defineReverseFirstByteInDescNumericIndexes();
 
-  protected abstract Map<Database.FileFormat,byte[]> getPossibleFileFormats();
+  protected abstract Map<String,Database.FileFormat> getPossibleFileFormats();
 
   @Override
   public String toString() {
@@ -660,7 +663,7 @@ public abstract class JetFormat {
     protected Charset defineCharset() { return Charset.defaultCharset(); }
 
     @Override
-    protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+    protected Map<String,Database.FileFormat> getPossibleFileFormats()
     {
       return PossibleFileFormats.POSSIBLE_VERSION_3;
     }
@@ -855,7 +858,7 @@ public abstract class JetFormat {
     protected Charset defineCharset() { return Charset.forName("UTF-16LE"); }
 
     @Override
-    protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+    protected Map<String,Database.FileFormat> getPossibleFileFormats()
     {
       return PossibleFileFormats.POSSIBLE_VERSION_4;
     }
@@ -873,7 +876,7 @@ public abstract class JetFormat {
     }
 
     @Override
-    protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+    protected Map<String,Database.FileFormat> getPossibleFileFormats()
     {
       return PossibleFileFormats.POSSIBLE_VERSION_MSISAM;
     }
@@ -893,7 +896,7 @@ public abstract class JetFormat {
     protected boolean defineReverseFirstByteInDescNumericIndexes() { return true; }
 
     @Override
-    protected Map<Database.FileFormat,byte[]> getPossibleFileFormats() {
+    protected Map<String,Database.FileFormat> getPossibleFileFormats() {
       return PossibleFileFormats.POSSIBLE_VERSION_12;
     }
   }
@@ -910,7 +913,7 @@ public abstract class JetFormat {
     }
 
     @Override
-    protected Map<Database.FileFormat,byte[]> getPossibleFileFormats() {
+    protected Map<String,Database.FileFormat> getPossibleFileFormats() {
       return PossibleFileFormats.POSSIBLE_VERSION_14;
     }
   }
diff --git a/src/java/com/healthmarketscience/jackcess/PropertyMap.java b/src/java/com/healthmarketscience/jackcess/PropertyMap.java
new file mode 100644 (file)
index 0000000..9ad26f5
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Map of properties for a given database object.
+ *
+ * @author James Ahlborn
+ */
+public class PropertyMap implements Iterable<PropertyMap.Property>
+{
+  public static final String ACCESS_VERSION_PROP = "AccessVersion";
+  public static final String TITLE_PROP = "Title";
+  public static final String AUTHOR_PROP = "Author";
+  public static final String COMPANY_PROP = "Company";
+
+  public static final String DEFAULT_VALUE_PROP = "DefaultValue";
+  public static final String REQUIRED_PROP = "Required";
+  public static final String ALLOW_ZERO_LEN_PROP = "AllowZeroLength";
+  public static final String DECIMAL_PLACES_PROP = "DecimalPlaces";
+  public static final String FORMAT_PROP = "Format";
+  public static final String INPUT_MASK_PROP = "InputMask";
+  public static final String CAPTION_PROP = "Caption";
+  public static final String VALIDATION_RULE_PROP = "ValidationRule";
+  public static final String VALIDATION_TEXT_PROP = "ValidationText";
+  public static final String GUID_PROP = "GUID";
+
+  private final String _mapName;
+  private final short _mapType;
+  private final Map<String,Property> _props = 
+    new LinkedHashMap<String,Property>();
+
+  PropertyMap(String name, short type) {
+    _mapName = name;
+    _mapType = type;
+  }
+
+  public String getName() {
+    return _mapName;
+  }
+
+  public short getType() {
+    return _mapType;
+  }
+
+  public int getSize() {
+    return _props.size();
+  }
+
+  public boolean isEmpty() {
+    return _props.isEmpty();
+  }
+
+  /**
+   * @return the property with the given name, if any
+   */
+  public Property get(String name) {
+    return _props.get(Database.toLookupName(name));
+  }
+
+  /**
+   * @return the value of the property with the given name, if any
+   */
+  public Object getValue(String name) {
+    return getValue(name, null);
+  }
+
+  /**
+   * @return the value of the property with the given name, if any, otherwise
+   *         the given defaultValue
+   */
+  public Object getValue(String name, Object defaultValue) {
+    Property prop = get(name);
+    Object value = defaultValue;
+    if((prop != null) && (prop.getValue() != null)) {
+      value = prop.getValue();
+    }
+    return value;
+  }
+
+  /**
+   * Puts a property into this map with the given information.
+   */
+  public void put(String name, DataType type, byte flag, Object value) {
+    _props.put(Database.toLookupName(name), 
+               new Property(name, type, flag, value));
+  }
+
+  public Iterator<Property> iterator() {
+    return _props.values().iterator();
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(PropertyMaps.DEFAULT_NAME.equals(getName()) ?
+              "<DEFAULT>" : getName())
+      .append(" {");
+    for(Iterator<Property> iter = iterator(); iter.hasNext(); ) {
+      sb.append(iter.next());
+      if(iter.hasNext()) {
+        sb.append(",");
+      }
+    }
+    sb.append("}");
+    return sb.toString();
+  }      
+
+  /**
+   * Info about a property defined in a PropertyMap.
+   */ 
+  public static final class Property
+  {
+    private final String _name;
+    private final DataType _type;
+    private final byte _flag;
+    private final Object _value;
+
+    private Property(String name, DataType type, byte flag, Object value) {
+      _name = name;
+      _type = type;
+      _flag = flag;
+      _value = value;
+    }
+
+    public String getName() {
+      return _name;
+    }
+
+    public DataType getType() {
+      return _type;
+    }
+
+    public Object getValue() {
+      return _value;
+    }
+
+    @Override
+    public String toString() {
+      Object val = getValue();
+      if(val instanceof byte[]) {
+        val = ByteUtil.toHexString((byte[])val);
+      }
+      return getName() + "[" + getType() + ":" + _flag + "]=" + val;
+    }
+  }
+
+}
diff --git a/src/java/com/healthmarketscience/jackcess/PropertyMaps.java b/src/java/com/healthmarketscience/jackcess/PropertyMaps.java
new file mode 100644 (file)
index 0000000..272e8d9
--- /dev/null
@@ -0,0 +1,337 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Collection of PropertyMap instances read from a single property data block.
+ *
+ * @author James Ahlborn
+ */
+public class PropertyMaps implements Iterable<PropertyMap>
+{
+  /** the name of the "default" properties for a PropertyMaps instance */
+  public static final String DEFAULT_NAME = "";
+
+  private static final short PROPERTY_NAME_LIST = 0x80;
+  private static final short DEFAULT_PROPERTY_VALUE_LIST = 0x00;
+  private static final short COLUMN_PROPERTY_VALUE_LIST = 0x01;
+
+  /** maps the PropertyMap name (case-insensitive) to the PropertyMap
+      instance */
+  private final Map<String,PropertyMap> _maps = 
+    new LinkedHashMap<String,PropertyMap>();
+  private final int _objectId;
+
+  public PropertyMaps(int objectId) {
+    _objectId = objectId;
+  }
+
+  public int getObjectId() {
+    return _objectId;
+  }
+
+  public int getSize() {
+    return _maps.size();
+  }
+
+  public boolean isEmpty() {
+    return _maps.isEmpty();
+  }
+
+  /**
+   * @return the unnamed "default" PropertyMap in this group, creating if
+   *         necessary.
+   */
+  public PropertyMap getDefault() {
+    return get(DEFAULT_NAME, DEFAULT_PROPERTY_VALUE_LIST);
+  }
+
+  /**
+   * @return the PropertyMap with the given name in this group, creating if
+   *         necessary
+   */
+  public PropertyMap get(String name) {
+    return get(name, COLUMN_PROPERTY_VALUE_LIST);
+  }
+
+  /**
+   * @return the PropertyMap with the given name and type in this group,
+   *         creating if necessary
+   */
+  private PropertyMap get(String name, short type) {
+    String lookupName = Database.toLookupName(name);
+    PropertyMap map = _maps.get(lookupName);
+    if(map == null) {
+      map = new PropertyMap(name, type);
+      _maps.put(lookupName, map);
+    }
+    return map;
+  }
+
+  /**
+   * Adds the given PropertyMap to this group.
+   */
+  public void put(PropertyMap map) {
+    _maps.put(Database.toLookupName(map.getName()), map);
+  }
+
+  public Iterator<PropertyMap> iterator() {
+    return _maps.values().iterator();
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    for(Iterator<PropertyMap> iter = iterator(); iter.hasNext(); ) {
+      sb.append(iter.next());
+      if(iter.hasNext()) {
+        sb.append("\n");
+      }
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Utility class for reading/writing property blocks.
+   */
+  static final class Handler
+  {
+    /** the current database */
+    private final Database _database;
+    /** cache of PropColumns used to read/write property values */
+    private final Map<DataType,PropColumn> _columns = 
+      new HashMap<DataType,PropColumn>();
+
+    Handler(Database database) {
+      _database = database;
+    }
+
+    /**
+     * @return a PropertyMaps instance decoded from the given bytes (always
+     *         returns non-{@code null} result).
+     */
+    public PropertyMaps read(byte[] propBytes, int objectId) 
+      throws IOException 
+    {
+
+      PropertyMaps maps = new PropertyMaps(objectId);
+      if((propBytes == null) || (propBytes.length == 0)) {
+        return maps;
+      }
+
+      ByteBuffer bb = ByteBuffer.wrap(propBytes)
+        .order(PageChannel.DEFAULT_BYTE_ORDER);
+
+      // check for known header
+      boolean knownType = false;
+      for(byte[] tmpType : JetFormat.PROPERTY_MAP_TYPES) {
+        if(ByteUtil.matchesRange(bb, bb.position(), tmpType)) {
+          ByteUtil.forward(bb, tmpType.length);
+          knownType = true;
+          break;
+        }
+      }
+
+      if(!knownType) {
+        throw new IOException("Uknown property map type " +
+                              ByteUtil.toHexString(bb, 4));
+      }
+
+      // parse each data "chunk"
+      List<String> propNames = null;
+      while(bb.hasRemaining()) {
+
+        int len = bb.getInt();
+        short type = bb.getShort();
+        int endPos = bb.position() + len - 6;
+
+        ByteBuffer bbBlock = PageChannel.narrowBuffer(bb, bb.position(), 
+                                                      endPos);
+
+        if(type == PROPERTY_NAME_LIST) {
+          propNames = readPropertyNames(bbBlock);
+        } else if((type == DEFAULT_PROPERTY_VALUE_LIST) ||
+                  (type == COLUMN_PROPERTY_VALUE_LIST)) {
+          maps.put(readPropertyValues(bbBlock, propNames, type));
+        } else {
+          throw new IOException("Unknown property block type " + type);
+        }
+
+        bb.position(endPos);
+      }
+
+      return maps;
+    }
+
+    /**
+     * @return the property names parsed from the given data chunk
+     */
+    private List<String> readPropertyNames(ByteBuffer bbBlock) {
+      List<String> names = new ArrayList<String>();
+      while(bbBlock.hasRemaining()) {
+        names.add(readPropName(bbBlock));
+      }
+      return names;
+    }
+
+    /**
+     * @return the PropertyMap created from the values parsed from the given
+     *         data chunk combined with the given property names
+     */
+    private PropertyMap readPropertyValues(
+        ByteBuffer bbBlock, List<String> propNames, short blockType) 
+      throws IOException
+    {
+      String mapName = DEFAULT_NAME;
+
+      if(bbBlock.hasRemaining()) {
+
+        // read the map name, if any
+        int nameBlockLen = bbBlock.getInt();
+        int endPos = bbBlock.position() + nameBlockLen - 4;
+        if(nameBlockLen > 6) {
+          mapName = readPropName(bbBlock);
+        }
+        bbBlock.position(endPos);
+      }
+      
+      PropertyMap map = new PropertyMap(mapName, blockType);
+
+      // read the values
+      while(bbBlock.hasRemaining()) {
+
+        int valLen = bbBlock.getShort();
+        int endPos = bbBlock.position() + valLen - 2;
+        byte flag = bbBlock.get();
+        DataType dataType = DataType.fromByte(bbBlock.get());
+        int nameIdx = bbBlock.getShort();
+        int dataSize = bbBlock.getShort();
+
+        String propName = propNames.get(nameIdx);
+        PropColumn col = getColumn(dataType, propName, dataSize);
+
+        byte[] data = new byte[dataSize];
+        bbBlock.get(data);
+        Object value = col.read(data);
+
+        map.put(propName, dataType, flag, value);
+
+        bbBlock.position(endPos);
+      }
+
+      return map;
+    }
+
+    /**
+     * Reads a property name from the given data block
+     */
+    private String readPropName(ByteBuffer buffer) { 
+      int nameLength = buffer.getShort();
+      byte[] nameBytes = new byte[nameLength];
+      buffer.get(nameBytes);
+      return Column.decodeUncompressedText(nameBytes, _database.getCharset());
+    }
+
+    /**
+     * Gets a PropColumn capable of reading/writing a property of the given
+     * DataType
+     */
+    private PropColumn getColumn(DataType dataType, String propName, 
+                                 int dataSize) {
+
+      if(isPseudoGuidColumn(dataType, propName, dataSize)) {
+        dataType = DataType.GUID;
+      }
+
+      PropColumn col = _columns.get(dataType);
+
+      if(col == null) {
+
+        // translate long value types into simple types
+        DataType colType = dataType;
+        if(dataType == DataType.MEMO) {
+          colType = DataType.TEXT;
+        } else if(dataType == DataType.OLE) {
+          colType = DataType.BINARY;
+        }
+
+        // create column with ability to read/write the given data type
+        col = ((colType == DataType.BOOLEAN) ? 
+               new BooleanPropColumn() : new PropColumn());
+        col.setType(colType);
+        if(col.isVariableLength()) {
+          col.setLength((short)colType.getMaxSize());
+        }
+      }
+
+      return col;
+    }
+
+    private boolean isPseudoGuidColumn(DataType dataType, String propName, 
+                                       int dataSize) {
+      // guids seem to be marked as "binary" fields
+      return((dataType == DataType.BINARY) && 
+             (dataSize == DataType.GUID.getFixedSize()) &&
+             PropertyMap.GUID_PROP.equalsIgnoreCase(propName));
+    }
+
+    /**
+     * Column adapted to work w/out a Table.
+     */
+    private class PropColumn extends Column
+    {
+      @Override
+      public Database getDatabase() {
+        return _database;
+      }
+    }
+
+    /**
+     * Normal boolean columns do not write into the actual row data, so we
+     * need to do a little extra work.
+     */
+    private final class BooleanPropColumn extends PropColumn
+    {
+      @Override
+      public Object read(byte[] data) throws IOException {
+        return ((data[0] != 0) ? Boolean.TRUE : Boolean.FALSE);
+      }
+
+      @Override
+      public ByteBuffer write(Object obj, int remainingRowLength)
+        throws IOException
+      {
+        ByteBuffer buffer = getPageChannel().createBuffer(1);
+        buffer.put(((Number)booleanToInteger(obj)).byteValue());
+        buffer.flip();
+        return buffer;
+      }
+    }
+  }
+}
index 1b4cdb8c156e39718dd8bbe6ec86a732c31e52e3..2d107afd8291dbdde665d47a4889dfba5bdfd8ef 100644 (file)
@@ -160,7 +160,11 @@ public class Table
   private final boolean _useBigIndex;
   /** optional error handler to use when row errors are encountered */
   private ErrorHandler _tableErrorHandler;
-  
+  /** properties for this table */
+  private PropertyMap _props;
+  /** properties group for this table (and columns) */
+  private PropertyMaps _propertyMaps;
+
   /** common cursor for iterating through the table, kept here for historic
       reasons */
   private Cursor _cursor;
@@ -332,6 +336,27 @@ public class Table
     _maxColumnCount = (short)_columns.size();
     _maxVarColumnCount = (short)_varColumns.size();
   }
+
+  /**
+   * @return the properties for this table
+   */
+  public PropertyMap getProperties() throws IOException {
+    if(_props == null) {
+      _props = getPropertyMaps().getDefault();
+    }
+    return _props;
+  }
+
+  /**
+   * @return all PropertyMaps for this table (and columns)
+   */
+  protected PropertyMaps getPropertyMaps() throws IOException {
+    if(_propertyMaps == null) {
+      _propertyMaps = getDatabase().getPropertiesForObject(
+          _tableDefPageNumber);
+    }
+    return _propertyMaps;
+  }
   
   /**
    * @return All of the Indexes on this table (unmodifiable List)
index 3f5cd27c3347ffa75eeb638f713c28644fc1bfdb..e60bf96c5bf83eddbe7227b93826f38c4c7083eb 100644 (file)
@@ -20,7 +20,7 @@ import static com.healthmarketscience.jackcess.DatabaseTest.*;
  */
 public class JetFormatTest extends TestCase {
 
-  private static final File DIR_TEST_DATA = new File("test/data");
+  static final File DIR_TEST_DATA = new File("test/data");
 
   /**
    * Defines known valid db test file base names.
diff --git a/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java b/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java
new file mode 100644 (file)
index 0000000..34d10d6
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import static com.healthmarketscience.jackcess.Database.*;
+import static com.healthmarketscience.jackcess.DatabaseTest.*;
+import static com.healthmarketscience.jackcess.JetFormatTest.*;
+
+/**
+ * @author James Ahlborn
+ */
+public class PropertiesTest extends TestCase
+{
+
+  public PropertiesTest(String name) throws Exception {
+    super(name);
+  }
+
+  public void testPropertyMaps() throws Exception
+  {
+    PropertyMaps maps = new PropertyMaps(10);
+    assertTrue(maps.isEmpty());
+    assertEquals(0, maps.getSize());
+    assertFalse(maps.iterator().hasNext());
+    assertEquals(10, maps.getObjectId());
+
+    PropertyMap defMap = maps.getDefault();
+    assertTrue(defMap.isEmpty());
+    assertEquals(0, defMap.getSize());
+    assertFalse(defMap.iterator().hasNext());
+
+    PropertyMap colMap = maps.get("testcol");
+    assertTrue(colMap.isEmpty());
+    assertEquals(0, colMap.getSize());
+    assertFalse(colMap.iterator().hasNext());
+    
+    assertFalse(maps.isEmpty());
+    assertEquals(2, maps.getSize());
+    
+    assertSame(defMap, maps.get(PropertyMaps.DEFAULT_NAME));
+    assertEquals(PropertyMaps.DEFAULT_NAME, defMap.getName());
+    assertSame(colMap, maps.get("TESTCOL"));
+    assertEquals("testcol", colMap.getName());
+
+    defMap.put("foo", DataType.TEXT, (byte)0, "bar");
+    defMap.put("baz", DataType.LONG, (byte)1, 13);
+
+    assertFalse(defMap.isEmpty());
+    assertEquals(2, defMap.getSize());
+
+    colMap.put("buzz", DataType.BOOLEAN, (byte)0, Boolean.TRUE);
+
+    assertFalse(colMap.isEmpty());
+    assertEquals(1, colMap.getSize());
+
+    assertEquals("bar", defMap.getValue("foo"));
+    assertEquals("bar", defMap.getValue("FOO"));
+    assertNull(colMap.getValue("foo"));
+    assertEquals(13, defMap.get("baz").getValue());
+    assertEquals(Boolean.TRUE, colMap.getValue("Buzz"));
+
+    assertEquals("bar", defMap.getValue("foo", "blah"));
+    assertEquals("blah", defMap.getValue("bogus", "blah"));
+
+    List<PropertyMap.Property> props = new ArrayList<PropertyMap.Property>();
+    for(PropertyMap map : maps) {
+      for(PropertyMap.Property prop : map) {
+        props.add(prop);
+      }
+    }
+
+    assertEquals(Arrays.asList(defMap.get("foo"), defMap.get("baz"), 
+                               colMap.get("buzz")), props);
+  }
+
+  public void testReadProperties() throws Exception
+  {
+    byte[] nameMapBytes = null;
+
+    for(TestDB testDb : SUPPORTED_DBS_TEST_FOR_READ) {
+      Database db = open(testDb);
+
+      Table t = db.getTable("Table1");
+      assertEquals(t.getTableDefPageNumber(), 
+                   t.getPropertyMaps().getObjectId());
+      PropertyMap tProps = t.getProperties();
+      assertEquals(PropertyMaps.DEFAULT_NAME, tProps.getName());
+      int expectedNumProps = 3;
+      if(db.getFileFormat() != Database.FileFormat.V1997) {
+        assertEquals("{5A29A676-1145-4D1A-AE47-9F5415CDF2F1}", 
+                     tProps.getValue(PropertyMap.GUID_PROP));
+        if(nameMapBytes == null) {
+          nameMapBytes = (byte[])tProps.getValue("NameMap");
+        } else {
+          assertTrue(Arrays.equals(nameMapBytes, 
+                                   (byte[])tProps.getValue("NameMap")));
+        }
+        expectedNumProps += 2;
+      }  
+      assertEquals(expectedNumProps, tProps.getSize());
+      assertEquals((byte)0, tProps.getValue("Orientation"));
+      assertEquals(Boolean.FALSE, tProps.getValue("OrderByOn"));
+      assertEquals((byte)2, tProps.getValue("DefaultView"));
+
+      PropertyMap colProps = t.getColumn("A").getProperties();
+      assertEquals("A", colProps.getName());
+      expectedNumProps = 9;
+      if(db.getFileFormat() != Database.FileFormat.V1997) {
+        assertEquals("{E9EDD90C-CE55-4151-ABE1-A1ACE1007515}", 
+                     colProps.getValue(PropertyMap.GUID_PROP));
+        ++expectedNumProps;
+      }
+      assertEquals(expectedNumProps, colProps.getSize());
+      assertEquals((short)-1, colProps.getValue("ColumnWidth"));
+      assertEquals((short)0, colProps.getValue("ColumnOrder"));
+      assertEquals(Boolean.FALSE, colProps.getValue("ColumnHidden"));
+      assertEquals(Boolean.FALSE, 
+                   colProps.getValue(PropertyMap.REQUIRED_PROP));
+      assertEquals(Boolean.FALSE, 
+                   colProps.getValue(PropertyMap.ALLOW_ZERO_LEN_PROP));
+      assertEquals((short)109, colProps.getValue("DisplayControl"));
+      assertEquals(Boolean.TRUE, colProps.getValue("UnicodeCompression"));
+      assertEquals((byte)0, colProps.getValue("IMEMode"));
+      assertEquals((byte)3, colProps.getValue("IMESentenceMode"));
+
+      PropertyMap dbProps = db.getDatabaseProperties();
+      assertTrue(((String)dbProps.getValue(PropertyMap.ACCESS_VERSION_PROP))
+                 .matches("[0-9]{2}[.][0-9]{2}"));
+
+      PropertyMap sumProps = db.getSummaryProperties();
+      assertEquals(3, sumProps.getSize());
+      assertEquals("test", sumProps.getValue(PropertyMap.TITLE_PROP));
+      assertEquals("tmccune", sumProps.getValue(PropertyMap.AUTHOR_PROP));
+      assertEquals("Health Market Science", sumProps.getValue(PropertyMap.COMPANY_PROP));
+
+      PropertyMap userProps = db.getUserDefinedProperties();
+      assertEquals(1, userProps.getSize());
+      assertEquals(Boolean.TRUE, userProps.getValue("ReplicateProject"));
+
+      db.close();
+    }
+  }
+
+  public void testParseProperties() throws Exception
+  {
+    for(FileFormat ff : SUPPORTED_FILEFORMATS_FOR_READ) {
+      File[] dbFiles = new File(DIR_TEST_DATA, ff.name()).listFiles();
+      for(File f : dbFiles) {
+
+        if(!f.isFile()) {
+          continue;
+        }
+
+        Database db = open(ff, f);
+
+        PropertyMap dbProps = db.getDatabaseProperties();
+        assertFalse(dbProps.isEmpty());
+        assertTrue(((String)dbProps.getValue(PropertyMap.ACCESS_VERSION_PROP))
+                   .matches("[0-9]{2}[.][0-9]{2}"));
+
+        for(Map<String,Object> row : db.getSystemCatalog()) {
+          int id = (Integer)row.get("Id");
+          byte[] propBytes = (byte[])row.get("LvProp");
+          PropertyMaps propMaps = db.getPropertiesForObject(id);
+          int byteLen = ((propBytes != null) ? propBytes.length : 0);
+          if(byteLen == 0) {
+            assertTrue(propMaps.isEmpty());
+          } else if(propMaps.isEmpty()) {
+            assertTrue(byteLen < 80);
+          } else {
+            assertTrue(byteLen > 0);
+          }
+        }
+
+        db.close();
+      }
+    }
+  }
+
+}
index 005d4702a32566495ed358b294807066cf3d3e43..ae2dd89c0e465c70565bcf4d90720895f51ddeea 100644 (file)
@@ -176,6 +176,14 @@ public class TableTest extends TestCase {
           return _testTable;
         }
         @Override
+        public JetFormat getFormat() {
+          return getTable().getFormat();
+        }
+        @Override
+        public PageChannel getPageChannel() {
+          return getTable().getPageChannel();
+        }
+        @Override
         protected Charset getCharset() {
           return getFormat().CHARSET;
         }