From f1cd07bf0464c1789bac5293c66667beb76b7d2d Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sun, 20 Mar 2011 21:19:32 +0000 Subject: [PATCH] add support for reading properties blobs; add accessors to various database objects for retrieving their respective properties git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@533 f203690c-595d-4dc9-a70b-905162fa7fd2 --- src/changes/changes.xml | 11 + .../healthmarketscience/jackcess/Column.java | 28 +- .../jackcess/Database.java | 246 ++++++++++--- .../jackcess/JetFormat.java | 49 +-- .../jackcess/PropertyMap.java | 169 +++++++++ .../jackcess/PropertyMaps.java | 337 ++++++++++++++++++ .../healthmarketscience/jackcess/Table.java | 27 +- .../jackcess/JetFormatTest.java | 2 +- .../jackcess/PropertiesTest.java | 205 +++++++++++ .../jackcess/TableTest.java | 8 + 10 files changed, 995 insertions(+), 87 deletions(-) create mode 100644 src/java/com/healthmarketscience/jackcess/PropertyMap.java create mode 100644 src/java/com/healthmarketscience/jackcess/PropertyMaps.java create mode 100644 test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java diff --git a/src/changes/changes.xml b/src/changes/changes.xml index df70933..59529aa 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -4,6 +4,17 @@ Tim McCune + + + Refactor table loading to use indexes. Do not load all table names at + database startup (should make startup faster). + + + 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. + + Add support for writing all fixed length column types into variable diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index e4b7ecd..bb0ffd4 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -172,9 +172,11 @@ public class Column implements Comparable { 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 { 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 { } 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 { 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 { /** * 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; } diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index 1c6e5b6..f4bb69a 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -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 SYSTEM_CATALOG_TABLE_NAME_COLUMNS = new HashSet(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 SYSTEM_CATALOG_PROPS_COLUMNS = + new HashSet(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 possibleFileFormats = + Map 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 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 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 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 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 tableNames) throws IOException; + + public Map getObjectRow(Integer parentId, String name, + Collection columns) + throws IOException + { + Cursor cur = findRow(parentId, name); + return ((cur != null) ? cur.getCurrentRow(columns) : null); + } + + public Map getObjectRow( + Integer objectId, Collection 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 rowPat = new HashMap(); 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); } } diff --git a/src/java/com/healthmarketscience/jackcess/JetFormat.java b/src/java/com/healthmarketscience/jackcess/JetFormat.java index 08db1e0..dbe3e47 100644 --- a/src/java/com/healthmarketscience/jackcess/JetFormat.java +++ b/src/java/com/healthmarketscience/jackcess/JetFormat.java @@ -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 POSSIBLE_VERSION_3 = - Collections.singletonMap(Database.FileFormat.V1997, (byte[])null); + private static final Map POSSIBLE_VERSION_3 = + Collections.singletonMap((String)null, Database.FileFormat.V1997); - private static final Map POSSIBLE_VERSION_4 = - new EnumMap(Database.FileFormat.class); + private static final Map POSSIBLE_VERSION_4 = + new HashMap(); - private static final Map POSSIBLE_VERSION_12 = - Collections.singletonMap(Database.FileFormat.V2007, (byte[])null); + private static final Map POSSIBLE_VERSION_12 = + Collections.singletonMap((String)null, Database.FileFormat.V2007); - private static final Map POSSIBLE_VERSION_14 = - Collections.singletonMap(Database.FileFormat.V2010, (byte[])null); + private static final Map POSSIBLE_VERSION_14 = + Collections.singletonMap((String)null, Database.FileFormat.V2010); - private static final Map POSSIBLE_VERSION_MSISAM = - Collections.singletonMap(Database.FileFormat.MSISAM, (byte[])null); + private static final Map 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 getPossibleFileFormats(); + protected abstract Map getPossibleFileFormats(); @Override public String toString() { @@ -660,7 +663,7 @@ public abstract class JetFormat { protected Charset defineCharset() { return Charset.defaultCharset(); } @Override - protected Map getPossibleFileFormats() + protected Map 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 getPossibleFileFormats() + protected Map getPossibleFileFormats() { return PossibleFileFormats.POSSIBLE_VERSION_4; } @@ -873,7 +876,7 @@ public abstract class JetFormat { } @Override - protected Map getPossibleFileFormats() + protected Map getPossibleFileFormats() { return PossibleFileFormats.POSSIBLE_VERSION_MSISAM; } @@ -893,7 +896,7 @@ public abstract class JetFormat { protected boolean defineReverseFirstByteInDescNumericIndexes() { return true; } @Override - protected Map getPossibleFileFormats() { + protected Map getPossibleFileFormats() { return PossibleFileFormats.POSSIBLE_VERSION_12; } } @@ -910,7 +913,7 @@ public abstract class JetFormat { } @Override - protected Map getPossibleFileFormats() { + protected Map 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 index 0000000..9ad26f5 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/PropertyMap.java @@ -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 +{ + 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 _props = + new LinkedHashMap(); + + 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 iterator() { + return _props.values().iterator(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(PropertyMaps.DEFAULT_NAME.equals(getName()) ? + "" : getName()) + .append(" {"); + for(Iterator 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 index 0000000..272e8d9 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/PropertyMaps.java @@ -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 +{ + /** 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 _maps = + new LinkedHashMap(); + 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 iterator() { + return _maps.values().iterator(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for(Iterator 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 _columns = + new HashMap(); + + 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 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 readPropertyNames(ByteBuffer bbBlock) { + List names = new ArrayList(); + 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 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; + } + } + } +} diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 1b4cdb8..2d107af 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -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) diff --git a/test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java b/test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java index 3f5cd27..e60bf96 100644 --- a/test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java @@ -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 index 0000000..34d10d6 --- /dev/null +++ b/test/src/java/com/healthmarketscience/jackcess/PropertiesTest.java @@ -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 props = new ArrayList(); + 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 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(); + } + } + } + +} diff --git a/test/src/java/com/healthmarketscience/jackcess/TableTest.java b/test/src/java/com/healthmarketscience/jackcess/TableTest.java index 005d470..ae2dd89 100644 --- a/test/src/java/com/healthmarketscience/jackcess/TableTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/TableTest.java @@ -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; } -- 2.39.5