<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
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) {
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() {
}
protected Charset getCharset() {
- return getTable().getDatabase().getCharset();
+ return getDatabase().getCharset();
}
protected TimeZone getTimeZone() {
- return getTable().getDatabase().getTimeZone();
+ return getDatabase().getTimeZone();
}
private void setAutoNumberGenerator()
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.
*
/**
* 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;
}
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 */
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));
/**
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
* Gets currently configured {@link Table.ColumnOrder} (always non-{@code
* null}).
*/
- public Table.ColumnOrder getColumnOrder()
- {
+ public Table.ColumnOrder getColumnOrder() {
return _columnOrder;
}
_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");
}
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
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.
*/
*/
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;
*/
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;
/**
* @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);
}
/**
*/
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);
+ }
}
/**
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;
}
}
@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
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);
}
}
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;
/**
/** 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);
}
}
protected abstract boolean defineReverseFirstByteInDescNumericIndexes();
- protected abstract Map<Database.FileFormat,byte[]> getPossibleFileFormats();
+ protected abstract Map<String,Database.FileFormat> getPossibleFileFormats();
@Override
public String toString() {
protected Charset defineCharset() { return Charset.defaultCharset(); }
@Override
- protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+ protected Map<String,Database.FileFormat> getPossibleFileFormats()
{
return PossibleFileFormats.POSSIBLE_VERSION_3;
}
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;
}
}
@Override
- protected Map<Database.FileFormat,byte[]> getPossibleFileFormats()
+ protected Map<String,Database.FileFormat> getPossibleFileFormats()
{
return PossibleFileFormats.POSSIBLE_VERSION_MSISAM;
}
protected boolean defineReverseFirstByteInDescNumericIndexes() { return true; }
@Override
- protected Map<Database.FileFormat,byte[]> getPossibleFileFormats() {
+ protected Map<String,Database.FileFormat> getPossibleFileFormats() {
return PossibleFileFormats.POSSIBLE_VERSION_12;
}
}
}
@Override
- protected Map<Database.FileFormat,byte[]> getPossibleFileFormats() {
+ protected Map<String,Database.FileFormat> getPossibleFileFormats() {
return PossibleFileFormats.POSSIBLE_VERSION_14;
}
}
--- /dev/null
+/*
+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;
+ }
+ }
+
+}
--- /dev/null
+/*
+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;
+ }
+ }
+ }
+}
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;
_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)
*/
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.
--- /dev/null
+/*
+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();
+ }
+ }
+ }
+
+}
return _testTable;
}
@Override
+ public JetFormat getFormat() {
+ return getTable().getFormat();
+ }
+ @Override
+ public PageChannel getPageChannel() {
+ return getTable().getPageChannel();
+ }
+ @Override
protected Charset getCharset() {
return getFormat().CHARSET;
}