From b5aa0575e557d975a062fb7eb34f1ecb5b6a5a29 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 26 Aug 2022 21:02:28 +0000 Subject: [PATCH] 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 --- src/changes/changes.xml | 6 +- .../jackcess/ColumnBuilder.java | 8 + .../jackcess/IndexBuilder.java | 16 +- .../healthmarketscience/jackcess/Table.java | 16 +- .../jackcess/TableDefinition.java | 125 +++++++++ .../jackcess/TableMetaData.java | 25 +- .../jackcess/impl/DatabaseImpl.java | 248 ++++++++++++------ .../jackcess/impl/TableDefinitionImpl.java | 60 +++++ .../jackcess/impl/TableImpl.java | 4 +- src/test/data/V1997/test2V1997.mdb | Bin 122880 -> 137216 bytes src/test/data/V2007/odbcLinkerTestV2007.accdb | Bin 0 -> 610304 bytes .../jackcess/DatabaseTest.java | 126 +-------- .../jackcess/LinkedTableTest.java | 198 ++++++++++++++ .../jackcess/impl/JetFormatTest.java | 1 + 14 files changed, 627 insertions(+), 206 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/TableDefinition.java create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java create mode 100644 src/test/data/V2007/odbcLinkerTestV2007.accdb create mode 100644 src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java 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 @@ - Add Table methods to access the creation and last modified dates. + Add Table methods to get the creation and last modified dates. + + + Add support for linked odbc tables. TableMetaData provides access to + connection string and local TableDefinition if available. 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 +public interface Table extends Iterable, TableDefinition { /** * enum which controls the ordering of the columns in a table. @@ -63,28 +63,33 @@ public interface Table extends Iterable * @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 * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ */ + @Override public List 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 * @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 getIndexes(); /** @@ -160,6 +171,7 @@ public interface Table extends Iterable * @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 * table * @usage _intermediate_method_ */ + @Override public Index getPrimaryKeyIndex(); /** @@ -176,6 +189,7 @@ public interface Table extends Iterable * 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. + *

+ * 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 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 does not automatically update the modified date of a + * Table. + * + * @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 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. */ @@ -55,8 +64,20 @@ 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 SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS = new HashSet(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 SYSTEM_CATALOG_PROPS_COLUMNS = new HashSet(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 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 @@ -2308,11 +2353,21 @@ public class DatabaseImpl implements Database, DateTimeContext return null; } + @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") @@ -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). + *

+ * 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. + *

+ * 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 addRows(List 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 addRows(List rows, - final boolean isBatchWrite) + protected List addRows(List 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 index b8b2ca204c4ec9773348e960f9d79bc2d8f467bf..8a6c47a8f3a4ccf7f0f43c769ab2d5f08454d1f6 100644 GIT binary patch delta 9155 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 E2PqHC(f|Me delta 441 zcmZqJz|nAkeE}OYBO}*lLALjdlNH$bHgUADvq&&Be3;xQUNK34hf#PkuYeM#Farb# zPd1Pc-Rvu{pPkWaGOvUZqwr<}iA(xSb*z&c-9;x$dPs;cGB_|WC@?T^FfcHI7(kxD zbVhZ?wuu{bC;#xcxcRB4G~2`tGMfUJIGAGDfck_di${n~ZVIoPxWRAokBE-Voss20 z6`Y$AfGYm6Og5YVRFUC0-G2w;g3UiN<}+?qTD6OD~Gobv7Kj&Ve@6PV>`{dhjp@ThWupx435p7EAm+;KbXTd`TTmr$?xZ!-MnY5Pu6yc z1B}JY(`Ak@=52mC*#hRe^Bhc!Va`Bv8E`1LxSdIZ@n8S81B_iPo4d}GF)us7r~m*( Cf{!Br diff --git a/src/test/data/V2007/odbcLinkerTestV2007.accdb b/src/test/data/V2007/odbcLinkerTestV2007.accdb new file mode 100644 index 0000000000000000000000000000000000000000..b6105340dd9465406aae0d32ac9f1a301182b138 GIT binary patch literal 610304 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 JdH>vu`VHG^P1XPa literal 0 HcmV?d00001 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> 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 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
getTables(Iterable
tableIter) + static List
getTables(Iterable
tableIter) { List
tableList = new ArrayList
(); 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> 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
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 cols = t.getColumns(); + assertEquals(20, cols.size()); + + List 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"), -- 2.39.5