diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2011-03-20 21:19:32 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2011-03-20 21:19:32 +0000 |
commit | f1cd07bf0464c1789bac5293c66667beb76b7d2d (patch) | |
tree | 218d013f2b384165bb20ffb8ee0628bca37df6bd /src | |
parent | 28f8e998fb4d56a62d633c6eda8fbb32a907a916 (diff) | |
download | jackcess-f1cd07bf0464c1789bac5293c66667beb76b7d2d.tar.gz jackcess-f1cd07bf0464c1789bac5293c66667beb76b7d2d.zip |
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
Diffstat (limited to 'src')
-rw-r--r-- | src/changes/changes.xml | 11 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Column.java | 28 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Database.java | 246 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/JetFormat.java | 49 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/PropertyMap.java | 169 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/PropertyMaps.java | 337 | ||||
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Table.java | 27 |
7 files changed, 781 insertions, 86 deletions
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 @@ <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 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<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() @@ -429,6 +435,16 @@ public class Column implements Comparable<Column> { } /** + * @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. * * @throws IllegalArgumentException if this column definition is invalid. @@ -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; } 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<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; } @@ -823,49 +844,40 @@ public class Database } /** + * @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 @@ -1181,6 +1203,77 @@ public class Database } /** + * @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. */ public String getDatabasePassword() throws IOException @@ -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); } } 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<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 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<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 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<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; + } + } + } +} 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) |