diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2022-08-26 21:02:28 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2022-08-26 21:02:28 +0000 |
commit | b5aa0575e557d975a062fb7eb34f1ecb5b6a5a29 (patch) | |
tree | 97e9f362b8701f0462ccb2b35fcc37a7550eaa02 /src | |
parent | f1403cdbb47ce8cb1839cd3700ab00c8c8a416bc (diff) | |
download | jackcess-b5aa0575e557d975a062fb7eb34f1ecb5b6a5a29.tar.gz jackcess-b5aa0575e557d975a062fb7eb34f1ecb5b6a5a29.zip |
Add support for linked odbc tables. TableMetaData provides access to connection string and local TableDefinition if available. Fixes #45
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1379 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src')
14 files changed, 627 insertions, 206 deletions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 6698a2d..5656044 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -6,7 +6,11 @@ <body> <release version="4.0.2" date="TBD"> <action dev="jahlborn" type="update"> - Add Table methods to access the creation and last modified dates. + Add Table methods to get the creation and last modified dates. + </action> + <action dev="jahlborn" type="update" issue="45"> + Add support for linked odbc tables. TableMetaData provides access to + connection string and local TableDefinition if available. </action> </release> <release version="4.0.1" date="2021-06-21"> diff --git a/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java b/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java index e063448..9253d06 100644 --- a/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java @@ -491,6 +491,14 @@ public class ColumnBuilder { * attributes. */ public Column addToTable(Table table) throws IOException { + return addToTableDefinition(table); + } + + /** + * Adds a new Column to the given TableDefinition with the currently + * configured attributes. + */ + public Column addToTableDefinition(TableDefinition table) throws IOException { return new TableUpdater((TableImpl)table).addColumn(this); } diff --git a/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java b/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java index d10a6fb..9ef480d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java @@ -38,7 +38,7 @@ import com.healthmarketscience.jackcess.impl.TableUpdater; * @see TableBuilder * @usage _general_class_ */ -public class IndexBuilder +public class IndexBuilder { /** name typically used by MS Access for the primary key index */ public static final String PRIMARY_KEY_NAME = "PrimaryKey"; @@ -138,7 +138,7 @@ public class IndexBuilder public IndexBuilder setUnique() { _flags |= IndexData.UNIQUE_INDEX_FLAG; return this; - } + } /** * Sets this index to encforce required. @@ -146,7 +146,7 @@ public class IndexBuilder public IndexBuilder setRequired() { _flags |= IndexData.REQUIRED_INDEX_FLAG; return this; - } + } /** * Sets this index to ignore null values. @@ -154,7 +154,7 @@ public class IndexBuilder public IndexBuilder setIgnoreNulls() { _flags |= IndexData.IGNORE_NULLS_INDEX_FLAG; return this; - } + } /** * @usage _advanced_method_ @@ -209,6 +209,14 @@ public class IndexBuilder * attributes. */ public Index addToTable(Table table) throws IOException { + return addToTableDefinition(table); + } + + /** + * Adds a new Index to the given TableDefinition with the currently + * configured attributes. + */ + public Index addToTableDefinition(TableDefinition table) throws IOException { return new TableUpdater((TableImpl)table).addIndex(this); } diff --git a/src/main/java/com/healthmarketscience/jackcess/Table.java b/src/main/java/com/healthmarketscience/jackcess/Table.java index 5462e80..539baae 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Table.java +++ b/src/main/java/com/healthmarketscience/jackcess/Table.java @@ -44,7 +44,7 @@ import com.healthmarketscience.jackcess.util.OleBlob; * @author James Ahlborn * @usage _general_class_ */ -public interface Table extends Iterable<Row> +public interface Table extends Iterable<Row>, TableDefinition { /** * enum which controls the ordering of the columns in a table. @@ -63,28 +63,33 @@ public interface Table extends Iterable<Row> * @return The name of the table * @usage _general_method_ */ + @Override public String getName(); /** * Whether or not this table has been marked as hidden. * @usage _general_method_ */ + @Override public boolean isHidden(); /** * Whether or not this table is a system (internal) table. * @usage _general_method_ */ + @Override public boolean isSystem(); /** * @usage _general_method_ */ + @Override public int getColumnCount(); /** * @usage _general_method_ */ + @Override public Database getDatabase(); /** @@ -120,24 +125,28 @@ public interface Table extends Iterable<Row> * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ */ + @Override public List<? extends Column> getColumns(); /** * @return the column with the given name * @usage _general_method_ */ + @Override public Column getColumn(String name); /** * @return the properties for this table * @usage _general_method_ */ + @Override public PropertyMap getProperties() throws IOException; /** * @return the created date for this table if available * @usage _general_method_ */ + @Override public LocalDateTime getCreatedDate() throws IOException; /** @@ -147,12 +156,14 @@ public interface Table extends Iterable<Row> * @return the last updated date for this table if available * @usage _general_method_ */ + @Override public LocalDateTime getUpdatedDate() throws IOException; /** * @return All of the Indexes on this table (unmodifiable List) * @usage _intermediate_method_ */ + @Override public List<? extends Index> getIndexes(); /** @@ -160,6 +171,7 @@ public interface Table extends Iterable<Row> * @throws IllegalArgumentException if there is no index with the given name * @usage _intermediate_method_ */ + @Override public Index getIndex(String name); /** @@ -168,6 +180,7 @@ public interface Table extends Iterable<Row> * table * @usage _intermediate_method_ */ + @Override public Index getPrimaryKeyIndex(); /** @@ -176,6 +189,7 @@ public interface Table extends Iterable<Row> * table and the given table * @usage _intermediate_method_ */ + @Override public Index getForeignKeyIndex(Table otherTable); /** diff --git a/src/main/java/com/healthmarketscience/jackcess/TableDefinition.java b/src/main/java/com/healthmarketscience/jackcess/TableDefinition.java new file mode 100644 index 0000000..76dff60 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/TableDefinition.java @@ -0,0 +1,125 @@ +/* +Copyright (c) 2022 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; + +/** + * The definition of a single database table. A TableDefinition instance is + * retrieved from a {@link TableMetaData} instance. The TableDefinition + * instance only provides access to the table metadata, but no table data. + * <p> + * A TableDefinition instance is not thread-safe (see {@link Database} for + * more thread-safety details). + * + * @author James Ahlborn + * @usage _intermediate_class_ + */ +public interface TableDefinition +{ + /** + * @return The name of the table + * @usage _general_method_ + */ + public String getName(); + + /** + * Whether or not this table has been marked as hidden. + * @usage _general_method_ + */ + public boolean isHidden(); + + /** + * Whether or not this table is a system (internal) table. + * @usage _general_method_ + */ + public boolean isSystem(); + + /** + * @usage _general_method_ + */ + public int getColumnCount(); + + /** + * @usage _general_method_ + */ + public Database getDatabase(); + + /** + * @return All of the columns in this table (unmodifiable List) + * @usage _general_method_ + */ + public List<? extends Column> getColumns(); + + /** + * @return the column with the given name + * @usage _general_method_ + */ + public Column getColumn(String name); + + /** + * @return the properties for this table + * @usage _general_method_ + */ + public PropertyMap getProperties() throws IOException; + + /** + * @return the created date for this table if available + * @usage _general_method_ + */ + public LocalDateTime getCreatedDate() throws IOException; + + /** + * Note: jackcess <i>does not automatically update the modified date of a + * Table</i>. + * + * @return the last updated date for this table if available + * @usage _general_method_ + */ + public LocalDateTime getUpdatedDate() throws IOException; + + /** + * @return All of the Indexes on this table (unmodifiable List) + * @usage _intermediate_method_ + */ + public List<? extends Index> getIndexes(); + + /** + * @return the index with the given name + * @throws IllegalArgumentException if there is no index with the given name + * @usage _intermediate_method_ + */ + public Index getIndex(String name); + + /** + * @return the primary key index for this table + * @throws IllegalArgumentException if there is no primary key index on this + * table + * @usage _intermediate_method_ + */ + public Index getPrimaryKeyIndex(); + + /** + * @return the foreign key index joining this table to the given other table + * @throws IllegalArgumentException if there is no relationship between this + * table and the given table + * @usage _intermediate_method_ + */ + public Index getForeignKeyIndex(Table otherTable); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java b/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java index 250932b..991d026 100644 --- a/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java +++ b/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java @@ -26,8 +26,17 @@ import java.io.IOException; * @author James Ahlborn * @usage _intermediate_class_ */ -public interface TableMetaData +public interface TableMetaData { + public enum Type { + LOCAL, LINKED, LINKED_ODBC; + } + + /** + * The type of table + */ + public Type getType(); + /** * The name of the table (as it is stored in the database) */ @@ -37,7 +46,7 @@ public interface TableMetaData * {@code true} if this is a linked table, {@code false} otherwise. */ public boolean isLinked(); - + /** * {@code true} if this is a system table, {@code false} otherwise. */ @@ -56,7 +65,19 @@ public interface TableMetaData public String getLinkedDbName(); /** + * The connection of this the linked database if this is a linked ODBC + * table, {@code null} otherwise. + */ + public String getConnectionName(); + + /** * Opens this table from the given Database instance. */ public Table open(Database db) throws IOException; + + /** + * Gets the local table definition from the given Database instance if + * available. Only useful for linked ODBC tables. + */ + public TableDefinition getTableDefinition(Database db) throws IOException; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 80799ef..7370626 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -67,6 +67,7 @@ import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.RuntimeIOException; import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.TableBuilder; +import com.healthmarketscience.jackcess.TableDefinition; import com.healthmarketscience.jackcess.TableMetaData; import com.healthmarketscience.jackcess.expr.EvalConfig; import com.healthmarketscience.jackcess.impl.query.QueryImpl; @@ -186,6 +187,8 @@ public class DatabaseImpl implements Database, DateTimeContext private static final String CAT_COL_DATABASE = "Database"; /** System catalog column name of the remote table name */ private static final String CAT_COL_FOREIGN_NAME = "ForeignName"; + /** System catalog column name of the remote connection name */ + private static final String CAT_COL_CONNECT_NAME = "Connect"; /** top-level parentid for a database */ private static final int DB_PARENT_ID = 0xF000000; @@ -236,6 +239,8 @@ public class DatabaseImpl implements Database, DateTimeContext private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined"; /** System object type for table definitions */ static final Short TYPE_TABLE = 1; + /** System object type for linked odbc tables */ + private static final Short TYPE_LINKED_ODBC_TABLE = 4; /** System object type for query definitions */ private static final Short TYPE_QUERY = 5; /** System object type for linked table definitions */ @@ -254,7 +259,8 @@ public class DatabaseImpl implements Database, DateTimeContext private static Collection<String> SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS = new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, CAT_COL_FLAGS, CAT_COL_PARENT_ID, - CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME)); + CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME, + CAT_COL_CONNECT_NAME)); /** 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)); @@ -267,6 +273,9 @@ public class DatabaseImpl implements Database, DateTimeContext private static final Pattern INVALID_IDENTIFIER_CHARS = Pattern.compile("[\\p{Cntrl}.!`\\]\\[]"); + /** regex to match a password in an ODBC string */ + private static final Pattern ODBC_PWD_PATTERN = Pattern.compile("\\bPWD=[^;]+"); + /** the File of the database */ private final Path _file; /** the simple name of the database */ @@ -332,7 +341,7 @@ public class DatabaseImpl implements Database, DateTimeContext private boolean _evaluateExpressions; /** factory for ColumnValidators */ private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; - /** cache of in-use tables */ + /** cache of in-use tables (or table definitions) */ private final TableCache _tableCache = new TableCache(); /** handler for reading/writing properteies */ private PropertyMaps.Handler _propsHandler; @@ -670,9 +679,10 @@ public class DatabaseImpl implements Database, DateTimeContext // common case, local table name == remote table name TableInfo tableInfo = lookupTable(table.getName()); - if((tableInfo != null) && tableInfo.isLinked() && - matchesLinkedTable(table, ((LinkedTableInfo)tableInfo).linkedTableName, - ((LinkedTableInfo)tableInfo).linkedDbName)) { + if((tableInfo != null) && + (tableInfo.getType() == TableMetaData.Type.LINKED) && + matchesLinkedTable(table, tableInfo.getLinkedTableName(), + tableInfo.getLinkedDbName())) { return true; } @@ -983,8 +993,8 @@ public class DatabaseImpl implements Database, DateTimeContext * Read the system catalog */ private void readSystemCatalog() throws IOException { - _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG, - SYSTEM_OBJECT_FLAGS); + _systemCatalog = loadTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG, + SYSTEM_OBJECT_FLAGS, TYPE_TABLE); try { _tableFinder = new DefaultTableFinder( @@ -1096,24 +1106,7 @@ public class DatabaseImpl implements Database, DateTimeContext * @usage _advanced_method_ */ public TableImpl getTable(int tableDefPageNumber) throws IOException { - - // first, check for existing table - TableImpl table = _tableCache.get(tableDefPageNumber); - if(table != null) { - return table; - } - - // lookup table info from system catalog - Row objectRow = _tableFinder.getObjectRow( - tableDefPageNumber, SYSTEM_CATALOG_COLUMNS); - if(objectRow == null) { - return null; - } - - String name = objectRow.getString(CAT_COL_NAME); - int flags = objectRow.getInt(CAT_COL_FLAGS); - - return readTable(name, tableDefPageNumber, flags); + return loadTable(null, tableDefPageNumber, 0, null); } /** @@ -1147,14 +1140,14 @@ public class DatabaseImpl implements Database, DateTimeContext private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables) throws IOException { - if(tableInfo.isLinked()) { + if(tableInfo.getType() == TableMetaData.Type.LINKED) { if(_linkedDbs == null) { _linkedDbs = new HashMap<String,Database>(); } - String linkedDbName = ((LinkedTableInfo)tableInfo).linkedDbName; - String linkedTableName = ((LinkedTableInfo)tableInfo).linkedTableName; + String linkedDbName = tableInfo.getLinkedDbName(); + String linkedTableName = tableInfo.getLinkedTableName(); Database linkedDb = _linkedDbs.get(linkedDbName); if(linkedDb == null) { linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName); @@ -1165,8 +1158,8 @@ public class DatabaseImpl implements Database, DateTimeContext includeSystemTables); } - return readTable(tableInfo.tableName, tableInfo.pageNumber, - tableInfo.flags); + return loadTable(tableInfo.tableName, tableInfo.pageNumber, + tableInfo.flags, tableInfo.tableType); } /** @@ -1384,9 +1377,7 @@ public class DatabaseImpl implements Database, DateTimeContext } } - private String createRelationshipName(RelationshipCreator creator) - throws IOException - { + private String createRelationshipName(RelationshipCreator creator) { // ensure that the final identifier name does not get too long // - the primary name is limited to ((max / 2) - 3) // - the total name is limited to (max - 3) @@ -1833,7 +1824,7 @@ public class DatabaseImpl implements Database, DateTimeContext /** * Reads a table with the given name from the given pageNumber. */ - private TableImpl readTable(String name, int pageNumber, int flags) + private TableImpl loadTable(String name, int pageNumber, int flags, Short type) throws IOException { // first, check for existing table @@ -1842,6 +1833,30 @@ public class DatabaseImpl implements Database, DateTimeContext return table; } + if(name == null) { + // lookup table info from system catalog + Row objectRow = _tableFinder.getObjectRow( + pageNumber, SYSTEM_CATALOG_COLUMNS); + if(objectRow == null) { + return null; + } + + name = objectRow.getString(CAT_COL_NAME); + flags = objectRow.getInt(CAT_COL_FLAGS); + type = objectRow.getShort(CAT_COL_TYPE); + } + + // need to load table from db + return _tableCache.put(readTable(name, pageNumber, flags, type)); + } + + /** + * Reads a table with the given name from the given pageNumber. + */ + private TableImpl readTable( + String name, int pageNumber, int flags, Short type) + throws IOException + { ByteBuffer buffer = takeSharedBuffer(); try { // need to load table from db @@ -1852,8 +1867,9 @@ public class DatabaseImpl implements Database, DateTimeContext "Looking for " + name + " at page " + pageNumber + ", but page type is " + pageType)); } - return _tableCache.put( - new TableImpl(this, buffer, pageNumber, name, flags)); + return (!TYPE_LINKED_ODBC_TABLE.equals(type) ? + new TableImpl(this, buffer, pageNumber, name, flags) : + new TableDefinitionImpl(this, buffer, pageNumber, name, flags)); } finally { releaseSharedBuffer(buffer); } @@ -1991,23 +2007,39 @@ public class DatabaseImpl implements Database, DateTimeContext { _tableLookup.put(toLookupName(tableName), createTableInfo(tableName, pageNumber, 0, type, - linkedDbName, linkedTableName)); + linkedDbName, linkedTableName, null)); // clear this, will be created next time needed _tableNames = null; } + private static TableInfo createTableInfo( + String tableName, Short type, Row row) { + + Integer pageNumber = row.getInt(CAT_COL_ID); + int flags = row.getInt(CAT_COL_FLAGS); + String linkedDbName = row.getString(CAT_COL_DATABASE); + String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME); + String connectName = row.getString(CAT_COL_CONNECT_NAME); + + return createTableInfo(tableName, pageNumber, flags, type, linkedDbName, + linkedTableName, connectName); + } + /** * Creates a TableInfo instance appropriate for the given table data. */ private static TableInfo createTableInfo( String tableName, Integer pageNumber, int flags, Short type, - String linkedDbName, String linkedTableName) + String linkedDbName, String linkedTableName, String connectName) { if(TYPE_LINKED_TABLE.equals(type)) { - return new LinkedTableInfo(pageNumber, tableName, flags, linkedDbName, - linkedTableName); + return new LinkedTableInfo(pageNumber, tableName, flags, type, + linkedDbName, linkedTableName); + } else if(TYPE_LINKED_ODBC_TABLE.equals(type)) { + return new LinkedODBCTableInfo(pageNumber, tableName, flags, type, + connectName, linkedTableName); } - return new TableInfo(pageNumber, tableName, flags); + return new TableInfo(pageNumber, tableName, flags, type); } /** @@ -2224,7 +2256,7 @@ public class DatabaseImpl implements Database, DateTimeContext } private static boolean isTableType(Short objType) { - return(TYPE_TABLE.equals(objType) || TYPE_LINKED_TABLE.equals(objType)); + return(TYPE_TABLE.equals(objType) || isAnyLinkedTableType(objType)); } public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) { @@ -2268,6 +2300,11 @@ public class DatabaseImpl implements Database, DateTimeContext return defaultValue; } + private static boolean isAnyLinkedTableType(Short type) { + return (TYPE_LINKED_TABLE.equals(type) || + TYPE_LINKED_ODBC_TABLE.equals(type)); + } + /** * Utility class for storing table page number and actual name. */ @@ -2276,11 +2313,19 @@ public class DatabaseImpl implements Database, DateTimeContext public final Integer pageNumber; public final String tableName; public final int flags; + public final Short tableType; - private TableInfo(Integer newPageNumber, String newTableName, int newFlags) { + private TableInfo(Integer newPageNumber, String newTableName, int newFlags, + Short newTableType) { pageNumber = newPageNumber; tableName = newTableName; flags = newFlags; + tableType = newTableType; + } + + @Override + public Type getType() { + return Type.LOCAL; } @Override @@ -2309,11 +2354,21 @@ public class DatabaseImpl implements Database, DateTimeContext } @Override + public String getConnectionName() { + return null; + } + + @Override public Table open(Database db) throws IOException { return ((DatabaseImpl)db).getTable(this, true); } @Override + public TableDefinition getTableDefinition(Database db) throws IOException { + return null; + } + + @Override public String toString() { ToStringBuilder sb = CustomToStringStyle.valueBuilder("TableMetaData") .append("name", getName()); @@ -2323,10 +2378,17 @@ public class DatabaseImpl implements Database, DateTimeContext if(isLinked()) { sb.append("isLinked", isLinked()) .append("linkedTableName", getLinkedTableName()) - .append("linkedDbName", getLinkedDbName()); + .append("linkedDbName", getLinkedDbName()) + .append("connectionName", maskPassword(getConnectionName())); } return sb.toString(); } + + private static String maskPassword(String connectionName) { + return ((connectionName != null) ? + ODBC_PWD_PATTERN.matcher(connectionName).replaceAll("PWD=XXXXXX") : + null); + } } /** @@ -2334,15 +2396,21 @@ public class DatabaseImpl implements Database, DateTimeContext */ private static class LinkedTableInfo extends TableInfo { - private final String linkedDbName; - private final String linkedTableName; + private final String _linkedDbName; + private final String _linkedTableName; private LinkedTableInfo(Integer newPageNumber, String newTableName, - int newFlags, String newLinkedDbName, + int newFlags, Short newTableType, + String newLinkedDbName, String newLinkedTableName) { - super(newPageNumber, newTableName, newFlags); - linkedDbName = newLinkedDbName; - linkedTableName = newLinkedTableName; + super(newPageNumber, newTableName, newFlags, newTableType); + _linkedDbName = newLinkedDbName; + _linkedTableName = newLinkedTableName; + } + + @Override + public Type getType() { + return Type.LINKED; } @Override @@ -2352,12 +2420,62 @@ public class DatabaseImpl implements Database, DateTimeContext @Override public String getLinkedTableName() { - return linkedTableName; + return _linkedTableName; } @Override public String getLinkedDbName() { - return linkedDbName; + return _linkedDbName; + } + } + + /** + * Utility class for storing linked ODBC table info + */ + private static class LinkedODBCTableInfo extends TableInfo + { + private final String _linkedTableName; + private final String _connectionName; + + private LinkedODBCTableInfo(Integer newPageNumber, String newTableName, + int newFlags, Short newTableType, + String connectName, + String newLinkedTableName) { + super(newPageNumber, newTableName, newFlags, newTableType); + _linkedTableName = newLinkedTableName; + _connectionName = connectName; + } + + @Override + public Type getType() { + return Type.LINKED_ODBC; + } + + @Override + public boolean isLinked() { + return true; + } + + @Override + public String getLinkedTableName() { + return _linkedTableName; + } + + @Override + public String getConnectionName() { + return _connectionName; + } + + @Override + public Table open(Database db) throws IOException { + return null; + } + + @Override + public TableDefinition getTableDefinition(Database db) throws IOException { + return (((pageNumber != null) && (pageNumber > 0)) ? + ((DatabaseImpl)db).getTable(this, true) : + null); } } @@ -2448,7 +2566,7 @@ public class DatabaseImpl implements Database, DateTimeContext } else if(systemTables) { tableNames.add(tableName); } - } else if(TYPE_LINKED_TABLE.equals(type) && linkedTables) { + } else if(linkedTables && isAnyLinkedTableType(type)) { tableNames.add(tableName); } } @@ -2524,13 +2642,8 @@ public class DatabaseImpl implements Database, DateTimeContext } String realName = row.getString(CAT_COL_NAME); - Integer pageNumber = row.getInt(CAT_COL_ID); - int flags = row.getInt(CAT_COL_FLAGS); - String linkedDbName = row.getString(CAT_COL_DATABASE); - String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME); - return createTableInfo(realName, pageNumber, flags, type, linkedDbName, - linkedTableName); + return createTableInfo(realName, type, row); } return null; @@ -2595,20 +2708,15 @@ public class DatabaseImpl implements Database, DateTimeContext Row row = _systemCatalogCursor.getCurrentRow( SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS); - Integer pageNumber = row.getInt(CAT_COL_ID); - String realName = row.getString(CAT_COL_NAME); - int flags = row.getInt(CAT_COL_FLAGS); Short type = row.getShort(CAT_COL_TYPE); if(!isTableType(type)) { return null; } - String linkedDbName = row.getString(CAT_COL_DATABASE); - String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME); + String realName = row.getString(CAT_COL_NAME); - return createTableInfo(realName, pageNumber, flags, type, linkedDbName, - linkedTableName); + return createTableInfo(realName, type, row); } @Override @@ -2686,13 +2794,7 @@ public class DatabaseImpl implements Database, DateTimeContext continue; } - Integer pageNumber = row.getInt(CAT_COL_ID); - int flags = row.getInt(CAT_COL_FLAGS); - String linkedDbName = row.getString(CAT_COL_DATABASE); - String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME); - - return createTableInfo(realName, pageNumber, flags, type, linkedDbName, - linkedTableName); + return createTableInfo(realName, type, row); } return null; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java new file mode 100644 index 0000000..6c918bd --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java @@ -0,0 +1,60 @@ +/* +Copyright (c) 2022 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A database table definition which does not allow any actual data + * interaction (read or write). + * <p> + * Note, in an ideal world, TableImpl would extend TableDefinitionImpl. + * However, since TableDefinitionImpl came later, it was easier to do it this + * way and avoid a lot of unnecessary code shuffling. + * <p> + * Is not thread-safe. + * + * @author James Ahlborn + * @usage _advanced_class_ + */ +public class TableDefinitionImpl extends TableImpl +{ + protected TableDefinitionImpl(DatabaseImpl database, ByteBuffer tableBuffer, + int pageNumber, String name, int flags) + throws IOException { + super(database, tableBuffer, pageNumber, name, flags); + } + + @Override + protected List<? extends Object[]> addRows(List<? extends Object[]> rows, + final boolean isBatchWrite) + throws IOException { + // all row additions eventually flow through this method + throw new UnsupportedOperationException( + withErrorContext("TableDefinition has no data access")); + } + + @Override + public RowState createRowState() { + // RowState is needed for all traversal operations, so this kills any data + // reading as well as update/delete methods + throw new UnsupportedOperationException( + withErrorContext("TableDefinition has no data access")); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index d05f640..c01c1a5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -2231,8 +2231,8 @@ public class TableImpl implements Table, PropertyMaps.Owner * rows have been written, and every time a data page is filled. * @param rows List of Object[] row values */ - private List<? extends Object[]> addRows(List<? extends Object[]> rows, - final boolean isBatchWrite) + protected List<? extends Object[]> addRows(List<? extends Object[]> rows, + final boolean isBatchWrite) throws IOException { if(rows.isEmpty()) { diff --git a/src/test/data/V1997/test2V1997.mdb b/src/test/data/V1997/test2V1997.mdb Binary files differindex b8b2ca2..8a6c47a 100644 --- a/src/test/data/V1997/test2V1997.mdb +++ b/src/test/data/V1997/test2V1997.mdb diff --git a/src/test/data/V2007/odbcLinkerTestV2007.accdb b/src/test/data/V2007/odbcLinkerTestV2007.accdb Binary files differnew file mode 100644 index 0000000..b610534 --- /dev/null +++ b/src/test/data/V2007/odbcLinkerTestV2007.accdb diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 5f5a488..3b90545 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -38,17 +38,16 @@ import java.util.UUID; import java.util.stream.Collectors; import static com.healthmarketscience.jackcess.Database.*; +import static com.healthmarketscience.jackcess.DatabaseBuilder.*; +import static com.healthmarketscience.jackcess.TestUtil.*; import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import com.healthmarketscience.jackcess.impl.RowIdImpl; import com.healthmarketscience.jackcess.impl.RowImpl; import com.healthmarketscience.jackcess.impl.TableImpl; -import com.healthmarketscience.jackcess.util.LinkResolver; import com.healthmarketscience.jackcess.util.RowFilterTest; import junit.framework.TestCase; -import static com.healthmarketscience.jackcess.TestUtil.*; -import static com.healthmarketscience.jackcess.DatabaseBuilder.*; /** @@ -865,123 +864,7 @@ public class DatabaseTest extends TestCase } } - public void testLinkedTables() throws Exception { - for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.LINKED)) { - Database db = openCopy(testDB); - - try { - db.getTable("Table2"); - fail("FileNotFoundException should have been thrown"); - } catch(FileNotFoundException e) { - // success - } - - TableMetaData tmd = db.getTableMetaData("Table2"); - assertEquals("Table2", tmd.getName()); - assertTrue(tmd.isLinked()); - assertFalse(tmd.isSystem()); - assertEquals("Table1", tmd.getLinkedTableName()); - assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName()); - - tmd = db.getTableMetaData("FooTable"); - assertNull(tmd); - - assertTrue(db.getLinkedDatabases().isEmpty()); - - final String linkeeDbName = "Z:\\jackcess_test\\linkeeTest.accdb"; - final File linkeeFile = new File("src/test/data/linkeeTest.accdb"); - db.setLinkResolver(new LinkResolver() { - public Database resolveLinkedDatabase(Database linkerdb, String dbName) - throws IOException { - assertEquals(linkeeDbName, dbName); - return DatabaseBuilder.open(linkeeFile); - } - }); - - Table t2 = db.getTable("Table2"); - - assertEquals(1, db.getLinkedDatabases().size()); - Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName); - assertNotNull(linkeeDb); - assertEquals(linkeeFile, linkeeDb.getFile()); - assertEquals("linkeeTest.accdb", ((DatabaseImpl)linkeeDb).getName()); - - List<? extends Map<String, Object>> expectedRows = - createExpectedTable( - createExpectedRow( - "ID", 1, - "Field1", "bar")); - - assertTable(expectedRows, t2); - - db.createLinkedTable("FooTable", linkeeDbName, "Table2"); - - tmd = db.getTableMetaData("FooTable"); - assertEquals("FooTable", tmd.getName()); - assertTrue(tmd.isLinked()); - assertFalse(tmd.isSystem()); - assertEquals("Table2", tmd.getLinkedTableName()); - assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName()); - - Table t3 = db.getTable("FooTable"); - - assertEquals(1, db.getLinkedDatabases().size()); - - expectedRows = - createExpectedTable( - createExpectedRow( - "ID", 1, - "Field1", "buzz")); - - assertTable(expectedRows, t3); - - tmd = db.getTableMetaData("Table1"); - assertEquals("Table1", tmd.getName()); - assertFalse(tmd.isLinked()); - assertFalse(tmd.isSystem()); - assertNull(tmd.getLinkedTableName()); - assertNull(tmd.getLinkedDbName()); - - Table t1 = tmd.open(db); - - assertFalse(db.isLinkedTable(null)); - assertTrue(db.isLinkedTable(t2)); - assertTrue(db.isLinkedTable(t3)); - assertFalse(db.isLinkedTable(t1)); - - List<Table> tables = getTables(db.newIterable()); - assertEquals(3, tables.size()); - assertTrue(tables.contains(t1)); - assertTrue(tables.contains(t2)); - assertTrue(tables.contains(t3)); - assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - - tables = getTables(db.newIterable().setIncludeNormalTables(false)); - assertEquals(2, tables.size()); - assertFalse(tables.contains(t1)); - assertTrue(tables.contains(t2)); - assertTrue(tables.contains(t3)); - assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - - tables = getTables(db.newIterable().withLocalUserTablesOnly()); - assertEquals(1, tables.size()); - assertTrue(tables.contains(t1)); - assertFalse(tables.contains(t2)); - assertFalse(tables.contains(t3)); - assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - - tables = getTables(db.newIterable().withSystemTablesOnly()); - assertTrue(tables.size() > 5); - assertFalse(tables.contains(t1)); - assertFalse(tables.contains(t2)); - assertFalse(tables.contains(t3)); - assertTrue(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - - db.close(); - } - } - - private static List<Table> getTables(Iterable<Table> tableIter) + static List<Table> getTables(Iterable<Table> tableIter) { List<Table> tableList = new ArrayList<Table>(); for(Table t : tableIter) { @@ -1102,9 +985,6 @@ public class DatabaseTest extends TestCase expectedCreateDate = "2004-05-28T17:51:48.701"; expectedUpdateDate = "2006-07-24T09:56:19.701"; } - System.out.println("FOO " + testDB.getExpectedFileFormat() + " " + - table.getCreatedDate() + " " + - table.getUpdatedDate()); assertEquals(expectedCreateDate, table.getCreatedDate().toString()); assertEquals(expectedUpdateDate, table.getUpdatedDate().toString()); } diff --git a/src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java b/src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java new file mode 100644 index 0000000..5763285 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java @@ -0,0 +1,198 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.healthmarketscience.jackcess.TestUtil.*; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.util.LinkResolver; +import junit.framework.TestCase; + +/** + * + * @author James Ahlborn + */ +public class LinkedTableTest extends TestCase +{ + + public void testLinkedTables() throws Exception { + for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.LINKED)) { + Database db = openCopy(testDB); + + try { + db.getTable("Table2"); + fail("FileNotFoundException should have been thrown"); + } catch(FileNotFoundException e) { + // success + } + + TableMetaData tmd = db.getTableMetaData("Table2"); + assertEquals("Table2", tmd.getName()); + assertTrue(tmd.isLinked()); + assertFalse(tmd.isSystem()); + assertEquals("Table1", tmd.getLinkedTableName()); + assertNull(tmd.getConnectionName()); + assertEquals(TableMetaData.Type.LINKED, tmd.getType()); + assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName()); + assertNull(tmd.getTableDefinition(db)); + + tmd = db.getTableMetaData("FooTable"); + assertNull(tmd); + + assertTrue(db.getLinkedDatabases().isEmpty()); + + final String linkeeDbName = "Z:\\jackcess_test\\linkeeTest.accdb"; + final File linkeeFile = new File("src/test/data/linkeeTest.accdb"); + db.setLinkResolver(new LinkResolver() { + @Override + public Database resolveLinkedDatabase(Database linkerdb, String dbName) + throws IOException { + assertEquals(linkeeDbName, dbName); + return DatabaseBuilder.open(linkeeFile); + } + }); + + Table t2 = db.getTable("Table2"); + + assertEquals(1, db.getLinkedDatabases().size()); + Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName); + assertNotNull(linkeeDb); + assertEquals(linkeeFile, linkeeDb.getFile()); + assertEquals("linkeeTest.accdb", ((DatabaseImpl)linkeeDb).getName()); + + List<? extends Map<String, Object>> expectedRows = + createExpectedTable( + createExpectedRow( + "ID", 1, + "Field1", "bar")); + + assertTable(expectedRows, t2); + + db.createLinkedTable("FooTable", linkeeDbName, "Table2"); + + tmd = db.getTableMetaData("FooTable"); + assertEquals("FooTable", tmd.getName()); + assertTrue(tmd.isLinked()); + assertFalse(tmd.isSystem()); + assertEquals("Table2", tmd.getLinkedTableName()); + assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName()); + + Table t3 = db.getTable("FooTable"); + + assertEquals(1, db.getLinkedDatabases().size()); + + expectedRows = + createExpectedTable( + createExpectedRow( + "ID", 1, + "Field1", "buzz")); + + assertTable(expectedRows, t3); + + tmd = db.getTableMetaData("Table1"); + assertEquals("Table1", tmd.getName()); + assertFalse(tmd.isLinked()); + assertFalse(tmd.isSystem()); + assertNull(tmd.getLinkedTableName()); + assertNull(tmd.getLinkedDbName()); + + Table t1 = tmd.open(db); + + assertFalse(db.isLinkedTable(null)); + assertTrue(db.isLinkedTable(t2)); + assertTrue(db.isLinkedTable(t3)); + assertFalse(db.isLinkedTable(t1)); + + List<Table> tables = DatabaseTest.getTables(db.newIterable()); + assertEquals(3, tables.size()); + assertTrue(tables.contains(t1)); + assertTrue(tables.contains(t2)); + assertTrue(tables.contains(t3)); + assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); + + tables = DatabaseTest.getTables(db.newIterable().setIncludeNormalTables(false)); + assertEquals(2, tables.size()); + assertFalse(tables.contains(t1)); + assertTrue(tables.contains(t2)); + assertTrue(tables.contains(t3)); + assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); + + tables = DatabaseTest.getTables(db.newIterable().withLocalUserTablesOnly()); + assertEquals(1, tables.size()); + assertTrue(tables.contains(t1)); + assertFalse(tables.contains(t2)); + assertFalse(tables.contains(t3)); + assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); + + tables = DatabaseTest.getTables(db.newIterable().withSystemTablesOnly()); + assertTrue(tables.size() > 5); + assertFalse(tables.contains(t1)); + assertFalse(tables.contains(t2)); + assertFalse(tables.contains(t3)); + assertTrue(tables.contains(((DatabaseImpl)db).getSystemCatalog())); + + db.close(); + } + } + + public void testOdbcLinkedTables() throws Exception { + for (final TestDB testDB : + TestDB.getSupportedForBasename(Basename.LINKED_ODBC)) { + Database db = openCopy(testDB); + + TableMetaData tmd = db.getTableMetaData("Ordrar"); + assertEquals(TableMetaData.Type.LINKED_ODBC, tmd.getType()); + assertEquals("dbo.Ordrar", tmd.getLinkedTableName()); + assertNull(tmd.getLinkedDbName()); + assertEquals("DSN=Magnapinna;Description=Safexit;UID=safexit;PWD=DummyPassword;APP=Microsoft Office;DATABASE=safexit", tmd.getConnectionName()); + assertFalse(tmd.toString().contains("DummyPassword")); + + TableDefinition t = tmd.getTableDefinition(db); + + List<? extends Column> cols = t.getColumns(); + assertEquals(20, cols.size()); + + List<? extends Index> idxs = t.getIndexes(); + assertEquals(5, idxs.size()); + + Table tbl = db.getTable("Ordrar"); + + try { + tbl.iterator(); + fail("UnsupportedOperationException should have been thrown"); + } catch(UnsupportedOperationException expected) { + // expected + } + + try { + tbl.addRow(1L,"bar"); + fail("UnsupportedOperationException should have been thrown"); + } catch(UnsupportedOperationException expected) { + // expected + } + + db.close(); + } + } + +} diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java index 6f01bca..0f6c889 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java @@ -53,6 +53,7 @@ public class JetFormatTest extends TestCase { COMPLEX("complexDataTest"), UNSUPPORTED("unsupportedFieldsTest"), LINKED("linkerTest"), + LINKED_ODBC("odbcLinkerTest"), BLOB("testOle"), CALC_FIELD("calcFieldTest"), BINARY_INDEX("binIdxTest"), |