]> source.dussan.org Git - jackcess.git/commitdiff
add initial support for linked tables (fixes issue 3479560)
authorJames Ahlborn <jtahlborn@yahoo.com>
Wed, 1 Feb 2012 04:25:23 +0000 (04:25 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Wed, 1 Feb 2012 04:25:23 +0000 (04:25 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@610 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/LinkResolver.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/PageChannel.java
src/java/com/healthmarketscience/jackcess/PropertyMaps.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/TableCreator.java
test/data/V2007/linkerTestV2007.accdb [new file with mode: 0755]
test/data/linkeeTest.accdb [new file with mode: 0755]
test/src/java/com/healthmarketscience/jackcess/DatabaseTest.java
test/src/java/com/healthmarketscience/jackcess/JetFormatTest.java

index 659eaefd26b6d105f10657679a520b5309a28814..df8d1752251ae1354bf64cf92a88a696eac81dda 100644 (file)
@@ -8,6 +8,11 @@
       <action dev="jahlborn" type="update">
         Rename and refactor the Cursor find methods to improve usability.
       </action>
+      <action dev="jahlborn" type="update" issue="3479560">
+        Add initial support for linked tables.  Linked tables will now be
+        loaded by the normal Database.getTable method.  Linked tables can be
+        created using the new Database.createLinkedTable method.
+      </action>
     </release>
     <release version="1.2.6" date="2011-12-16">
       <action dev="jahlborn" type="update" issue="3358272">
index 174f95f5c4bdde205a13ec956bc5ba8b12211e78..4ef116a45fccaac847ac8e6228f7924ac17878be 100644 (file)
@@ -47,6 +47,7 @@ import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.ConcurrentModificationException;
 import java.util.Date;
 import java.util.HashMap;
@@ -182,6 +183,19 @@ public class Database
       }
     };
 
+  /**
+   * default link resolver used if none provided
+   * @usage _general_field_
+   */
+  public static final LinkResolver DEFAULT_LINK_RESOLVER = new LinkResolver() {
+      public Database resolveLinkedDatabase(Database linkerDb, 
+                                            String linkeeFileName)
+        throws IOException
+      {
+        return Database.open(new File(linkeeFileName));
+      }
+    };
+
   /** the resource path to be used when loading classpath resources */
   static final String RESOURCE_PATH = 
     System.getProperty(RESOURCE_PATH_PROPERTY, DEFAULT_RESOURCE_PATH);
@@ -244,6 +258,10 @@ public class Database
   private static final String CAT_COL_FLAGS = "Flags";
   /** System catalog column name of the properties column */
   private static final String CAT_COL_PROPS = "LvProp";
+  /** System catalog column name of the remote database */
+  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";
 
   /** top-level parentid for a database */
   private static final int DB_PARENT_ID = 0xF000000;
@@ -320,9 +338,11 @@ public class Database
   /** Name of the user-defined properties object */
   private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined";
   /** System object type for table definitions */
-  private static final Short TYPE_TABLE = (short) 1;
+  static final Short TYPE_TABLE = 1;
   /** System object type for query definitions */
-  private static final Short TYPE_QUERY = (short) 5;
+  private static final Short TYPE_QUERY = 5;
+  /** System object type for linked table definitions */
+  private static final Short TYPE_LINKED_TABLE = 6;
 
   /** max number of table lookups to cache */
   private static final int MAX_CACHED_LOOKUP_TABLES = 50;
@@ -330,7 +350,8 @@ public class Database
   /** the columns to read when reading system catalog normally */
   private static Collection<String> SYSTEM_CATALOG_COLUMNS =
     new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
-                                      CAT_COL_FLAGS));
+                                      CAT_COL_FLAGS, CAT_COL_DATABASE, 
+                                      CAT_COL_FOREIGN_NAME));
   /** the columns to read when finding table names */
   private static Collection<String> SYSTEM_CATALOG_TABLE_NAME_COLUMNS =
     new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, 
@@ -380,7 +401,9 @@ public class Database
        "yesno"
     ));
   }
-  
+
+  /** the File of the database */
+  private final File _file;
   /** Buffer to hold database pages */
   private ByteBuffer _buffer;
   /** ID of the Tables system object */
@@ -446,6 +469,10 @@ public class Database
   private PropertyMaps _summaryPropMaps;
   /** user-defined properties */
   private PropertyMaps _userDefPropMaps;
+  /** linked table resolver */
+  private LinkResolver _linkResolver;
+  /** any linked databases which have been opened */
+  private Map<String,Database> _linkedDbs;
 
 
   /**
@@ -584,7 +611,8 @@ public class Database
       }
     }
 
-    return new Database(channel, autoSync, null, charset, timeZone, provider);
+    return new Database(mdbFile, channel, autoSync, null, charset, timeZone, 
+                        provider);
   }
   
   /**
@@ -698,8 +726,8 @@ public class Database
     channel.truncate(0);
     transferFrom(channel, getResourceAsStream(fileFormat._emptyFile));
     channel.force(true);
-    return new Database(channel, autoSync, fileFormat, charset, timeZone,
-                        null);
+    return new Database(mdbFile, channel, autoSync, fileFormat, charset, 
+                        timeZone, null);
   }
 
   /**
@@ -724,6 +752,7 @@ public class Database
   
   /**
    * Create a new database by reading it in from a FileChannel.
+   * @param file the File to which the channel is connected 
    * @param channel File channel of the database.  This needs to be a
    *    FileChannel instead of a ReadableByteChannel because we need to
    *    randomly jump around to various points in the file.
@@ -739,14 +768,14 @@ public class Database
    * @param charset Charset to use, if {@code null}, uses default
    * @param timeZone TimeZone to use, if {@code null}, uses default
    */
-  protected Database(FileChannel channel, boolean autoSync,
+  protected Database(File file, FileChannel channel, boolean autoSync,
                      FileFormat fileFormat, Charset charset, TimeZone timeZone,
                      CodecProvider provider)
     throws IOException
   {
     boolean success = false;
     try {
-
+      _file = file;
       _format = JetFormat.getFormat(channel);
       _charset = ((charset == null) ? getDefaultCharset(_format) : charset);
       _columnOrder = getDefaultColumnOrder();
@@ -776,6 +805,13 @@ public class Database
     }
   }
 
+  /**
+   * Returns the File underlying this Database
+   */
+  public File getFile() {
+    return _file;
+  }
+
   /**
    * @usage _advanced_method_
    */
@@ -865,6 +901,35 @@ public class Database
     _dbErrorHandler = newErrorHandler;
   }    
 
+  /**
+   * Gets the currently configured LinkResolver (always non-{@code null}).
+   * This will be used to handle all linked database loading.
+   * @usage _intermediate_method_
+   */
+  public LinkResolver getLinkResolver() {
+    return((_linkResolver != null) ? _linkResolver : DEFAULT_LINK_RESOLVER);
+  }
+
+  /**
+   * Sets a new LinkResolver.  If {@code null}, resets to the
+   * {@link #DEFAULT_LINK_RESOLVER}.
+   * @usage _intermediate_method_
+   */
+  public void setLinkResolver(LinkResolver newLinkResolver) {
+    _linkResolver = newLinkResolver;
+  }    
+
+  /**
+   * Returns an unmodifiable view of the currently loaded linked databases,
+   * mapped from the linked database file name to the linked database.  This
+   * information may be useful for implementing a LinkResolver.
+   * @usage _intermediate_method_
+   */
+  public Map<String,Database> getLinkedDatabases() {
+    return ((_linkedDbs == null) ? Collections.<String,Database>emptyMap() : 
+            Collections.unmodifiableMap(_linkedDbs));
+  }
+
   /**
    * Gets currently configured TimeZone (always non-{@code null}).
    * @usage _intermediate_method_
@@ -1198,6 +1263,24 @@ public class Database
       return null;
     }
 
+    if(tableInfo.isLinked()) {
+
+      if(_linkedDbs == null) {
+        _linkedDbs = new HashMap<String,Database>();
+      }
+
+      String linkedDbName = ((LinkedTableInfo)tableInfo).linkedDbName;
+      String linkedTableName = ((LinkedTableInfo)tableInfo).linkedTableName;
+      Database linkedDb = _linkedDbs.get(linkedDbName);
+      if(linkedDb == null) {
+        linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName);
+        _linkedDbs.put(linkedDbName, linkedDb);
+      }
+      
+      return linkedDb.getTable(linkedTableName, includeSystemTables, 
+                               useBigIndex);
+    }
+
     return readTable(tableInfo.tableName, tableInfo.pageNumber,
                      tableInfo.flags, useBigIndex);
   }
@@ -1233,15 +1316,46 @@ public class Database
     new TableCreator(this, name, columns, indexes).createTable();
   }
 
+  /**
+   * Create a new table in this database
+   * @param name Name of the table to create
+   * @usage _general_method_
+   */
+  public void createLinkedTable(String name, String linkedDbName, 
+                                String linkedTableName)
+    throws IOException
+  {
+    if(lookupTable(name) != null) {
+      throw new IllegalArgumentException(
+          "Cannot create linked table with name of existing table");
+    }
+
+    validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table");
+    validateIdentifierName(linkedDbName, DataType.MEMO.getMaxSize(), 
+                           "linked database");
+    validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH, 
+                           "linked table");
+
+    int linkedTableId = _tableFinder.getNextFreeSyntheticId();
+
+    addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName, 
+                linkedTableName);
+  }
+
   /**
    * Adds a newly created table to the relevant internal database structures.
    */
-  void addNewTable(String name, int tdefPageNumber) throws IOException {
+  void addNewTable(String name, int tdefPageNumber, Short type, 
+                   String linkedDbName, String linkedTableName) 
+    throws IOException 
+  {
     //Add this table to our internal list.
-    addTable(name, Integer.valueOf(tdefPageNumber));
+    addTable(name, Integer.valueOf(tdefPageNumber), type, linkedDbName,
+             linkedTableName);
     
     //Add this table to system tables
-    addToSystemCatalog(name, tdefPageNumber);
+    addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, 
+                       linkedTableName);
     addToAccessControlEntries(tdefPageNumber);
   }
 
@@ -1539,7 +1653,8 @@ public class Database
    * @param name Table name
    * @param pageNumber Page number that contains the table definition
    */
-  private void addToSystemCatalog(String name, int pageNumber)
+  private void addToSystemCatalog(String name, int pageNumber, Short type, 
+                                  String linkedDbName, String linkedTableName)
     throws IOException
   {
     Object[] catalogRow = new Object[_systemCatalog.getColumnCount()];
@@ -1554,7 +1669,7 @@ public class Database
       } else if (CAT_COL_NAME.equals(col.getName())) {
         catalogRow[idx] = name;
       } else if (CAT_COL_TYPE.equals(col.getName())) {
-        catalogRow[idx] = TYPE_TABLE;
+        catalogRow[idx] = type;
       } else if (CAT_COL_DATE_CREATE.equals(col.getName()) ||
                  CAT_COL_DATE_UPDATE.equals(col.getName())) {
         catalogRow[idx] = creationTime;
@@ -1567,6 +1682,10 @@ public class Database
         catalogRow[idx] = owner;
         owner[0] = (byte) 0xcf;
         owner[1] = (byte) 0x5f;
+      } else if (CAT_COL_DATABASE.equals(col.getName())) {
+        catalogRow[idx] = linkedDbName;
+      } else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) {
+        catalogRow[idx] = linkedTableName;
       }
     }
     _systemCatalog.addRow(catalogRow);
@@ -1786,18 +1905,29 @@ public class Database
   }
 
   /**
-   * Flushes any current changes to the database file to disk.
+   * Flushes any current changes to the database file (and any linked
+   * databases) to disk.
    * @usage _general_method_
    */
   public void flush() throws IOException {
+    if(_linkedDbs != null) {
+      for(Database linkedDb : _linkedDbs.values()) {
+        linkedDb.flush();
+      }
+    }
     _pageChannel.flush();
   }
   
   /**
-   * Close the database file
+   * Close the database file (and any linked databases)
    * @usage _general_method_
    */
   public void close() throws IOException {
+    if(_linkedDbs != null) {
+      for(Database linkedDb : _linkedDbs.values()) {
+        linkedDb.close();
+      }
+    }
     _pageChannel.close();
   }
   
@@ -1848,14 +1978,30 @@ public class Database
   /**
    * Adds a table to the _tableLookup and resets the _tableNames set
    */
-  private void addTable(String tableName, Integer pageNumber)
+  private void addTable(String tableName, Integer pageNumber, Short type, 
+                        String linkedDbName, String linkedTableName)
   {
     _tableLookup.put(toLookupName(tableName),
-                     new TableInfo(pageNumber, tableName, 0));
+                     createTableInfo(tableName, pageNumber, 0, type, 
+                                     linkedDbName, linkedTableName));
     // clear this, will be created next time needed
     _tableNames = null;
   }
 
+  /**
+   * 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)
+  {
+    if(TYPE_LINKED_TABLE.equals(type)) {
+      return new LinkedTableInfo(pageNumber, tableName, flags, linkedDbName,
+                                 linkedTableName);
+    }
+    return new TableInfo(pageNumber, tableName, flags);
+  }
+
   /**
    * @return the tableInfo of the given table, if any
    */
@@ -2036,6 +2182,10 @@ public class Database
     return stream;
   }
 
+  private static boolean isTableType(Short objType) {
+    return(TYPE_TABLE.equals(objType) || TYPE_LINKED_TABLE.equals(objType));
+  }
+
   /**
    * Utility class for storing table page number and actual name.
    */
@@ -2045,13 +2195,37 @@ public class Database
     public final String tableName;
     public final int flags;
 
-    private TableInfo(Integer newPageNumber,
-                      String newTableName, 
-                      int newFlags) {
+    private TableInfo(Integer newPageNumber, String newTableName, int newFlags) {
       pageNumber = newPageNumber;
       tableName = newTableName;
       flags = newFlags;
     }
+
+    public boolean isLinked() {
+      return false;
+    }
+  }
+
+  /**
+   * Utility class for storing linked table info
+   */
+  private static class LinkedTableInfo extends TableInfo
+  {
+    private final String linkedDbName;
+    private final String linkedTableName;
+
+    private LinkedTableInfo(Integer newPageNumber, String newTableName, 
+                            int newFlags, String newLinkedDbName, 
+                            String newLinkedTableName) {
+      super(newPageNumber, newTableName, newFlags);
+      linkedDbName = newLinkedDbName;
+      linkedTableName = newLinkedTableName;
+    }
+
+    @Override
+    public boolean isLinked() {
+      return true;
+    }
   }
 
   /**
@@ -2133,7 +2307,7 @@ public class Database
         Short type = (Short)row.get(CAT_COL_TYPE);
         int parentId = (Integer)row.get(CAT_COL_PARENT_ID);
 
-        if((parentId == _tableParentId) && TYPE_TABLE.equals(type) && 
+        if((parentId == _tableParentId) && isTableType(type) && 
            (isSystemObject(flags) == systemTables)) {
           tableNames.add(tableName);
         }
@@ -2150,6 +2324,18 @@ public class Database
 
     public abstract TableInfo lookupTable(String tableName)
       throws IOException;
+
+    protected abstract int findMaxSyntheticId() throws IOException;
+
+    public int getNextFreeSyntheticId() throws IOException
+    {
+      int maxSynthId = findMaxSyntheticId();
+      if(maxSynthId >= -1) {
+        // bummer, no more ids available
+        throw new IllegalStateException("Too many database objects!");
+      }
+      return maxSynthId + 1;
+    }
   }
 
   /**
@@ -2163,6 +2349,14 @@ public class Database
     private DefaultTableFinder(IndexCursor systemCatalogCursor) {
       _systemCatalogCursor = systemCatalogCursor;
     }
+    
+    private void initIdCursor() throws IOException {
+      if(_systemCatalogIdCursor == null) {
+        _systemCatalogIdCursor = new CursorBuilder(_systemCatalog)
+          .setIndexByColumnNames(CAT_COL_ID)
+          .toIndexCursor();
+      }
+    }
 
     @Override
     protected Cursor findRow(Integer parentId, String name) 
@@ -2175,12 +2369,7 @@ public class Database
     @Override
     protected Cursor findRow(Integer objectId) throws IOException 
     {
-      if(_systemCatalogIdCursor == null) {
-        _systemCatalogIdCursor = new CursorBuilder(_systemCatalog)
-          .setIndexByColumnNames(CAT_COL_ID)
-          .toIndexCursor();
-      }
-
+      initIdCursor();
       return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ?
               _systemCatalogIdCursor : null);
     }
@@ -2199,11 +2388,15 @@ public class Database
       int flags = (Integer)row.get(CAT_COL_FLAGS);
       Short type = (Short)row.get(CAT_COL_TYPE);
 
-      if(!TYPE_TABLE.equals(type)) {
+      if(!isTableType(type)) {
         return null;
       }
 
-      return new TableInfo(pageNumber, realName, flags);
+      String linkedDbName = (String)row.get(CAT_COL_DATABASE);
+      String linkedTableName = (String)row.get(CAT_COL_FOREIGN_NAME);
+
+      return createTableInfo(realName, pageNumber, flags, type, linkedDbName,
+                             linkedTableName);
     }
     
     @Override
@@ -2214,6 +2407,21 @@ public class Database
         .setEndEntry(_tableParentId, IndexData.MAX_VALUE)
         .toIndexCursor();
     }
+
+    @Override
+    protected int findMaxSyntheticId() throws IOException {
+      initIdCursor();
+      _systemCatalogIdCursor.reset();
+
+      // synthetic ids count up from min integer.  so the current, highest,
+      // in-use synthetic id is the max id < 0.
+      _systemCatalogIdCursor.findClosestRowByEntry(0);
+      if(!_systemCatalogIdCursor.moveToPreviousRow()) {
+        return Integer.MIN_VALUE;
+      }
+      Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
+      return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol);
+    }
   }
   
   /**
@@ -2253,7 +2461,7 @@ public class Database
               SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) {
 
         Short type = (Short)row.get(CAT_COL_TYPE);
-        if(!TYPE_TABLE.equals(type)) {
+        if(!isTableType(type)) {
           continue;
         }
 
@@ -2269,7 +2477,11 @@ public class Database
 
         Integer pageNumber = (Integer)row.get(CAT_COL_ID);
         int flags = (Integer)row.get(CAT_COL_FLAGS);
-        return new TableInfo(pageNumber, realName, flags);
+        String linkedDbName = (String)row.get(CAT_COL_DATABASE);
+        String linkedTableName = (String)row.get(CAT_COL_FOREIGN_NAME);
+
+        return createTableInfo(realName, pageNumber, flags, type, linkedDbName,
+                               linkedTableName);
       }
 
       return null;
@@ -2279,6 +2491,21 @@ public class Database
     protected Cursor getTableNamesCursor() throws IOException {
       return _systemCatalogCursor;
     }
+
+    @Override
+    protected int findMaxSyntheticId() throws IOException {
+      // find max id < 0
+      Column idCol = _systemCatalog.getColumn(CAT_COL_ID);
+      _systemCatalogCursor.reset();
+      int curMaxSynthId = Integer.MIN_VALUE;
+      while(_systemCatalogCursor.moveToNextRow()) {
+        int id = (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
+        if((id > curMaxSynthId) && (id < 0)) {
+          curMaxSynthId = id;
+        }
+      }
+      return curMaxSynthId;
+    }
   }
 
   /**
@@ -2331,6 +2558,5 @@ public class Database
         _tables.remove(oldRef.getPageNumber());
       }
     }
-  }
-  
+  }  
 }
diff --git a/src/java/com/healthmarketscience/jackcess/LinkResolver.java b/src/java/com/healthmarketscience/jackcess/LinkResolver.java
new file mode 100644 (file)
index 0000000..3ce7315
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.io.IOException;
+
+/**
+ * Resolver for linked databases.
+ *
+ * @author James Ahlborn
+ */
+public interface LinkResolver 
+{
+  /**
+   * Returns the appropriate Database instance for the linkeeFileName from the
+   * given linkerDb.
+   */
+  public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName)
+    throws IOException;
+}
index 083d608d4bfb2b847db07d59bafef200662f25d0..68ba5cd1fa5a99e27341004c5027dfa6218f8416 100644 (file)
@@ -120,6 +120,10 @@ public class PageChannel implements Channel, Flushable {
     return _format;
   }
 
+  public boolean isAutoSync() {
+    return _autoSync;
+  }
+
   /**
    * Returns the next page number based on the given file size.
    */
index 272e8d9bb2d363f63382b48939f48ade4acfb197..2120b56d07422bfa1d92f627644fed1cb3f79abf 100644 (file)
@@ -159,7 +159,7 @@ public class PropertyMaps implements Iterable<PropertyMap>
       }
 
       if(!knownType) {
-        throw new IOException("Uknown property map type " +
+        throw new IOException("Unknown property map type " +
                               ByteUtil.toHexString(bb, 4));
       }
 
index 5819bcd879b2f1d317d565958ee4fc00387dad28..df53a8880f5e4ddb4e13449b1994d0685bc969a0 100644 (file)
@@ -209,7 +209,7 @@ public class Table
    */
   protected Table(Database database, ByteBuffer tableBuffer,
                   int pageNumber, String name, int flags, boolean useBigIndex)
-  throws IOException
+    throws IOException
   {
     _database = database;
     _tableDefPageNumber = pageNumber;
index 556227cd046090e451b16468ff88d777a2c6667c..75aab7c7a6f88c8d39bec82d0e2148ed031202b7 100644 (file)
@@ -132,7 +132,7 @@ class TableCreator
     Table.writeTableDefinition(this);
 
     // update the database with the new table info
-    _database.addNewTable(_name, _tdefPageNumber);
+    _database.addNewTable(_name, _tdefPageNumber, Database.TYPE_TABLE, null, null);
   }
 
   /**
diff --git a/test/data/V2007/linkerTestV2007.accdb b/test/data/V2007/linkerTestV2007.accdb
new file mode 100755 (executable)
index 0000000..57c6b25
Binary files /dev/null and b/test/data/V2007/linkerTestV2007.accdb differ
diff --git a/test/data/linkeeTest.accdb b/test/data/linkeeTest.accdb
new file mode 100755 (executable)
index 0000000..a159097
Binary files /dev/null and b/test/data/linkeeTest.accdb differ
index 4cfd656f895248bad63889b2d61eb0d451b0d107..efda478f5afd3f6695bb4e802ba544c089c64cf9 100644 (file)
@@ -1216,6 +1216,62 @@ 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
+      }
+
+      assertTrue(db.getLinkedDatabases().isEmpty());
+
+      final String linkeeDbName = "Z:\\jackcess_test\\linkeeTest.accdb";
+      final File linkeeFile = new File("test/data/linkeeTest.accdb");
+      db.setLinkResolver(new LinkResolver() {
+        public Database resolveLinkedDatabase(Database linkerdb, String dbName)
+          throws IOException {
+          assertEquals(linkeeDbName, dbName);
+          return Database.open(linkeeFile);
+        }
+      });
+
+      Table t2 = db.getTable("Table2");
+
+      assertEquals(1, db.getLinkedDatabases().size());
+      Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName);
+      assertNotNull(linkeeDb);
+      assertEquals(linkeeFile, linkeeDb.getFile());
+      
+      List<Map<String, Object>> expectedRows =
+        createExpectedTable(
+            createExpectedRow(
+                "ID", 1,
+                "Field1", "bar"));
+
+      assertTable(expectedRows, t2);
+
+      db.createLinkedTable("FooTable", linkeeDbName, "Table2");      
+
+      Table t3 = db.getTable("FooTable");
+
+      assertEquals(1, db.getLinkedDatabases().size());
+
+      expectedRows =
+        createExpectedTable(
+            createExpectedRow(
+                "ID", 1,
+                "Field1", "buzz"));
+
+      assertTable(expectedRows, t3);
+      
+      db.close();
+    }
+  }
+
   private void checkRawValue(String expected, Object val)
   {
     if(expected != null) {
index ec11dce4273cf1dbd5bcccb867fb9a7c8597d1f4..c5cacfbda59a576524566052ed7f71b2c2fcb71f 100644 (file)
@@ -43,7 +43,8 @@ public class JetFormatTest extends TestCase {
     INDEX_PROPERTIES("testIndexProperties"),
     PROMOTION("testPromotion"),
     COMPLEX("complexDataTest"),
-    UNSUPPORTED("unsupportedFieldsTest");
+    UNSUPPORTED("unsupportedFieldsTest"),
+    LINKED("linkerTest");
 
     private final String _basename;