Browse Source

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
tags/jackcess-4.0.2
James Ahlborn 1 year ago
parent
commit
b5aa0575e5

+ 5
- 1
src/changes/changes.xml View File

@@ -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">

+ 8
- 0
src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java View File

@@ -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);
}


+ 12
- 4
src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java View File

@@ -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);
}


+ 15
- 1
src/main/java/com/healthmarketscience/jackcess/Table.java View File

@@ -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);

/**

+ 125
- 0
src/main/java/com/healthmarketscience/jackcess/TableDefinition.java View File

@@ -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);
}

+ 23
- 2
src/main/java/com/healthmarketscience/jackcess/TableMetaData.java View File

@@ -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;
}

+ 175
- 73
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java View File

@@ -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
@@ -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;

+ 60
- 0
src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java View File

@@ -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"));
}
}

+ 2
- 2
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java View File

@@ -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()) {

BIN
src/test/data/V1997/test2V1997.mdb View File


BIN
src/test/data/V2007/odbcLinkerTestV2007.accdb View File


+ 3
- 123
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java View File

@@ -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());
}

+ 198
- 0
src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java View File

@@ -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();
}
}

}

+ 1
- 0
src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java View File

@@ -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"),

Loading…
Cancel
Save