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

<body> <body>
<release version="4.0.2" date="TBD"> <release version="4.0.2" date="TBD">
<action dev="jahlborn" type="update"> <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> </action>
</release> </release>
<release version="4.0.1" date="2021-06-21"> <release version="4.0.1" date="2021-06-21">

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

* attributes. * attributes.
*/ */
public Column addToTable(Table table) throws IOException { 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); return new TableUpdater((TableImpl)table).addColumn(this);
} }



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

* @see TableBuilder * @see TableBuilder
* @usage _general_class_ * @usage _general_class_
*/ */
public class IndexBuilder
public class IndexBuilder
{ {
/** name typically used by MS Access for the primary key index */ /** name typically used by MS Access for the primary key index */
public static final String PRIMARY_KEY_NAME = "PrimaryKey"; public static final String PRIMARY_KEY_NAME = "PrimaryKey";
public IndexBuilder setUnique() { public IndexBuilder setUnique() {
_flags |= IndexData.UNIQUE_INDEX_FLAG; _flags |= IndexData.UNIQUE_INDEX_FLAG;
return this; return this;
}
}


/** /**
* Sets this index to encforce required. * Sets this index to encforce required.
public IndexBuilder setRequired() { public IndexBuilder setRequired() {
_flags |= IndexData.REQUIRED_INDEX_FLAG; _flags |= IndexData.REQUIRED_INDEX_FLAG;
return this; return this;
}
}


/** /**
* Sets this index to ignore null values. * Sets this index to ignore null values.
public IndexBuilder setIgnoreNulls() { public IndexBuilder setIgnoreNulls() {
_flags |= IndexData.IGNORE_NULLS_INDEX_FLAG; _flags |= IndexData.IGNORE_NULLS_INDEX_FLAG;
return this; return this;
}
}


/** /**
* @usage _advanced_method_ * @usage _advanced_method_
* attributes. * attributes.
*/ */
public Index addToTable(Table table) throws IOException { 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); return new TableUpdater((TableImpl)table).addIndex(this);
} }



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

* @author James Ahlborn * @author James Ahlborn
* @usage _general_class_ * @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. * enum which controls the ordering of the columns in a table.
* @return The name of the table * @return The name of the table
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public String getName(); public String getName();


/** /**
* Whether or not this table has been marked as hidden. * Whether or not this table has been marked as hidden.
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public boolean isHidden(); public boolean isHidden();


/** /**
* Whether or not this table is a system (internal) table. * Whether or not this table is a system (internal) table.
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public boolean isSystem(); public boolean isSystem();


/** /**
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public int getColumnCount(); public int getColumnCount();


/** /**
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public Database getDatabase(); public Database getDatabase();


/** /**
* @return All of the columns in this table (unmodifiable List) * @return All of the columns in this table (unmodifiable List)
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public List<? extends Column> getColumns(); public List<? extends Column> getColumns();


/** /**
* @return the column with the given name * @return the column with the given name
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public Column getColumn(String name); public Column getColumn(String name);


/** /**
* @return the properties for this table * @return the properties for this table
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public PropertyMap getProperties() throws IOException; public PropertyMap getProperties() throws IOException;


/** /**
* @return the created date for this table if available * @return the created date for this table if available
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public LocalDateTime getCreatedDate() throws IOException; public LocalDateTime getCreatedDate() throws IOException;


/** /**
* @return the last updated date for this table if available * @return the last updated date for this table if available
* @usage _general_method_ * @usage _general_method_
*/ */
@Override
public LocalDateTime getUpdatedDate() throws IOException; public LocalDateTime getUpdatedDate() throws IOException;


/** /**
* @return All of the Indexes on this table (unmodifiable List) * @return All of the Indexes on this table (unmodifiable List)
* @usage _intermediate_method_ * @usage _intermediate_method_
*/ */
@Override
public List<? extends Index> getIndexes(); public List<? extends Index> getIndexes();


/** /**
* @throws IllegalArgumentException if there is no index with the given name * @throws IllegalArgumentException if there is no index with the given name
* @usage _intermediate_method_ * @usage _intermediate_method_
*/ */
@Override
public Index getIndex(String name); public Index getIndex(String name);


/** /**
* table * table
* @usage _intermediate_method_ * @usage _intermediate_method_
*/ */
@Override
public Index getPrimaryKeyIndex(); public Index getPrimaryKeyIndex();


/** /**
* table and the given table * table and the given table
* @usage _intermediate_method_ * @usage _intermediate_method_
*/ */
@Override
public Index getForeignKeyIndex(Table otherTable); public Index getForeignKeyIndex(Table otherTable);


/** /**

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

/*
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

* @author James Ahlborn * @author James Ahlborn
* @usage _intermediate_class_ * @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) * The name of the table (as it is stored in the database)
*/ */
* {@code true} if this is a linked table, {@code false} otherwise. * {@code true} if this is a linked table, {@code false} otherwise.
*/ */
public boolean isLinked(); public boolean isLinked();
/** /**
* {@code true} if this is a system table, {@code false} otherwise. * {@code true} if this is a system table, {@code false} otherwise.
*/ */
*/ */
public String getLinkedDbName(); 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. * Opens this table from the given Database instance.
*/ */
public Table open(Database db) throws IOException; 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

import com.healthmarketscience.jackcess.RuntimeIOException; import com.healthmarketscience.jackcess.RuntimeIOException;
import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.Table;
import com.healthmarketscience.jackcess.TableBuilder; import com.healthmarketscience.jackcess.TableBuilder;
import com.healthmarketscience.jackcess.TableDefinition;
import com.healthmarketscience.jackcess.TableMetaData; import com.healthmarketscience.jackcess.TableMetaData;
import com.healthmarketscience.jackcess.expr.EvalConfig; import com.healthmarketscience.jackcess.expr.EvalConfig;
import com.healthmarketscience.jackcess.impl.query.QueryImpl; import com.healthmarketscience.jackcess.impl.query.QueryImpl;
private static final String CAT_COL_DATABASE = "Database"; private static final String CAT_COL_DATABASE = "Database";
/** System catalog column name of the remote table name */ /** System catalog column name of the remote table name */
private static final String CAT_COL_FOREIGN_NAME = "ForeignName"; 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 */ /** top-level parentid for a database */
private static final int DB_PARENT_ID = 0xF000000; private static final int DB_PARENT_ID = 0xF000000;
private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined"; private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined";
/** System object type for table definitions */ /** System object type for table definitions */
static final Short TYPE_TABLE = 1; 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 */ /** System object type for query definitions */
private static final Short TYPE_QUERY = 5; private static final Short TYPE_QUERY = 5;
/** System object type for linked table definitions */ /** System object type for linked table definitions */
private static Collection<String> SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS = private static Collection<String> SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS =
new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
CAT_COL_FLAGS, CAT_COL_PARENT_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 */ /** the columns to read when getting object propertyes */
private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS = private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS =
new HashSet<String>(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS)); new HashSet<String>(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS));
private static final Pattern INVALID_IDENTIFIER_CHARS = private static final Pattern INVALID_IDENTIFIER_CHARS =
Pattern.compile("[\\p{Cntrl}.!`\\]\\[]"); 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 */ /** the File of the database */
private final Path _file; private final Path _file;
/** the simple name of the database */ /** the simple name of the database */
private boolean _evaluateExpressions; private boolean _evaluateExpressions;
/** factory for ColumnValidators */ /** factory for ColumnValidators */
private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE;
/** cache of in-use tables */
/** cache of in-use tables (or table definitions) */
private final TableCache _tableCache = new TableCache(); private final TableCache _tableCache = new TableCache();
/** handler for reading/writing properteies */ /** handler for reading/writing properteies */
private PropertyMaps.Handler _propsHandler; private PropertyMaps.Handler _propsHandler;


// common case, local table name == remote table name // common case, local table name == remote table name
TableInfo tableInfo = lookupTable(table.getName()); 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; return true;
} }


* Read the system catalog * Read the system catalog
*/ */
private void readSystemCatalog() throws IOException { 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 { try {
_tableFinder = new DefaultTableFinder( _tableFinder = new DefaultTableFinder(
* @usage _advanced_method_ * @usage _advanced_method_
*/ */
public TableImpl getTable(int tableDefPageNumber) throws IOException { 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);
} }


/** /**
private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables) private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables)
throws IOException throws IOException
{ {
if(tableInfo.isLinked()) {
if(tableInfo.getType() == TableMetaData.Type.LINKED) {


if(_linkedDbs == null) { if(_linkedDbs == null) {
_linkedDbs = new HashMap<String,Database>(); _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); Database linkedDb = _linkedDbs.get(linkedDbName);
if(linkedDb == null) { if(linkedDb == null) {
linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName); linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName);
includeSystemTables); includeSystemTables);
} }


return readTable(tableInfo.tableName, tableInfo.pageNumber,
tableInfo.flags);
return loadTable(tableInfo.tableName, tableInfo.pageNumber,
tableInfo.flags, tableInfo.tableType);
} }


/** /**
} }
} }


private String createRelationshipName(RelationshipCreator creator)
throws IOException
{
private String createRelationshipName(RelationshipCreator creator) {
// ensure that the final identifier name does not get too long // ensure that the final identifier name does not get too long
// - the primary name is limited to ((max / 2) - 3) // - the primary name is limited to ((max / 2) - 3)
// - the total name is limited to (max - 3) // - the total name is limited to (max - 3)
/** /**
* Reads a table with the given name from the given pageNumber. * 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 throws IOException
{ {
// first, check for existing table // first, check for existing table
return table; 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(); ByteBuffer buffer = takeSharedBuffer();
try { try {
// need to load table from db // need to load table from db
"Looking for " + name + " at page " + pageNumber + "Looking for " + name + " at page " + pageNumber +
", but page type is " + pageType)); ", 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 { } finally {
releaseSharedBuffer(buffer); releaseSharedBuffer(buffer);
} }
{ {
_tableLookup.put(toLookupName(tableName), _tableLookup.put(toLookupName(tableName),
createTableInfo(tableName, pageNumber, 0, type, createTableInfo(tableName, pageNumber, 0, type,
linkedDbName, linkedTableName));
linkedDbName, linkedTableName, null));
// clear this, will be created next time needed // clear this, will be created next time needed
_tableNames = null; _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. * Creates a TableInfo instance appropriate for the given table data.
*/ */
private static TableInfo createTableInfo( private static TableInfo createTableInfo(
String tableName, Integer pageNumber, int flags, Short type, String tableName, Integer pageNumber, int flags, Short type,
String linkedDbName, String linkedTableName)
String linkedDbName, String linkedTableName, String connectName)
{ {
if(TYPE_LINKED_TABLE.equals(type)) { 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);
} }


/** /**
} }


private static boolean isTableType(Short objType) { 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) { public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) {
return defaultValue; 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. * Utility class for storing table page number and actual name.
*/ */
public final Integer pageNumber; public final Integer pageNumber;
public final String tableName; public final String tableName;
public final int flags; 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; pageNumber = newPageNumber;
tableName = newTableName; tableName = newTableName;
flags = newFlags; flags = newFlags;
tableType = newTableType;
}

@Override
public Type getType() {
return Type.LOCAL;
} }


@Override @Override
return null; return null;
} }


@Override
public String getConnectionName() {
return null;
}

@Override @Override
public Table open(Database db) throws IOException { public Table open(Database db) throws IOException {
return ((DatabaseImpl)db).getTable(this, true); return ((DatabaseImpl)db).getTable(this, true);
} }


@Override
public TableDefinition getTableDefinition(Database db) throws IOException {
return null;
}

@Override @Override
public String toString() { public String toString() {
ToStringBuilder sb = CustomToStringStyle.valueBuilder("TableMetaData") ToStringBuilder sb = CustomToStringStyle.valueBuilder("TableMetaData")
if(isLinked()) { if(isLinked()) {
sb.append("isLinked", isLinked()) sb.append("isLinked", isLinked())
.append("linkedTableName", getLinkedTableName()) .append("linkedTableName", getLinkedTableName())
.append("linkedDbName", getLinkedDbName());
.append("linkedDbName", getLinkedDbName())
.append("connectionName", maskPassword(getConnectionName()));
} }
return sb.toString(); return sb.toString();
} }

private static String maskPassword(String connectionName) {
return ((connectionName != null) ?
ODBC_PWD_PATTERN.matcher(connectionName).replaceAll("PWD=XXXXXX") :
null);
}
} }


/** /**
*/ */
private static class LinkedTableInfo extends TableInfo 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, private LinkedTableInfo(Integer newPageNumber, String newTableName,
int newFlags, String newLinkedDbName,
int newFlags, Short newTableType,
String newLinkedDbName,
String newLinkedTableName) { 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 @Override


@Override @Override
public String getLinkedTableName() { public String getLinkedTableName() {
return linkedTableName;
return _linkedTableName;
} }


@Override @Override
public String getLinkedDbName() { 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);
} }
} }


} else if(systemTables) { } else if(systemTables) {
tableNames.add(tableName); tableNames.add(tableName);
} }
} else if(TYPE_LINKED_TABLE.equals(type) && linkedTables) {
} else if(linkedTables && isAnyLinkedTableType(type)) {
tableNames.add(tableName); tableNames.add(tableName);
} }
} }
} }


String realName = row.getString(CAT_COL_NAME); 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; return null;


Row row = _systemCatalogCursor.getCurrentRow( Row row = _systemCatalogCursor.getCurrentRow(
SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS); 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); Short type = row.getShort(CAT_COL_TYPE);


if(!isTableType(type)) { if(!isTableType(type)) {
return null; 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 @Override
continue; 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; return null;

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

/*
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

* rows have been written, and every time a data page is filled. * rows have been written, and every time a data page is filled.
* @param rows List of Object[] row values * @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 throws IOException
{ {
if(rows.isEmpty()) { 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

import java.util.stream.Collectors; import java.util.stream.Collectors;


import static com.healthmarketscience.jackcess.Database.*; 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.ColumnImpl;
import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
import com.healthmarketscience.jackcess.impl.RowIdImpl; import com.healthmarketscience.jackcess.impl.RowIdImpl;
import com.healthmarketscience.jackcess.impl.RowImpl; import com.healthmarketscience.jackcess.impl.RowImpl;
import com.healthmarketscience.jackcess.impl.TableImpl; import com.healthmarketscience.jackcess.impl.TableImpl;
import com.healthmarketscience.jackcess.util.LinkResolver;
import com.healthmarketscience.jackcess.util.RowFilterTest; import com.healthmarketscience.jackcess.util.RowFilterTest;
import junit.framework.TestCase; import junit.framework.TestCase;
import static com.healthmarketscience.jackcess.TestUtil.*;
import static com.healthmarketscience.jackcess.DatabaseBuilder.*;




/** /**
} }
} }


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>(); List<Table> tableList = new ArrayList<Table>();
for(Table t : tableIter) { for(Table t : tableIter) {
expectedCreateDate = "2004-05-28T17:51:48.701"; expectedCreateDate = "2004-05-28T17:51:48.701";
expectedUpdateDate = "2006-07-24T09:56:19.701"; expectedUpdateDate = "2006-07-24T09:56:19.701";
} }
System.out.println("FOO " + testDB.getExpectedFileFormat() + " " +
table.getCreatedDate() + " " +
table.getUpdatedDate());
assertEquals(expectedCreateDate, table.getCreatedDate().toString()); assertEquals(expectedCreateDate, table.getCreatedDate().toString());
assertEquals(expectedUpdateDate, table.getUpdatedDate().toString()); assertEquals(expectedUpdateDate, table.getUpdatedDate().toString());
} }

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

/*
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

COMPLEX("complexDataTest"), COMPLEX("complexDataTest"),
UNSUPPORTED("unsupportedFieldsTest"), UNSUPPORTED("unsupportedFieldsTest"),
LINKED("linkerTest"), LINKED("linkerTest"),
LINKED_ODBC("odbcLinkerTest"),
BLOB("testOle"), BLOB("testOle"),
CALC_FIELD("calcFieldTest"), CALC_FIELD("calcFieldTest"),
BINARY_INDEX("binIdxTest"), BINARY_INDEX("binIdxTest"),

Loading…
Cancel
Save