git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1379 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-4.0.2
@@ -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"> |
@@ -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); | |||
} | |||
@@ -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); | |||
} | |||
@@ -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); | |||
/** |
@@ -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); | |||
} |
@@ -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; | |||
} |
@@ -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; |
@@ -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")); | |||
} | |||
} |
@@ -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()) { |
@@ -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()); | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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"), |