View Javadoc
1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl;
18  
19  import java.io.File;
20  import java.io.FileNotFoundException;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.lang.ref.ReferenceQueue;
24  import java.lang.ref.WeakReference;
25  import java.nio.ByteBuffer;
26  import java.nio.channels.Channels;
27  import java.nio.channels.FileChannel;
28  import java.nio.channels.ReadableByteChannel;
29  import java.nio.charset.Charset;
30  import java.nio.file.Files;
31  import java.nio.file.OpenOption;
32  import java.nio.file.Path;
33  import java.nio.file.StandardOpenOption;
34  import java.text.SimpleDateFormat;
35  import java.time.LocalDateTime;
36  import java.time.ZoneId;
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.Collection;
40  import java.util.Collections;
41  import java.util.Date;
42  import java.util.EnumMap;
43  import java.util.HashMap;
44  import java.util.HashSet;
45  import java.util.Iterator;
46  import java.util.List;
47  import java.util.Map;
48  import java.util.NoSuchElementException;
49  import java.util.Set;
50  import java.util.TimeZone;
51  import java.util.TreeSet;
52  import java.util.regex.Pattern;
53  
54  import com.healthmarketscience.jackcess.ColumnBuilder;
55  import com.healthmarketscience.jackcess.Cursor;
56  import com.healthmarketscience.jackcess.CursorBuilder;
57  import com.healthmarketscience.jackcess.DataType;
58  import com.healthmarketscience.jackcess.Database;
59  import com.healthmarketscience.jackcess.DatabaseBuilder;
60  import com.healthmarketscience.jackcess.DateTimeType;
61  import com.healthmarketscience.jackcess.Index;
62  import com.healthmarketscience.jackcess.IndexBuilder;
63  import com.healthmarketscience.jackcess.IndexCursor;
64  import com.healthmarketscience.jackcess.PropertyMap;
65  import com.healthmarketscience.jackcess.Relationship;
66  import com.healthmarketscience.jackcess.Row;
67  import com.healthmarketscience.jackcess.RuntimeIOException;
68  import com.healthmarketscience.jackcess.Table;
69  import com.healthmarketscience.jackcess.TableBuilder;
70  import com.healthmarketscience.jackcess.TableDefinition;
71  import com.healthmarketscience.jackcess.TableMetaData;
72  import com.healthmarketscience.jackcess.expr.EvalConfig;
73  import com.healthmarketscience.jackcess.impl.query.QueryImpl;
74  import com.healthmarketscience.jackcess.query.Query;
75  import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher;
76  import com.healthmarketscience.jackcess.util.ColumnValidatorFactory;
77  import com.healthmarketscience.jackcess.util.ErrorHandler;
78  import com.healthmarketscience.jackcess.util.LinkResolver;
79  import com.healthmarketscience.jackcess.util.ReadOnlyFileChannel;
80  import com.healthmarketscience.jackcess.util.SimpleColumnValidatorFactory;
81  import com.healthmarketscience.jackcess.util.TableIterableBuilder;
82  import org.apache.commons.lang3.StringUtils;
83  import org.apache.commons.lang3.builder.ToStringBuilder;
84  import org.apache.commons.logging.Log;
85  import org.apache.commons.logging.LogFactory;
86  
87  
88  /**
89   *
90   * @author Tim McCune
91   * @usage _intermediate_class_
92   */
93  public class DatabaseImpl implements Database, DateTimeContext
94  {
95    private static final Log LOG = LogFactory.getLog(DatabaseImpl.class);
96  
97    /** this is the default "userId" used if we cannot find existing info.  this
98        seems to be some standard "Admin" userId for access files */
99    private static final byte[] SYS_DEFAULT_SID = new byte[] {
100     (byte) 0xA6, (byte) 0x33};
101 
102   /** the default value for the resource path used to load classpath
103    *  resources.
104    */
105   public static final String DEFAULT_RESOURCE_PATH =
106     "com/healthmarketscience/jackcess/";
107 
108   /** the resource path to be used when loading classpath resources */
109   static final String RESOURCE_PATH =
110     System.getProperty(RESOURCE_PATH_PROPERTY, DEFAULT_RESOURCE_PATH);
111 
112   /** whether or not this jvm has "broken" nio support */
113   static final boolean BROKEN_NIO = Boolean.TRUE.toString().equalsIgnoreCase(
114       System.getProperty(BROKEN_NIO_PROPERTY));
115 
116   /** additional internal details about each FileFormat */
117   private static final Map<Database.FileFormat,FileFormatDetails> FILE_FORMAT_DETAILS =
118     new EnumMap<Database.FileFormat,FileFormatDetails>(Database.FileFormat.class);
119 
120   static {
121     addFileFormatDetails(FileFormat.V1997, null, JetFormat.VERSION_3);
122     addFileFormatDetails(FileFormat.GENERIC_JET4, null, JetFormat.VERSION_4);
123     addFileFormatDetails(FileFormat.V2000, "empty", JetFormat.VERSION_4);
124     addFileFormatDetails(FileFormat.V2003, "empty2003", JetFormat.VERSION_4);
125     addFileFormatDetails(FileFormat.V2007, "empty2007", JetFormat.VERSION_12);
126     addFileFormatDetails(FileFormat.V2010, "empty2010", JetFormat.VERSION_14);
127     addFileFormatDetails(FileFormat.V2016, "empty2016", JetFormat.VERSION_16);
128     addFileFormatDetails(FileFormat.V2019, "empty2019", JetFormat.VERSION_17);
129     addFileFormatDetails(FileFormat.MSISAM, null, JetFormat.VERSION_MSISAM);
130   }
131 
132   /** System catalog always lives on page 2 */
133   private static final int PAGE_SYSTEM_CATALOG = 2;
134   /** Name of the system catalog */
135   private static final String TABLE_SYSTEM_CATALOG = "MSysObjects";
136 
137   /** this is the access control bit field for created tables.  the value used
138       is equivalent to full access (Visual Basic DAO PermissionEnum constant:
139       dbSecFullAccess) */
140   private static final Integer SYS_FULL_ACCESS_ACM = 1048575;
141 
142   /** ACE table column name of the actual access control entry */
143   private static final String ACE_COL_ACM = "ACM";
144   /** ACE table column name of the inheritable attributes flag */
145   private static final String ACE_COL_F_INHERITABLE = "FInheritable";
146   /** ACE table column name of the relevant objectId */
147   private static final String ACE_COL_OBJECT_ID = "ObjectId";
148   /** ACE table column name of the relevant userId */
149   private static final String ACE_COL_SID = "SID";
150 
151   /** Relationship table column name of the column count */
152   private static final String REL_COL_COLUMN_COUNT = "ccolumn";
153   /** Relationship table column name of the flags */
154   private static final String REL_COL_FLAGS = "grbit";
155   /** Relationship table column name of the index of the columns */
156   private static final String REL_COL_COLUMN_INDEX = "icolumn";
157   /** Relationship table column name of the "to" column name */
158   private static final String REL_COL_TO_COLUMN = "szColumn";
159   /** Relationship table column name of the "to" table name */
160   private static final String REL_COL_TO_TABLE = "szObject";
161   /** Relationship table column name of the "from" column name */
162   private static final String REL_COL_FROM_COLUMN = "szReferencedColumn";
163   /** Relationship table column name of the "from" table name */
164   private static final String REL_COL_FROM_TABLE = "szReferencedObject";
165   /** Relationship table column name of the relationship */
166   private static final String REL_COL_NAME = "szRelationship";
167 
168   /** System catalog column name of the page on which system object definitions
169       are stored */
170   private static final String CAT_COL_ID = "Id";
171   /** System catalog column name of the name of a system object */
172   private static final String CAT_COL_NAME = "Name";
173   private static final String CAT_COL_OWNER = "Owner";
174   /** System catalog column name of a system object's parent's id */
175   private static final String CAT_COL_PARENT_ID = "ParentId";
176   /** System catalog column name of the type of a system object */
177   private static final String CAT_COL_TYPE = "Type";
178   /** System catalog column name of the date a system object was created */
179   private static final String CAT_COL_DATE_CREATE = "DateCreate";
180   /** System catalog column name of the date a system object was updated */
181   private static final String CAT_COL_DATE_UPDATE = "DateUpdate";
182   /** System catalog column name of the flags column */
183   private static final String CAT_COL_FLAGS = "Flags";
184   /** System catalog column name of the properties column */
185   static final String CAT_COL_PROPS = "LvProp";
186   /** System catalog column name of the remote database */
187   private static final String CAT_COL_DATABASE = "Database";
188   /** System catalog column name of the remote table name */
189   private static final String CAT_COL_FOREIGN_NAME = "ForeignName";
190   /** System catalog column name of the remote connection name */
191   private static final String CAT_COL_CONNECT_NAME = "Connect";
192 
193   /** top-level parentid for a database */
194   private static final int DB_PARENT_ID = 0xF000000;
195 
196   /** the maximum size of any of the included "empty db" resources */
197   private static final long MAX_EMPTYDB_SIZE = 440000L;
198 
199   /** this object is a "system" object */
200   static final int SYSTEM_OBJECT_FLAG = 0x80000000;
201   /** this object is another type of "system" object */
202   static final int ALT_SYSTEM_OBJECT_FLAG = 0x02;
203   /** this object is hidden */
204   public static final int HIDDEN_OBJECT_FLAG = 0x08;
205   /** all flags which seem to indicate some type of system object */
206   static final int SYSTEM_OBJECT_FLAGS =
207     SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG;
208 
209   /** read-only channel access mode */
210   public static final OpenOption[] RO_CHANNEL_OPTS =
211     {StandardOpenOption.READ};
212   /** read/write channel access mode for existing files */
213   public static final OpenOption[] RW_CHANNEL_OPTS =
214     {StandardOpenOption.READ, StandardOpenOption.WRITE};
215   /** read/write/create channel access mode for new files */
216   public static final OpenOption[] RWC_CHANNEL_OPTS =
217   {StandardOpenOption.READ, StandardOpenOption.WRITE,
218    StandardOpenOption.CREATE};
219 
220   /** Name of the system object that is the parent of all tables */
221   private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables";
222   /** Name of the system object that is the parent of all databases */
223   private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases";
224   /** Name of the system object that is the parent of all relationships */
225   private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = "Relationships";
226   /** Name of the table that contains system access control entries */
227   private static final String TABLE_SYSTEM_ACES = "MSysACEs";
228   /** Name of the table that contains table relationships */
229   private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships";
230   /** Name of the table that contains queries */
231   private static final String TABLE_SYSTEM_QUERIES = "MSysQueries";
232   /** Name of the table that contains complex type information */
233   private static final String TABLE_SYSTEM_COMPLEX_COLS = "MSysComplexColumns";
234   /** Name of the main database properties object */
235   private static final String OBJECT_NAME_DB_PROPS = "MSysDb";
236   /** Name of the summary properties object */
237   private static final String OBJECT_NAME_SUMMARY_PROPS = "SummaryInfo";
238   /** Name of the user-defined properties object */
239   private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined";
240   /** System object type for table definitions */
241   static final Short TYPE_TABLE = 1;
242   /** System object type for linked odbc tables */
243   private static final Short TYPE_LINKED_ODBC_TABLE = 4;
244   /** System object type for query definitions */
245   private static final Short TYPE_QUERY = 5;
246   /** System object type for linked table definitions */
247   private static final Short TYPE_LINKED_TABLE = 6;
248   /** System object type for relationships */
249   private static final Short TYPE_RELATIONSHIP = 8;
250 
251   /** max number of table lookups to cache */
252   private static final int MAX_CACHED_LOOKUP_TABLES = 50;
253 
254   /** the columns to read when reading system catalog normally */
255   private static Collection<String> SYSTEM_CATALOG_COLUMNS =
256     new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
257                                       CAT_COL_FLAGS, CAT_COL_PARENT_ID));
258   /** the columns to read when finding table details */
259   private static Collection<String> SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS =
260     new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
261                                       CAT_COL_FLAGS, CAT_COL_PARENT_ID,
262                                       CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME,
263                                       CAT_COL_CONNECT_NAME));
264   /** the columns to read when getting object propertyes */
265   private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS =
266     new HashSet<String>(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS));
267   /** the columns to read when grabbing dates */
268   private static Collection<String> SYSTEM_CATALOG_DATE_COLUMNS =
269     new HashSet<String>(Arrays.asList(CAT_COL_ID,
270                                       CAT_COL_DATE_CREATE, CAT_COL_DATE_UPDATE));
271 
272   /** regex matching characters which are invalid in identifier names */
273   private static final Pattern INVALID_IDENTIFIER_CHARS =
274     Pattern.compile("[\\p{Cntrl}.!`\\]\\[]");
275 
276   /** regex to match a password in an ODBC string */
277   private static final Pattern ODBC_PWD_PATTERN = Pattern.compile("\\bPWD=[^;]+");
278 
279   /** the File of the database */
280   private final Path _file;
281   /** the simple name of the database */
282   private final String _name;
283   /** whether or not this db is read-only */
284   private final boolean _readOnly;
285   /** Buffer to hold database pages */
286   private ByteBuffer _buffer;
287   /** ID of the Tables system object */
288   private Integer _tableParentId;
289   /** Format that the containing database is in */
290   private final JetFormat _format;
291   /**
292    * Cache map of UPPERCASE table names to page numbers containing their
293    * definition and their stored table name (max size
294    * MAX_CACHED_LOOKUP_TABLES).
295    */
296   private final Map<String, TableInfo> _tableLookup =
297     new SimpleCache<String,TableInfo>(MAX_CACHED_LOOKUP_TABLES);
298   /** set of table names as stored in the mdb file, created on demand */
299   private Set<String> _tableNames;
300   /** Reads and writes database pages */
301   private final PageChannel _pageChannel;
302   /** System catalog table */
303   private TableImpl _systemCatalog;
304   /** utility table finder */
305   private TableFinder _tableFinder;
306   /** System access control entries table (initialized on first use) */
307   private TableImpl _accessControlEntries;
308   /** ID of the Relationships system object */
309   private Integer _relParentId;
310   /** SIDs to use for the ACEs added for new relationships */
311   private final List<byte[]> _newRelSIDs = new ArrayList<byte[]>();
312   /** System relationships table (initialized on first use) */
313   private TableImpl _relationships;
314   /** System queries table (initialized on first use) */
315   private TableImpl _queries;
316   /** System complex columns table (initialized on first use) */
317   private TableImpl _complexCols;
318   /** SIDs to use for the ACEs added for new tables */
319   private final List<byte[]> _newTableSIDs = new ArrayList<byte[]>();
320   /** optional error handler to use when row errors are encountered */
321   private ErrorHandler _dbErrorHandler;
322   /** the file format of the database */
323   private FileFormat _fileFormat;
324   /** charset to use when handling text */
325   private Charset _charset;
326   /** timezone to use when handling dates */
327   private TimeZone _timeZone;
328   /** zoneId to use when handling dates */
329   private ZoneId _zoneId;
330   /** language sort order to be used for textual columns */
331   private ColumnImpl.SortOrder _defaultSortOrder;
332   /** default code page to be used for textual columns (in some dbs) */
333   private Short _defaultCodePage;
334   /** the ordering used for table columns */
335   private Table.ColumnOrder _columnOrder;
336   /** whether or not enforcement of foreign-keys is enabled */
337   private boolean _enforceForeignKeys;
338   /** whether or not auto numbers can be directly inserted by the user */
339   private boolean _allowAutoNumInsert;
340   /** whether or not to evaluate expressions */
341   private boolean _evaluateExpressions;
342   /** factory for ColumnValidators */
343   private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE;
344   /** cache of in-use tables (or table definitions) */
345   private final TableCache _tableCache = new TableCache();
346   /** handler for reading/writing properteies */
347   private PropertyMaps.Handler _propsHandler;
348   /** ID of the Databases system object */
349   private Integer _dbParentId;
350   /** owner of objects we create */
351   private byte[] _newObjOwner;
352   /** core database properties */
353   private PropertyMaps _dbPropMaps;
354   /** summary properties */
355   private PropertyMaps _summaryPropMaps;
356   /** user-defined properties */
357   private PropertyMaps _userDefPropMaps;
358   /** linked table resolver */
359   private LinkResolver _linkResolver;
360   /** any linked databases which have been opened */
361   private Map<String,Database> _linkedDbs;
362   /** shared state used when enforcing foreign keys */
363   private final FKEnforcer.SharedState _fkEnforcerSharedState =
364     FKEnforcer.initSharedState();
365   /** shared context for evaluating expressions */
366   private DBEvalContext _evalCtx;
367   /** factory for the appropriate date/time type */
368   private ColumnImpl.DateTimeFactory _dtf;
369 
370   /**
371    * Open an existing Database.  If the existing file is not writeable or the
372    * readOnly flag is {@code true}, the file will be opened read-only.
373    * @param mdbFile File containing the database
374    * @param readOnly iff {@code true}, force opening file in read-only
375    *                 mode
376    * @param channel  pre-opened FileChannel.  if provided explicitly, it will
377    *                 not be closed by this Database instance
378    * @param autoSync whether or not to enable auto-syncing on write.  if
379    *                 {@code true}, writes will be immediately flushed to disk.
380    *                 This leaves the database in a (fairly) consistent state
381    *                 on each write, but can be very inefficient for many
382    *                 updates.  if {@code false}, flushing to disk happens at
383    *                 the jvm's leisure, which can be much faster, but may
384    *                 leave the database in an inconsistent state if failures
385    *                 are encountered during writing.  Writes may be flushed at
386    *                 any time using {@link #flush}.
387    * @param charset  Charset to use, if {@code null}, uses default
388    * @param timeZone TimeZone to use, if {@code null}, uses default
389    * @param provider CodecProvider for handling page encoding/decoding, may be
390    *                 {@code null} if no special encoding is necessary
391    * @usage _advanced_method_
392    */
393   public static DatabaseImpl open(
394       Path mdbFile, boolean readOnly, FileChannel channel,
395       boolean autoSync, Charset charset, TimeZone timeZone,
396       CodecProvider provider, boolean ignoreSystemCatalogIndex)
397     throws IOException
398   {
399     boolean closeChannel = false;
400     if(channel == null) {
401       if(!Files.isReadable(mdbFile)) {
402         throw new FileNotFoundException("given file does not exist: " +
403                                         mdbFile);
404       }
405 
406       // force read-only for non-writable files
407       readOnly |= !Files.isWritable(mdbFile);
408 
409       // open file channel
410       channel = openChannel(mdbFile, readOnly, false);
411       closeChannel = true;
412     }
413 
414     boolean success = false;
415     try {
416 
417       boolean wrapChannelRO = false;
418       if(!readOnly) {
419         // verify that format supports writing
420         JetFormat jetFormat = JetFormat.getFormat(channel);
421 
422         if(jetFormat.READ_ONLY) {
423           // force read-only mode
424           wrapChannelRO = true;
425           readOnly = true;
426         }
427       } else if(!closeChannel) {
428         // we are in read-only mode but the channel was opened externally, so
429         // we don't know if it is enforcing read-only status.  wrap it just to
430         // be safe
431         wrapChannelRO = true;
432       }
433 
434       if(wrapChannelRO) {
435         // wrap the channel with a read-only version to enforce
436         // non-writability
437         channel = new ReadOnlyFileChannel(channel);
438       }
439 
440       DatabaseImplss/impl/DatabaseImpl.html#DatabaseImpl">DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync,
441                                          null, charset, timeZone, provider,
442                                          readOnly, ignoreSystemCatalogIndex);
443       success = true;
444       return db;
445 
446     } finally {
447       if(!success && closeChannel) {
448         // something blew up, shutdown the channel (quietly)
449         ByteUtil.closeQuietly(channel);
450       }
451     }
452   }
453 
454   /**
455    * Create a new Database for the given fileFormat
456    * @param fileFormat version of new database.
457    * @param mdbFile Location to write the new database to.  <b>If this file
458    *                already exists, it will be overwritten.</b>
459    * @param channel  pre-opened FileChannel.  if provided explicitly, it will
460    *                 not be closed by this Database instance
461    * @param autoSync whether or not to enable auto-syncing on write.  if
462    *                 {@code true}, writes will be immediately flushed to disk.
463    *                 This leaves the database in a (fairly) consistent state
464    *                 on each write, but can be very inefficient for many
465    *                 updates.  if {@code false}, flushing to disk happens at
466    *                 the jvm's leisure, which can be much faster, but may
467    *                 leave the database in an inconsistent state if failures
468    *                 are encountered during writing.  Writes may be flushed at
469    *                 any time using {@link #flush}.
470    * @param charset  Charset to use, if {@code null}, uses default
471    * @param timeZone TimeZone to use, if {@code null}, uses default
472    * @usage _advanced_method_
473    */
474   public static DatabaseImpl create(FileFormat fileFormat, Path mdbFile,
475                                     FileChannel channel, boolean autoSync,
476                                     Charset charset, TimeZone timeZone)
477     throws IOException
478   {
479     FileFormatDetails details = getFileFormatDetails(fileFormat);
480     if (details.getFormat().READ_ONLY) {
481       throw new IOException("File format " + fileFormat +
482                             " does not support writing for " + mdbFile);
483     }
484     if(details.getEmptyFilePath() == null) {
485       throw new IOException("File format " + fileFormat +
486                             " does not support file creation for " + mdbFile);
487     }
488 
489     boolean closeChannel = false;
490     if(channel == null) {
491       channel = openChannel(mdbFile, false, true);
492       closeChannel = true;
493     }
494 
495     boolean success = false;
496     try {
497       channel.truncate(0);
498       transferDbFrom(channel, getResourceAsStream(details.getEmptyFilePath()));
499       channel.force(true);
500       DatabaseImplss/impl/DatabaseImpl.html#DatabaseImpl">DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync,
501                                          fileFormat, charset, timeZone, null,
502                                          false, false);
503       success = true;
504       return db;
505     } finally {
506       if(!success && closeChannel) {
507         // something blew up, shutdown the channel (quietly)
508         ByteUtil.closeQuietly(channel);
509       }
510     }
511   }
512 
513   /**
514    * Package visible only to support unit tests via DatabaseTest.openChannel().
515    * @param mdbFile file to open
516    * @param readOnly true if read-only
517    * @return a FileChannel on the given file.
518    * @exception FileNotFoundException
519    *            if the mode is <tt>"r"</tt> but the given file object does
520    *            not denote an existing regular file, or if the mode begins
521    *            with <tt>"rw"</tt> but the given file object does not denote
522    *            an existing, writable regular file and a new regular file of
523    *            that name cannot be created, or if some other error occurs
524    *            while opening or creating the file
525    */
526   static FileChannel openChannel(
527       Path mdbFile, boolean readOnly, boolean create)
528     throws IOException
529   {
530     OpenOption[] opts = (readOnly ? RO_CHANNEL_OPTS :
531                          (create ? RWC_CHANNEL_OPTS : RW_CHANNEL_OPTS));
532     return FileChannel.open(mdbFile, opts);
533   }
534 
535   /**
536    * Create a new database by reading it in from a FileChannel.
537    * @param file the File to which the channel is connected
538    * @param channel File channel of the database.  This needs to be a
539    *    FileChannel instead of a ReadableByteChannel because we need to
540    *    randomly jump around to various points in the file.
541    * @param autoSync whether or not to enable auto-syncing on write.  if
542    *                 {@code true}, writes will be immediately flushed to disk.
543    *                 This leaves the database in a (fairly) consistent state
544    *                 on each write, but can be very inefficient for many
545    *                 updates.  if {@code false}, flushing to disk happens at
546    *                 the jvm's leisure, which can be much faster, but may
547    *                 leave the database in an inconsistent state if failures
548    *                 are encountered during writing.  Writes may be flushed at
549    *                 any time using {@link #flush}.
550    * @param fileFormat version of new database (if known)
551    * @param charset Charset to use, if {@code null}, uses default
552    * @param timeZone TimeZone to use, if {@code null}, uses default
553    */
554   protected DatabaseImpl(Path file, FileChannel channel, boolean closeChannel,
555                          boolean autoSync, FileFormat fileFormat, Charset charset,
556                          TimeZone timeZone, CodecProvider provider,
557                          boolean readOnly, boolean ignoreSystemCatalogIndex)
558     throws IOException
559   {
560     _file = file;
561     _name = getName(file);
562     _readOnly = readOnly;
563     _format = JetFormat.getFormat(channel);
564     _charset = ((charset == null) ? getDefaultCharset(_format) : charset);
565     _columnOrder = getDefaultColumnOrder();
566     _enforceForeignKeys = getDefaultEnforceForeignKeys();
567     _allowAutoNumInsert = getDefaultAllowAutoNumberInsert();
568     _evaluateExpressions = getDefaultEvaluateExpressions();
569     _fileFormat = fileFormat;
570     setZoneInfo(timeZone, null);
571     _dtf = ColumnImpl.getDateTimeFactory(getDefaultDateTimeType());
572     _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync);
573     if(provider == null) {
574       provider = DefaultCodecProvider.INSTANCE;
575     }
576     // note, it's slighly sketchy to pass ourselves along partially
577     // constructed, but only our _format and _pageChannel refs should be
578     // needed
579     _pageChannel.initialize(this, provider);
580     _buffer = _pageChannel.createPageBuffer();
581     readSystemCatalog(ignoreSystemCatalogIndex);
582   }
583 
584   @Override
585   public File getFile() {
586     return ((_file != null) ? _file.toFile() : null);
587   }
588 
589   @Override
590   public Path getPath() {
591     return _file;
592   }
593 
594   public String getName() {
595     return _name;
596   }
597 
598   public boolean isReadOnly() {
599     return _readOnly;
600   }
601 
602   /**
603    * @usage _advanced_method_
604    */
605   public PageChannel getPageChannel() {
606     return _pageChannel;
607   }
608 
609   /**
610    * @usage _advanced_method_
611    */
612   public JetFormat getFormat() {
613     return _format;
614   }
615 
616   /**
617    * @return The system catalog table
618    * @usage _advanced_method_
619    */
620   public TableImpl getSystemCatalog() {
621     return _systemCatalog;
622   }
623 
624   /**
625    * @return The system Access Control Entries table (loaded on demand)
626    * @usage _advanced_method_
627    */
628   public TableImpl getAccessControlEntries() throws IOException {
629     if(_accessControlEntries == null) {
630       _accessControlEntries = getRequiredSystemTable(TABLE_SYSTEM_ACES);
631     }
632     return _accessControlEntries;
633   }
634 
635   /**
636    * @return the complex column system table (loaded on demand)
637    * @usage _advanced_method_
638    */
639   public TableImpl getSystemComplexColumns() throws IOException {
640     if(_complexCols == null) {
641       _complexCols = getRequiredSystemTable(TABLE_SYSTEM_COMPLEX_COLS);
642     }
643     return _complexCols;
644   }
645 
646   @Override
647   public ErrorHandler getErrorHandler() {
648     return((_dbErrorHandler != null) ? _dbErrorHandler : ErrorHandler.DEFAULT);
649   }
650 
651   @Override
652   public void setErrorHandler(ErrorHandler newErrorHandler) {
653     _dbErrorHandler = newErrorHandler;
654   }
655 
656   @Override
657   public LinkResolver getLinkResolver() {
658     return((_linkResolver != null) ? _linkResolver : LinkResolver.DEFAULT);
659   }
660 
661   @Override
662   public void setLinkResolver(LinkResolver newLinkResolver) {
663     _linkResolver = newLinkResolver;
664   }
665 
666   @Override
667   public Map<String,Database> getLinkedDatabases() {
668     return ((_linkedDbs == null) ? Collections.<String,Database>emptyMap() :
669             Collections.unmodifiableMap(_linkedDbs));
670   }
671 
672   @Override
673   public boolean isLinkedTable(Table table) throws IOException {
674 
675     if((table == null) || (this == table.getDatabase())) {
676       // if the table is null or this db owns the table, not linked
677       return false;
678     }
679 
680     // common case, local table name == remote table name
681     TableInfo tableInfo = lookupTable(table.getName());
682     if((tableInfo != null) &&
683        (tableInfo.getType() == TableMetaData.Type.LINKED) &&
684        matchesLinkedTable(table, tableInfo.getLinkedTableName(),
685                           tableInfo.getLinkedDbName())) {
686       return true;
687     }
688 
689     // but, the local table name may not match the remote table name, so we
690     // need to do a search if the common case fails
691     return _tableFinder.isLinkedTable(table);
692   }
693 
694   private boolean matchesLinkedTable(Table table, String linkedTableName,
695                                      String linkedDbName) {
696     return (table.getName().equalsIgnoreCase(linkedTableName) &&
697             (_linkedDbs != null) &&
698             (_linkedDbs.get(linkedDbName) == table.getDatabase()));
699   }
700 
701   @Override
702   public TimeZone getTimeZone() {
703     return _timeZone;
704   }
705 
706   @Override
707   public void setTimeZone(TimeZone newTimeZone) {
708     setZoneInfo(newTimeZone, null);
709   }
710 
711   @Override
712   public ZoneId getZoneId() {
713     return _zoneId;
714   }
715 
716   @Override
717   public void setZoneId(ZoneId newZoneId) {
718     setZoneInfo(null, newZoneId);
719   }
720 
721   private void setZoneInfo(TimeZone newTimeZone, ZoneId newZoneId) {
722     if(newTimeZone != null) {
723       newZoneId = newTimeZone.toZoneId();
724     } else if(newZoneId != null) {
725       newTimeZone = TimeZone.getTimeZone(newZoneId);
726     } else {
727       newTimeZone = getDefaultTimeZone();
728       newZoneId = newTimeZone.toZoneId();
729     }
730 
731     _timeZone = newTimeZone;
732     _zoneId = newZoneId;
733   }
734 
735   @Override
736   public DateTimeType getDateTimeType() {
737     return _dtf.getType();
738   }
739 
740   @Override
741   public void setDateTimeType(DateTimeType dateTimeType) {
742     _dtf = ColumnImpl.getDateTimeFactory(dateTimeType);
743   }
744 
745   @Override
746   public ColumnImpl.DateTimeFactory getDateTimeFactory() {
747     return _dtf;
748   }
749 
750   @Override
751   public Charset getCharset()
752   {
753     return _charset;
754   }
755 
756   @Override
757   public void setCharset(Charset newCharset) {
758     if(newCharset == null) {
759       newCharset = getDefaultCharset(getFormat());
760     }
761     _charset = newCharset;
762   }
763 
764   @Override
765   public Table.ColumnOrder getColumnOrder() {
766     return _columnOrder;
767   }
768 
769   @Override
770   public void setColumnOrder(Table.ColumnOrder newColumnOrder) {
771     if(newColumnOrder == null) {
772       newColumnOrder = getDefaultColumnOrder();
773     }
774     _columnOrder = newColumnOrder;
775   }
776 
777   @Override
778   public boolean isEnforceForeignKeys() {
779     return _enforceForeignKeys;
780   }
781 
782   @Override
783   public void setEnforceForeignKeys(Boolean newEnforceForeignKeys) {
784     if(newEnforceForeignKeys == null) {
785       newEnforceForeignKeys = getDefaultEnforceForeignKeys();
786     }
787     _enforceForeignKeys = newEnforceForeignKeys;
788   }
789 
790   @Override
791   public boolean isAllowAutoNumberInsert() {
792     return _allowAutoNumInsert;
793   }
794 
795   @Override
796   public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) {
797     if(allowAutoNumInsert == null) {
798       allowAutoNumInsert = getDefaultAllowAutoNumberInsert();
799     }
800     _allowAutoNumInsert = allowAutoNumInsert;
801   }
802 
803   @Override
804   public boolean isEvaluateExpressions() {
805     return _evaluateExpressions;
806   }
807 
808   @Override
809   public void setEvaluateExpressions(Boolean evaluateExpressions) {
810     if(evaluateExpressions == null) {
811       evaluateExpressions = getDefaultEvaluateExpressions();
812     }
813     _evaluateExpressions = evaluateExpressions;
814   }
815 
816   @Override
817   public ColumnValidatorFactory getColumnValidatorFactory() {
818     return _validatorFactory;
819   }
820 
821   @Override
822   public void setColumnValidatorFactory(ColumnValidatorFactory newFactory) {
823     if(newFactory == null) {
824       newFactory = SimpleColumnValidatorFactory.INSTANCE;
825     }
826     _validatorFactory = newFactory;
827   }
828 
829   /**
830    * @usage _advanced_method_
831    */
832   FKEnforcer.SharedState getFKEnforcerSharedState() {
833     return _fkEnforcerSharedState;
834   }
835 
836   @Override
837   public EvalConfig getEvalConfig() {
838     return getEvalContext();
839   }
840 
841   /**
842    * @usage _advanced_method_
843    */
844   DBEvalContext getEvalContext() {
845     if(_evalCtx == null) {
846       _evalCtx = new DBEvalContext(this);
847     }
848     return _evalCtx;
849   }
850 
851   /**
852    * Returns a SimpleDateFormat for the given format string which is
853    * configured with a compatible Calendar instance (see
854    * {@link DatabaseBuilder#toCompatibleCalendar}) and this database's
855    * {@link TimeZone}.
856    */
857   public SimpleDateFormat createDateFormat(String formatStr) {
858     SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
859     sdf.setTimeZone(getTimeZone());
860     return sdf;
861   }
862 
863   /**
864    * @return the current handler for reading/writing properties, creating if
865    * necessary
866    */
867   private PropertyMaps.Handler getPropsHandler() {
868     if(_propsHandler == null) {
869       _propsHandler = new PropertyMaps.Handler(this);
870     }
871     return _propsHandler;
872   }
873 
874   @Override
875   public FileFormat getFileFormat() throws IOException {
876 
877     if(_fileFormat == null) {
878 
879       Map<String,FileFormat> possibleFileFormats =
880         getFormat().getPossibleFileFormats();
881 
882       if(possibleFileFormats.size() == 1) {
883 
884         // single possible format (null key), easy enough
885         _fileFormat = possibleFileFormats.get(null);
886 
887       } else {
888 
889         // need to check the "AccessVersion" property
890         String accessVersion = (String)getDatabaseProperties().getValue(
891             PropertyMap.ACCESS_VERSION_PROP);
892 
893         if(isBlank(accessVersion)) {
894           // no access version, fall back to "generic"
895           accessVersion = null;
896         }
897 
898         _fileFormat = possibleFileFormats.get(accessVersion);
899 
900         if(_fileFormat == null) {
901           throw new IllegalStateException(withErrorContext(
902                   "Could not determine FileFormat"));
903         }
904       }
905     }
906     return _fileFormat;
907   }
908 
909   /**
910    * @return a (possibly cached) page ByteBuffer for internal use.  the
911    *         returned buffer should be released using
912    *         {@link #releaseSharedBuffer} when no longer in use
913    */
914   private ByteBuffer takeSharedBuffer() {
915     // we try to re-use a single shared _buffer, but occassionally, it may be
916     // needed by multiple operations at the same time (e.g. loading a
917     // secondary table while loading a primary table).  this method ensures
918     // that we don't corrupt the _buffer, but instead force the second caller
919     // to use a new buffer.
920     if(_buffer != null) {
921       ByteBuffer curBuffer = _buffer;
922       _buffer = null;
923       return curBuffer;
924     }
925     return _pageChannel.createPageBuffer();
926   }
927 
928   /**
929    * Relinquishes use of a page ByteBuffer returned by
930    * {@link #takeSharedBuffer}.
931    */
932   private void releaseSharedBuffer(ByteBuffer buffer) {
933     // we always stuff the returned buffer back into _buffer.  it doesn't
934     // really matter if multiple values over-write, at the end of the day, we
935     // just need one shared buffer
936     _buffer = buffer;
937   }
938 
939   /**
940    * @return the currently configured database default language sort order for
941    *         textual columns
942    * @usage _intermediate_method_
943    */
944   public ColumnImpl.SortOrder getDefaultSortOrder() throws IOException {
945 
946     if(_defaultSortOrder == null) {
947       initRootPageInfo();
948     }
949     return _defaultSortOrder;
950   }
951 
952   /**
953    * @return the currently configured database default code page for textual
954    *         data (may not be relevant to all database versions)
955    * @usage _intermediate_method_
956    */
957   public short getDefaultCodePage() throws IOException {
958 
959     if(_defaultCodePage == null) {
960       initRootPageInfo();
961     }
962     return _defaultCodePage;
963   }
964 
965   /**
966    * Reads various config info from the db page 0.
967    */
968   private void initRootPageInfo() throws IOException {
969     ByteBuffer buffer = takeSharedBuffer();
970     try {
971       _pageChannel.readRootPage(buffer);
972       _defaultSortOrder = ColumnImpl.readSortOrder(
973           buffer, _format.OFFSET_SORT_ORDER, _format);
974       _defaultCodePage = buffer.getShort(_format.OFFSET_CODE_PAGE);
975     } finally {
976       releaseSharedBuffer(buffer);
977     }
978   }
979 
980   /**
981    * @return a PropertyMaps instance decoded from the given bytes (always
982    *         returns non-{@code null} result).
983    * @usage _intermediate_method_
984    */
985   public PropertyMaps readProperties(byte[] propsBytes, int objectId,
986                                      RowIdImpl rowId)
987     throws IOException
988   {
989     return getPropsHandler().read(propsBytes, objectId, rowId, null);
990   }
991 
992   /**
993    * Read the system catalog
994    */
995   private void readSystemCatalog(boolean ignoreSystemCatalogIndex)
996     throws IOException {
997     _systemCatalog = loadTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
998                                SYSTEM_OBJECT_FLAGS, TYPE_TABLE);
999 
1000     if(!ignoreSystemCatalogIndex) {
1001       try {
1002         _tableFinder = new DefaultTableFinder(
1003             _systemCatalog.newCursor()
1004             .setIndexByColumnNames(CAT_COL_PARENT_ID, CAT_COL_NAME)
1005             .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
1006             .toIndexCursor());
1007       } catch(IllegalArgumentException e) {
1008         if(LOG.isDebugEnabled()) {
1009           LOG.debug(withErrorContext(
1010                         "Could not find expected index on table " +
1011                         _systemCatalog.getName()));
1012         }
1013         // use table scan instead
1014         _tableFinder = new FallbackTableFinder(
1015             _systemCatalog.newCursor()
1016             .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
1017             .toCursor());
1018       }
1019     } else {
1020       if(LOG.isDebugEnabled()) {
1021         LOG.debug(withErrorContext(
1022                       "Ignoring index on table " + _systemCatalog.getName()));
1023       }
1024       // use table scan instead
1025       _tableFinder = new FallbackTableFinder(
1026           _systemCatalog.newCursor()
1027           .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
1028           .toCursor());
1029     }
1030 
1031     _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID,
1032                                                SYSTEM_OBJECT_NAME_TABLES);
1033 
1034     if(_tableParentId == null) {
1035       throw new IOException(withErrorContext(
1036               "Did not find required parent table id"));
1037     }
1038 
1039     if (LOG.isDebugEnabled()) {
1040       LOG.debug(withErrorContext(
1041           "Finished reading system catalog.  Tables: " + getTableNames()));
1042     }
1043   }
1044 
1045   @Override
1046   public Set<String> getTableNames() throws IOException {
1047     if(_tableNames == null) {
1048       _tableNames = getTableNames(true, false, true);
1049     }
1050     return _tableNames;
1051   }
1052 
1053   @Override
1054   public Set<String> getSystemTableNames() throws IOException {
1055     return getTableNames(false, true, false);
1056   }
1057 
1058   private Set<String> getTableNames(boolean normalTables, boolean systemTables,
1059                                     boolean linkedTables)
1060     throws IOException
1061   {
1062     Set<String> tableNames = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
1063     _tableFinder.getTableNames(tableNames, normalTables, systemTables,
1064                                linkedTables);
1065     return tableNames;
1066   }
1067 
1068   @Override
1069   public Iterator<Table> iterator() {
1070     try {
1071       return new TableIterator(getTableNames());
1072     } catch(IOException e) {
1073       throw new RuntimeIOException(e);
1074     }
1075   }
1076 
1077   public Iterator<Table> iterator(TableIterableBuilder builder) {
1078     try {
1079       return new TableIterator(getTableNames(builder.isIncludeNormalTables(),
1080                                              builder.isIncludeSystemTables(),
1081                                              builder.isIncludeLinkedTables()));
1082     } catch(IOException e) {
1083       throw new RuntimeIOException(e);
1084     }
1085   }
1086 
1087   @Override
1088   public TableIterableBuilder newIterable() {
1089     return new TableIterableBuilder(this);
1090   }
1091 
1092   @Override
1093   public Iterable<TableMetaData> newTableMetaDataIterable() {
1094     return new Iterable<TableMetaData>() {
1095       @Override
1096       public Iterator<TableMetaData> iterator() {
1097         try {
1098           return _tableFinder.iterateTableMetaData();
1099         } catch(IOException e) {
1100           throw new RuntimeIOException(e);
1101         }
1102       }
1103     };
1104   }
1105 
1106   @Override
1107   public TableImpl getTable(String name) throws IOException {
1108     return getTable(name, false);
1109   }
1110 
1111   @Override
1112   public TableMetaData getTableMetaData(String name) throws IOException {
1113     return getTableInfo(name, true);
1114   }
1115 
1116   /**
1117    * @param tableDefPageNumber the page number of a table definition
1118    * @return The table, or null if it doesn't exist
1119    * @usage _advanced_method_
1120    */
1121   public TableImpl getTable(int tableDefPageNumber) throws IOException {
1122     return loadTable(null, tableDefPageNumber, 0, null);
1123   }
1124 
1125   /**
1126    * @param name Table name
1127    * @param includeSystemTables whether to consider returning a system table
1128    * @return The table, or null if it doesn't exist
1129    */
1130   protected TableImpl getTable(String name, boolean includeSystemTables)
1131     throws IOException
1132   {
1133     TableInfo tableInfo = getTableInfo(name, includeSystemTables);
1134     return ((tableInfo != null) ?
1135             getTable(tableInfo, includeSystemTables) : null);
1136   }
1137 
1138   private TableInfo getTableInfo(String name, boolean includeSystemTables)
1139     throws IOException
1140   {
1141     TableInfo tableInfo = lookupTable(name);
1142 
1143     if ((tableInfo == null) || (tableInfo.pageNumber == null)) {
1144       return null;
1145     }
1146     if(!includeSystemTables && tableInfo.isSystem()) {
1147       return null;
1148     }
1149 
1150     return tableInfo;
1151   }
1152 
1153   private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables)
1154     throws IOException
1155   {
1156     if(tableInfo.getType() == TableMetaData.Type.LINKED) {
1157 
1158       if(_linkedDbs == null) {
1159         _linkedDbs = new HashMap<String,Database>();
1160       }
1161 
1162       String linkedDbName = tableInfo.getLinkedDbName();
1163       String linkedTableName = tableInfo.getLinkedTableName();
1164       Database linkedDb = _linkedDbs.get(linkedDbName);
1165       if(linkedDb == null) {
1166         linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName);
1167         _linkedDbs.put(linkedDbName, linkedDb);
1168       }
1169 
1170       return ((DatabaseImpl)linkedDb).getTable(linkedTableName,
1171                                                includeSystemTables);
1172     }
1173 
1174     return loadTable(tableInfo.tableName, tableInfo.pageNumber,
1175                      tableInfo.flags, tableInfo.tableType);
1176   }
1177 
1178   /**
1179    * Create a new table in this database
1180    * @param name Name of the table to create
1181    * @param columns List of Columns in the table
1182    * @deprecated use {@link TableBuilder} instead
1183    */
1184   @Deprecated
1185   public void createTable(String name, List<ColumnBuilder> columns)
1186     throws IOException
1187   {
1188     createTable(name, columns, null);
1189   }
1190 
1191   /**
1192    * Create a new table in this database
1193    * @param name Name of the table to create
1194    * @param columns List of Columns in the table
1195    * @param indexes List of IndexBuilders describing indexes for the table
1196    * @deprecated use {@link TableBuilder} instead
1197    */
1198   @Deprecated
1199   public void createTable(String name, List<ColumnBuilder> columns,
1200                           List<IndexBuilder> indexes)
1201     throws IOException
1202   {
1203     new TableBuilder(name)
1204       .addColumns(columns)
1205       .addIndexes(indexes)
1206       .toTable(this);
1207   }
1208 
1209   @Override
1210   public void createLinkedTable(String name, String linkedDbName,
1211                                 String linkedTableName)
1212     throws IOException
1213   {
1214     if(lookupTable(name) != null) {
1215       throw new IllegalArgumentException(withErrorContext(
1216           "Cannot create linked table with name of existing table '" + name +
1217           "'"));
1218     }
1219 
1220     validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table");
1221     validateName(linkedDbName, DataType.MEMO.getMaxSize(),
1222                  "linked database");
1223     validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH,
1224                            "linked table");
1225 
1226     getPageChannel().startWrite();
1227     try {
1228 
1229       int linkedTableId = _tableFinder.getNextFreeSyntheticId();
1230 
1231       addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName,
1232                   linkedTableName);
1233 
1234     } finally {
1235       getPageChannel().finishWrite();
1236     }
1237   }
1238 
1239   /**
1240    * Adds a newly created table to the relevant internal database structures.
1241    */
1242   void addNewTable(String name, int tdefPageNumber, Short type,
1243                    String linkedDbName, String linkedTableName)
1244     throws IOException
1245   {
1246     //Add this table to our internal list.
1247     addTable(name, Integer.valueOf(tdefPageNumber), type, linkedDbName,
1248              linkedTableName);
1249 
1250     //Add this table to system tables
1251     addToSystemCatalog(name, tdefPageNumber, type, linkedDbName,
1252                        linkedTableName, _tableParentId);
1253     addToAccessControlEntries(tdefPageNumber, _tableParentId, _newTableSIDs);
1254   }
1255 
1256   @Override
1257   public List<Relationship> getRelationships(Tablef="../../../../com/healthmarketscience/jackcess/Table.html#Table">Table table1, Table table2)
1258     throws IOException
1259   {
1260     return getRelationships((TableImpl/../../../com/healthmarketscience/jackcess/impl/TableImpl.html#TableImpl">TableImpl)table1, (TableImpl)table2);
1261   }
1262 
1263   public List<Relationship> getRelationships(
1264       TableImpl./../../../com/healthmarketscience/jackcess/impl/TableImpl.html#TableImpl">TableImpl table1, TableImpl table2)
1265     throws IOException
1266   {
1267     int nameCmp = table1.getName().compareTo(table2.getName());
1268     if(nameCmp == 0) {
1269       throw new IllegalArgumentException(withErrorContext(
1270               "Must provide two different tables"));
1271     }
1272     if(nameCmp > 0) {
1273       // we "order" the two tables given so that we will return a collection
1274       // of relationships in the same order regardless of whether we are given
1275       // (TableFoo, TableBar) or (TableBar, TableFoo).
1276       TableImpl tmp = table1;
1277       table1 = table2;
1278       table2 = tmp;
1279     }
1280 
1281     return getRelationshipsImpl(table1, table2, true);
1282   }
1283 
1284   @Override
1285   public List<Relationship> getRelationships(Table table)
1286     throws IOException
1287   {
1288     if(table == null) {
1289       throw new IllegalArgumentException(withErrorContext("Must provide a table"));
1290     }
1291     // since we are getting relationships specific to certain table include
1292     // all tables
1293     return getRelationshipsImpl((TableImpl)table, null, true);
1294   }
1295 
1296   @Override
1297   public List<Relationship> getRelationships()
1298     throws IOException
1299   {
1300     return getRelationshipsImpl(null, null, false);
1301   }
1302 
1303   @Override
1304   public List<Relationship> getSystemRelationships()
1305     throws IOException
1306   {
1307     return getRelationshipsImpl(null, null, true);
1308   }
1309 
1310   private List<Relationship> getRelationshipsImpl(
1311       TableImpl./../../../com/healthmarketscience/jackcess/impl/TableImpl.html#TableImpl">TableImpl table1, TableImpl table2, boolean includeSystemTables)
1312     throws IOException
1313   {
1314     initRelationships();
1315 
1316     List<Relationship> relationships = new ArrayList<Relationship>();
1317 
1318     if(table1 != null) {
1319       Cursor cursor = createCursorWithOptionalIndex(
1320           _relationships, REL_COL_FROM_TABLE, table1.getName());
1321       collectRelationships(cursor, table1, table2, relationships,
1322                            includeSystemTables);
1323       cursor = createCursorWithOptionalIndex(
1324           _relationships, REL_COL_TO_TABLE, table1.getName());
1325       collectRelationships(cursor, table2, table1, relationships,
1326                            includeSystemTables);
1327     } else {
1328       collectRelationships(new CursorBuilder(_relationships).toCursor(),
1329                            null, null, relationships, includeSystemTables);
1330     }
1331 
1332     return relationships;
1333   }
1334 
1335   RelationshipImpl writeRelationship(RelationshipCreator creator)
1336     throws IOException
1337   {
1338     initRelationships();
1339 
1340     String name = createRelationshipName(creator);
1341     RelationshipImpl newRel = creator.createRelationshipImpl(name);
1342 
1343     ColumnImpl ccol = _relationships.getColumn(REL_COL_COLUMN_COUNT);
1344     ColumnImpl flagCol = _relationships.getColumn(REL_COL_FLAGS);
1345     ColumnImpl icol = _relationships.getColumn(REL_COL_COLUMN_INDEX);
1346     ColumnImpl nameCol = _relationships.getColumn(REL_COL_NAME);
1347     ColumnImpl fromTableCol = _relationships.getColumn(REL_COL_FROM_TABLE);
1348     ColumnImpl fromColCol = _relationships.getColumn(REL_COL_FROM_COLUMN);
1349     ColumnImpl toTableCol = _relationships.getColumn(REL_COL_TO_TABLE);
1350     ColumnImpl toColCol = _relationships.getColumn(REL_COL_TO_COLUMN);
1351 
1352     int numCols = newRel.getFromColumns().size();
1353     List<Object[]> rows = new ArrayList<Object[]>(numCols);
1354     for(int i = 0; i < numCols; ++i) {
1355       Object[] row = new Object[_relationships.getColumnCount()];
1356       ccol.setRowValue(row, numCols);
1357       flagCol.setRowValue(row, newRel.getFlags());
1358       icol.setRowValue(row, i);
1359       nameCol.setRowValue(row, name);
1360       fromTableCol.setRowValue(row, newRel.getFromTable().getName());
1361       fromColCol.setRowValue(row, newRel.getFromColumns().get(i).getName());
1362       toTableCol.setRowValue(row, newRel.getToTable().getName());
1363       toColCol.setRowValue(row, newRel.getToColumns().get(i).getName());
1364       rows.add(row);
1365     }
1366 
1367     getPageChannel().startWrite();
1368     try {
1369 
1370       int relObjId = _tableFinder.getNextFreeSyntheticId();
1371       _relationships.addRows(rows);
1372       addToSystemCatalog(name, relObjId, TYPE_RELATIONSHIP, null, null,
1373                          _relParentId);
1374       addToAccessControlEntries(relObjId, _relParentId, _newRelSIDs);
1375 
1376     } finally {
1377       getPageChannel().finishWrite();
1378     }
1379 
1380     return newRel;
1381   }
1382 
1383   private void initRelationships() throws IOException {
1384     // the relationships table does not get loaded until first accessed
1385     if(_relationships == null) {
1386       // need the parent id of the relationships objects
1387       _relParentId = _tableFinder.findObjectId(DB_PARENT_ID,
1388                                                SYSTEM_OBJECT_NAME_RELATIONSHIPS);
1389       _relationships = getRequiredSystemTable(TABLE_SYSTEM_RELATIONSHIPS);
1390     }
1391   }
1392 
1393   private String createRelationshipName(RelationshipCreator creator) {
1394     // ensure that the final identifier name does not get too long
1395     // - the primary name is limited to ((max / 2) - 3)
1396     // - the total name is limited to (max - 3)
1397     int maxIdLen = getFormat().MAX_INDEX_NAME_LENGTH;
1398     int limit = (maxIdLen / 2) - 3;
1399     String origName = creator.getName();
1400     if (origName == null) {
1401       origName = creator.getPrimaryTable().getName();
1402       if(origName.length() > limit) {
1403         origName = origName.substring(0, limit);
1404       }
1405       origName += creator.getSecondaryTable().getName();
1406     }
1407     limit = maxIdLen - 3;
1408     if(origName.length() > limit) {
1409       origName = origName.substring(0, limit);
1410     }
1411 
1412     // now ensure name is unique
1413     Set<String> names = new HashSet<String>();
1414 
1415     // collect the names of all relationships for uniqueness check
1416     for(Row row :
1417           CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames(
1418               SYSTEM_CATALOG_COLUMNS))
1419     {
1420       String name = row.getString(CAT_COL_NAME);
1421       if (name != null && TYPE_RELATIONSHIP.equals(row.get(CAT_COL_TYPE))) {
1422         names.add(toLookupName(name));
1423       }
1424     }
1425 
1426     if(creator.hasReferentialIntegrity()) {
1427       // relationship name will also be index name in secondary table, so must
1428       // check those names as well
1429       for(Index idx : creator.getSecondaryTable().getIndexes()) {
1430         names.add(toLookupName(idx.getName()));
1431       }
1432     }
1433 
1434     String baseName = toLookupName(origName);
1435     String name = baseName;
1436     int i = 0;
1437     while(names.contains(name)) {
1438       name = baseName + (++i);
1439     }
1440 
1441     return ((i == 0) ? origName : (origName + i));
1442   }
1443 
1444   @Override
1445   public List<Query> getQueries() throws IOException
1446   {
1447     // the queries table does not get loaded until first accessed
1448     if(_queries == null) {
1449       _queries = getRequiredSystemTable(TABLE_SYSTEM_QUERIES);
1450     }
1451 
1452     // find all the queries from the system catalog
1453     List<Row> queryInfo = new ArrayList<Row>();
1454     Map<Integer,List<QueryImpl.Row>> queryRowMap =
1455       new HashMap<Integer,List<QueryImpl.Row>>();
1456     for(Row row :
1457           CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames(
1458               SYSTEM_CATALOG_COLUMNS))
1459     {
1460       String name = row.getString(CAT_COL_NAME);
1461       if (name != null && TYPE_QUERY.equals(row.get(CAT_COL_TYPE))) {
1462         queryInfo.add(row);
1463         Integer id = row.getInt(CAT_COL_ID);
1464         queryRowMap.put(id, new ArrayList<QueryImpl.Row>());
1465       }
1466     }
1467 
1468     // find all the query rows
1469     for(Row row : CursorImpl.createCursor(_queries)) {
1470       QueryImpl.Row queryRow = new QueryImpl.Row(row);
1471       List<QueryImpl.Row> queryRows = queryRowMap.get(queryRow.objectId);
1472       if(queryRows == null) {
1473         LOG.warn(withErrorContext(
1474                      "Found rows for query with id " + queryRow.objectId +
1475                      " missing from system catalog"));
1476         continue;
1477       }
1478       queryRows.add(queryRow);
1479     }
1480 
1481     // lastly, generate all the queries
1482     List<Query> queries = new ArrayList<Query>();
1483     for(Row row : queryInfo) {
1484       String name = row.getString(CAT_COL_NAME);
1485       Integer id = row.getInt(CAT_COL_ID);
1486       int flags = row.getInt(CAT_COL_FLAGS);
1487       List<QueryImpl.Row> queryRows = queryRowMap.get(id);
1488       queries.add(QueryImpl.create(flags, name, queryRows, id));
1489     }
1490 
1491     return queries;
1492   }
1493 
1494   @Override
1495   public TableImpl getSystemTable(String tableName) throws IOException
1496   {
1497     return getTable(tableName, true);
1498   }
1499 
1500   private TableImpl getRequiredSystemTable(String tableName) throws IOException
1501   {
1502     TableImpl table = getSystemTable(tableName);
1503     if(table == null) {
1504       throw new IOException(withErrorContext(
1505               "Could not find system table " + tableName));
1506     }
1507     return table;
1508   }
1509 
1510   @Override
1511   public PropertyMap getDatabaseProperties() throws IOException {
1512     if(_dbPropMaps == null) {
1513       _dbPropMaps = getPropertiesForDbObject(OBJECT_NAME_DB_PROPS);
1514     }
1515     return _dbPropMaps.getDefault();
1516   }
1517 
1518   @Override
1519   public PropertyMap getSummaryProperties() throws IOException {
1520     if(_summaryPropMaps == null) {
1521       _summaryPropMaps = getPropertiesForDbObject(OBJECT_NAME_SUMMARY_PROPS);
1522     }
1523     return _summaryPropMaps.getDefault();
1524   }
1525 
1526   @Override
1527   public PropertyMap getUserDefinedProperties() throws IOException {
1528     if(_userDefPropMaps == null) {
1529       _userDefPropMaps = getPropertiesForDbObject(OBJECT_NAME_USERDEF_PROPS);
1530     }
1531     return _userDefPropMaps.getDefault();
1532   }
1533 
1534   /**
1535    * @return the PropertyMaps for the object with the given id
1536    * @usage _advanced_method_
1537    */
1538   public PropertyMaps getPropertiesForObject(
1539       int objectId, PropertyMaps.Owner owner)
1540     throws IOException
1541   {
1542     return readProperties(
1543         objectId, _tableFinder.getObjectRow(
1544             objectId, SYSTEM_CATALOG_PROPS_COLUMNS), owner);
1545   }
1546 
1547   LocalDateTime getCreateDateForObject(int objectId) throws IOException {
1548     return getDateForObject(objectId, CAT_COL_DATE_CREATE);
1549   }
1550 
1551   LocalDateTime getUpdateDateForObject(int objectId) throws IOException {
1552     return getDateForObject(objectId, CAT_COL_DATE_UPDATE);
1553   }
1554 
1555   private LocalDateTime getDateForObject(int objectId, String dateCol)
1556     throws IOException {
1557     Row row = _tableFinder.getObjectRow(objectId, SYSTEM_CATALOG_DATE_COLUMNS);
1558     if(row == null) {
1559       return null;
1560     }
1561     Object date = row.get(dateCol);
1562     return ((date != null) ? ColumnImpl.toLocalDateTime(date, this) : null);
1563   }
1564 
1565   private Integer getDbParentId() throws IOException {
1566     if(_dbParentId == null) {
1567       // need the parent id of the databases objects
1568       _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID,
1569                                               SYSTEM_OBJECT_NAME_DATABASES);
1570       if(_dbParentId == null) {
1571         throw new IOException(withErrorContext(
1572                 "Did not find required parent db id"));
1573       }
1574     }
1575     return _dbParentId;
1576   }
1577 
1578   private byte[] getNewObjectOwner() throws IOException {
1579     if(_newObjOwner == null) {
1580       // there doesn't seem to be any obvious way to find the main "owner" of
1581       // an access db, but certain db objects seem to have the common db
1582       // owner.  we attempt to grab the db properties object and use its
1583       // owner.
1584       Row msysDbRow = _tableFinder.getObjectRow(
1585           getDbParentId(), OBJECT_NAME_DB_PROPS,
1586           Collections.singleton(CAT_COL_OWNER));
1587       byte[] owner = null;
1588       if(msysDbRow != null) {
1589         owner = msysDbRow.getBytes(CAT_COL_OWNER);
1590       }
1591       _newObjOwner = (((owner != null) && (owner.length > 0)) ?
1592                       owner : SYS_DEFAULT_SID);
1593     }
1594     return _newObjOwner;
1595   }
1596 
1597   /**
1598    * @return property group for the given "database" object
1599    */
1600   private PropertyMaps getPropertiesForDbObject(String dbName)
1601     throws IOException
1602   {
1603     return readProperties(
1604         -1, _tableFinder.getObjectRow(
1605             getDbParentId(), dbName, SYSTEM_CATALOG_PROPS_COLUMNS), null);
1606   }
1607 
1608   private PropertyMaps readProperties(int objectId, Row objectRow,
1609                                       PropertyMaps.Owner owner)
1610     throws IOException
1611   {
1612     byte[] propsBytes = null;
1613     RowIdImpl rowId = null;
1614     if(objectRow != null) {
1615       propsBytes = objectRow.getBytes(CAT_COL_PROPS);
1616       objectId = objectRow.getInt(CAT_COL_ID);
1617       rowId = (RowIdImpl)objectRow.getId();
1618     }
1619     return getPropsHandler().read(propsBytes, objectId, rowId, owner);
1620   }
1621 
1622   @Override
1623   public String getDatabasePassword() throws IOException
1624   {
1625     ByteBuffer buffer = takeSharedBuffer();
1626     try {
1627       _pageChannel.readRootPage(buffer);
1628 
1629       byte[] pwdBytes = new byte[_format.SIZE_PASSWORD];
1630       buffer.position(_format.OFFSET_PASSWORD);
1631       buffer.get(pwdBytes);
1632 
1633       // de-mask password using extra password mask if necessary (the extra
1634       // password mask is generated from the database creation date stored in
1635       // the header)
1636       byte[] pwdMask = getPasswordMask(buffer, _format);
1637       if(pwdMask != null) {
1638         for(int i = 0; i < pwdBytes.length; ++i) {
1639           pwdBytes[i] ^= pwdMask[i % pwdMask.length];
1640         }
1641       }
1642 
1643       boolean hasPassword = false;
1644       for(int i = 0; i < pwdBytes.length; ++i) {
1645         if(pwdBytes[i] != 0) {
1646           hasPassword = true;
1647           break;
1648         }
1649       }
1650 
1651       if(!hasPassword) {
1652         return null;
1653       }
1654 
1655       String pwd = ColumnImpl.decodeUncompressedText(pwdBytes, getCharset());
1656 
1657       // remove any trailing null chars
1658       int idx = pwd.indexOf('\0');
1659       if(idx >= 0) {
1660         pwd = pwd.substring(0, idx);
1661       }
1662 
1663       return pwd;
1664     } finally {
1665       releaseSharedBuffer(buffer);
1666     }
1667   }
1668 
1669   /**
1670    * Finds the relationships matching the given from and to tables from the
1671    * given cursor and adds them to the given list.
1672    */
1673   private void collectRelationships(
1674       Cursor cursor, TableImpl./../../com/healthmarketscience/jackcess/impl/TableImpl.html#TableImpl">TableImpl fromTable, TableImpl toTable,
1675       List<Relationship> relationships, boolean includeSystemTables)
1676     throws IOException
1677   {
1678     String fromTableName = ((fromTable != null) ? fromTable.getName() : null);
1679     String toTableName = ((toTable != null) ? toTable.getName() : null);
1680 
1681     for(Row row : cursor) {
1682       String fromName = row.getString(REL_COL_FROM_TABLE);
1683       String toName = row.getString(REL_COL_TO_TABLE);
1684 
1685       if(((fromTableName == null) ||
1686           fromTableName.equalsIgnoreCase(fromName)) &&
1687          ((toTableName == null) ||
1688           toTableName.equalsIgnoreCase(toName))) {
1689 
1690         String relName = row.getString(REL_COL_NAME);
1691 
1692         // found more info for a relationship.  see if we already have some
1693         // info for this relationship
1694         Relationship rel = null;
1695         for(Relationship tmp : relationships) {
1696           if(tmp.getName().equalsIgnoreCase(relName)) {
1697             rel = tmp;
1698             break;
1699           }
1700         }
1701 
1702         TableImpl relFromTable = fromTable;
1703         if(relFromTable == null) {
1704           relFromTable = getTable(fromName, includeSystemTables);
1705           if(relFromTable == null) {
1706             // invalid table or ignoring system tables, just ignore
1707             continue;
1708           }
1709         }
1710         TableImpl relToTable = toTable;
1711         if(relToTable == null) {
1712           relToTable = getTable(toName, includeSystemTables);
1713           if(relToTable == null) {
1714             // invalid table or ignoring system tables, just ignore
1715             continue;
1716           }
1717         }
1718 
1719         if(rel == null) {
1720           // new relationship
1721           int numCols = row.getInt(REL_COL_COLUMN_COUNT);
1722           int flags = row.getInt(REL_COL_FLAGS);
1723           rel = new RelationshipImpl(relName, relFromTable, relToTable,
1724                                      flags, numCols);
1725           relationships.add(rel);
1726         }
1727 
1728         // add column info
1729         int colIdx = row.getInt(REL_COL_COLUMN_INDEX);
1730         ColumnImpl fromCol = relFromTable.getColumn(
1731             row.getString(REL_COL_FROM_COLUMN));
1732         ColumnImpl toCol = relToTable.getColumn(
1733             row.getString(REL_COL_TO_COLUMN));
1734 
1735         rel.getFromColumns().set(colIdx, fromCol);
1736         rel.getToColumns().set(colIdx, toCol);
1737       }
1738     }
1739   }
1740 
1741   /**
1742    * Add a new table to the system catalog
1743    * @param name Table name
1744    * @param objectId id of the new object
1745    */
1746   private void addToSystemCatalog(String name, int objectId, Short type,
1747                                   String linkedDbName, String linkedTableName,
1748                                   Integer parentId)
1749     throws IOException
1750   {
1751     byte[] owner = getNewObjectOwner();
1752     Object[] catalogRow = new Object[_systemCatalog.getColumnCount()];
1753     int idx = 0;
1754     Date creationTime = new Date();
1755     for (Iterator<ColumnImpl> iter = _systemCatalog.getColumns().iterator();
1756          iter.hasNext(); idx++)
1757     {
1758       ColumnImpl col = iter.next();
1759       if (CAT_COL_ID.equals(col.getName())) {
1760         catalogRow[idx] = Integer.valueOf(objectId);
1761       } else if (CAT_COL_NAME.equals(col.getName())) {
1762         catalogRow[idx] = name;
1763       } else if (CAT_COL_TYPE.equals(col.getName())) {
1764         catalogRow[idx] = type;
1765       } else if (CAT_COL_DATE_CREATE.equals(col.getName()) ||
1766                  CAT_COL_DATE_UPDATE.equals(col.getName())) {
1767         catalogRow[idx] = creationTime;
1768       } else if (CAT_COL_PARENT_ID.equals(col.getName())) {
1769         catalogRow[idx] = parentId;
1770       } else if (CAT_COL_FLAGS.equals(col.getName())) {
1771         catalogRow[idx] = Integer.valueOf(0);
1772       } else if (CAT_COL_OWNER.equals(col.getName())) {
1773         catalogRow[idx] = owner;
1774       } else if (CAT_COL_DATABASE.equals(col.getName())) {
1775         catalogRow[idx] = linkedDbName;
1776       } else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) {
1777         catalogRow[idx] = linkedTableName;
1778       }
1779     }
1780     _systemCatalog.addRow(catalogRow);
1781   }
1782 
1783   /**
1784    * Adds a new object to the system's access control entries
1785    */
1786   private void addToAccessControlEntries(
1787       Integer objectId, Integer parentId, List<byte[]> sids)
1788     throws IOException
1789   {
1790     if(sids.isEmpty()) {
1791       collectNewObjectSIDs(parentId, sids);
1792     }
1793 
1794     TableImpl acEntries = getAccessControlEntries();
1795     ColumnImpl acmCol = acEntries.getColumn(ACE_COL_ACM);
1796     ColumnImpl inheritCol = acEntries.getColumn(ACE_COL_F_INHERITABLE);
1797     ColumnImpl objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID);
1798     ColumnImpl sidCol = acEntries.getColumn(ACE_COL_SID);
1799 
1800     // construct a collection of ACE entries
1801     List<Object[]> aceRows = new ArrayList<Object[]>(sids.size());
1802     for(byte[] sid : sids) {
1803       Object[] aceRow = new Object[acEntries.getColumnCount()];
1804       acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM);
1805       inheritCol.setRowValue(aceRow, Boolean.FALSE);
1806       objIdCol.setRowValue(aceRow, objectId);
1807       sidCol.setRowValue(aceRow, sid);
1808       aceRows.add(aceRow);
1809     }
1810     acEntries.addRows(aceRows);
1811   }
1812 
1813   /**
1814    * Find collection of SIDs for the given parent id.
1815    */
1816   private void collectNewObjectSIDs(Integer parentId, List<byte[]> sids)
1817     throws IOException
1818   {
1819     // search for ACEs matching the given parentId.  use the index on the
1820     // objectId column if found (should be there)
1821     Cursor cursor = createCursorWithOptionalIndex(
1822         getAccessControlEntries(), ACE_COL_OBJECT_ID, parentId);
1823 
1824     for(Row row : cursor) {
1825       Integer objId = row.getInt(ACE_COL_OBJECT_ID);
1826       if(parentId.equals(objId)) {
1827         sids.add(row.getBytes(ACE_COL_SID));
1828       }
1829     }
1830 
1831     if(sids.isEmpty()) {
1832       // if all else fails, use the hard-coded default
1833       sids.add(SYS_DEFAULT_SID);
1834     }
1835   }
1836 
1837   /**
1838    * Reads a table with the given name from the given pageNumber.
1839    */
1840   private TableImpl loadTable(String name, int pageNumber, int flags, Short type)
1841     throws IOException
1842   {
1843     // first, check for existing table
1844     TableImpl table = _tableCache.get(pageNumber);
1845     if(table != null) {
1846       return table;
1847     }
1848 
1849     if(name == null) {
1850       // lookup table info from system catalog
1851       Row objectRow = _tableFinder.getObjectRow(
1852           pageNumber, SYSTEM_CATALOG_COLUMNS);
1853       if(objectRow == null) {
1854         return null;
1855       }
1856 
1857       name = objectRow.getString(CAT_COL_NAME);
1858       flags = objectRow.getInt(CAT_COL_FLAGS);
1859       type = objectRow.getShort(CAT_COL_TYPE);
1860     }
1861 
1862     // need to load table from db
1863     return _tableCache.put(readTable(name, pageNumber, flags, type));
1864   }
1865 
1866   /**
1867    * Reads a table with the given name from the given pageNumber.
1868    */
1869   private TableImpl readTable(
1870       String name, int pageNumber, int flags, Short type)
1871     throws IOException
1872   {
1873     ByteBuffer buffer = takeSharedBuffer();
1874     try {
1875       // need to load table from db
1876       _pageChannel.readPage(buffer, pageNumber);
1877       byte pageType = buffer.get(0);
1878       if (pageType != PageTypes.TABLE_DEF) {
1879         throw new IOException(withErrorContext(
1880             "Looking for " + name + " at page " + pageNumber +
1881             ", but page type is " + pageType));
1882       }
1883       return (!TYPE_LINKED_ODBC_TABLE.equals(type) ?
1884               new TableImpl(this, buffer, pageNumber, name, flags) :
1885               new TableDefinitionImpl(this, buffer, pageNumber, name, flags));
1886     } finally {
1887       releaseSharedBuffer(buffer);
1888     }
1889   }
1890 
1891   /**
1892    * Creates a Cursor restricted to the given column value if possible (using
1893    * an existing index), otherwise a simple table cursor.
1894    */
1895   private Cursor createCursorWithOptionalIndex(
1896       TableImpl table, String colName, Object colValue)
1897     throws IOException
1898   {
1899     try {
1900       return table.newCursor()
1901         .setIndexByColumnNames(colName)
1902         .setSpecificEntry(colValue)
1903         .toCursor();
1904     } catch(IllegalArgumentException e) {
1905       if(LOG.isDebugEnabled()) {
1906         LOG.debug(withErrorContext(
1907             "Could not find expected index on table " + table.getName()));
1908       }
1909     }
1910     // use table scan instead
1911     return CursorImpl.createCursor(table);
1912   }
1913 
1914   @Override
1915   public void flush() throws IOException {
1916     if(_linkedDbs != null) {
1917       for(Database linkedDb : _linkedDbs.values()) {
1918         linkedDb.flush();
1919       }
1920     }
1921     _pageChannel.flush();
1922   }
1923 
1924   @Override
1925   public void close() throws IOException {
1926     if(_linkedDbs != null) {
1927       for(Database linkedDb : _linkedDbs.values()) {
1928         linkedDb.close();
1929       }
1930     }
1931     _pageChannel.close();
1932   }
1933 
1934   public void validateNewTableName(String name) throws IOException {
1935     validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table");
1936 
1937     if(lookupTable(name) != null) {
1938       throw new IllegalArgumentException(withErrorContext(
1939               "Cannot create table with name of existing table '" + name + "'"));
1940     }
1941   }
1942 
1943   /**
1944    * Validates an identifier name.
1945    *
1946    * Names of fields, controls, and objects in Microsoft Access:
1947    * <ul>
1948    * <li>Can include any combination of letters, numbers, spaces, and special
1949    *     characters except a period (.), an exclamation point (!), an accent
1950    *     grave (`), and brackets ([ ]).</li>
1951    * <li>Can't begin with leading spaces.</li>
1952    * <li>Can't include control characters (ASCII values 0 through 31).</li>
1953    * </ul>
1954    *
1955    * @usage _advanced_method_
1956    */
1957   public static void validateIdentifierName(String name,
1958                                             int maxLength,
1959                                             String identifierType)
1960   {
1961     // basic name validation
1962     validateName(name, maxLength, identifierType);
1963 
1964     // additional identifier validation
1965     if(INVALID_IDENTIFIER_CHARS.matcher(name).find()) {
1966       throw new IllegalArgumentException(
1967           identifierType + " name '" + name + "' contains invalid characters");
1968     }
1969 
1970     // cannot start with spaces
1971     if(name.charAt(0) == ' ') {
1972       throw new IllegalArgumentException(
1973           identifierType + " name '" + name +
1974           "' cannot start with a space character");
1975     }
1976   }
1977 
1978   /**
1979    * Validates a name.
1980    */
1981   private static void validateName(String name, int maxLength, String nameType)
1982   {
1983     if(isBlank(name)) {
1984       throw new IllegalArgumentException(
1985           nameType + " must have non-blank name");
1986     }
1987     if(name.length() > maxLength) {
1988       throw new IllegalArgumentException(
1989           nameType + " name is longer than max length of " + maxLength +
1990           ": " + name);
1991     }
1992   }
1993 
1994   /**
1995    * Returns {@code true} if the given string is {@code null} or all blank
1996    * space, {@code false} otherwise.
1997    */
1998   public static boolean isBlank(String name) {
1999     return StringUtils.isBlank(name);
2000   }
2001 
2002   /**
2003    * Returns the given string trimmed, or {@code null} if the string is {@code
2004    * null} or empty.
2005    */
2006   public static String trimToNull(String str) {
2007     return StringUtils.trimToNull(str);
2008   }
2009 
2010   @Override
2011   public String toString() {
2012     return ToStringBuilder.reflectionToString(this);
2013   }
2014 
2015   /**
2016    * Adds a table to the _tableLookup and resets the _tableNames set
2017    */
2018   private void addTable(String tableName, Integer pageNumber, Short type,
2019                         String linkedDbName, String linkedTableName)
2020   {
2021     _tableLookup.put(toLookupName(tableName),
2022                      createTableInfo(tableName, pageNumber, 0, type,
2023                                      linkedDbName, linkedTableName, null));
2024     // clear this, will be created next time needed
2025     _tableNames = null;
2026   }
2027 
2028   private static TableInfo createTableInfo(
2029       String tableName, Short type, Row row) {
2030 
2031     Integer pageNumber = row.getInt(CAT_COL_ID);
2032     int flags = row.getInt(CAT_COL_FLAGS);
2033     String linkedDbName = row.getString(CAT_COL_DATABASE);
2034     String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
2035     String connectName = row.getString(CAT_COL_CONNECT_NAME);
2036 
2037     return createTableInfo(tableName, pageNumber, flags, type, linkedDbName,
2038                            linkedTableName, connectName);
2039   }
2040 
2041   /**
2042    * Creates a TableInfo instance appropriate for the given table data.
2043    */
2044   private static TableInfo createTableInfo(
2045       String tableName, Integer pageNumber, int flags, Short type,
2046       String linkedDbName, String linkedTableName, String connectName)
2047   {
2048     if(TYPE_LINKED_TABLE.equals(type)) {
2049       return new LinkedTableInfo(pageNumber, tableName, flags, type,
2050                                  linkedDbName, linkedTableName);
2051     } else if(TYPE_LINKED_ODBC_TABLE.equals(type)) {
2052       return new LinkedODBCTableInfo(pageNumber, tableName, flags, type,
2053                                      connectName, linkedTableName);
2054     }
2055     return new TableInfo(pageNumber, tableName, flags, type);
2056   }
2057 
2058   /**
2059    * @return the tableInfo of the given table, if any
2060    */
2061   private TableInfo lookupTable(String tableName) throws IOException {
2062 
2063     String lookupTableName = toLookupName(tableName);
2064     TableInfo tableInfo = _tableLookup.get(lookupTableName);
2065     if(tableInfo != null) {
2066       return tableInfo;
2067     }
2068 
2069     tableInfo = _tableFinder.lookupTable(tableName);
2070 
2071     if(tableInfo != null) {
2072       // cache for later
2073       _tableLookup.put(lookupTableName, tableInfo);
2074     }
2075 
2076     return tableInfo;
2077   }
2078 
2079   /**
2080    * @return a string usable in the _tableLookup map.
2081    */
2082   public static String toLookupName(String name) {
2083     return ((name != null) ? name.toUpperCase() : null);
2084   }
2085 
2086   /**
2087    * @return {@code true} if the given flags indicate that an object is some
2088    *         sort of system object, {@code false} otherwise.
2089    */
2090   private static boolean isSystemObject(int flags) {
2091     return ((flags & SYSTEM_OBJECT_FLAGS) != 0);
2092   }
2093 
2094   /**
2095    * Returns the default TimeZone.  This is normally the platform default
2096    * TimeZone as returned by {@link TimeZone#getDefault}, but can be
2097    * overridden using the system property
2098    * {@value com.healthmarketscience.jackcess.Database#TIMEZONE_PROPERTY}.
2099    * @usage _advanced_method_
2100    */
2101   public static TimeZone getDefaultTimeZone()
2102   {
2103     String tzProp = System.getProperty(TIMEZONE_PROPERTY);
2104     if(tzProp != null) {
2105       tzProp = tzProp.trim();
2106       if(tzProp.length() > 0) {
2107         return TimeZone.getTimeZone(tzProp);
2108       }
2109     }
2110 
2111     // use system default
2112     return TimeZone.getDefault();
2113   }
2114 
2115   /**
2116    * Returns the default Charset for the given JetFormat.  This may or may not
2117    * be platform specific, depending on the format, but can be overridden
2118    * using a system property composed of the prefix
2119    * {@value com.healthmarketscience.jackcess.Database#CHARSET_PROPERTY_PREFIX}
2120    * followed by the JetFormat version to which the charset should apply,
2121    * e.g. {@code "com.healthmarketscience.jackcess.charset.VERSION_3"}.
2122    * @usage _advanced_method_
2123    */
2124   public static Charset getDefaultCharset(JetFormat format)
2125   {
2126     String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format);
2127     if(csProp != null) {
2128       csProp = csProp.trim();
2129       if(csProp.length() > 0) {
2130         return Charset.forName(csProp);
2131       }
2132     }
2133 
2134     // use format default
2135     return format.CHARSET;
2136   }
2137 
2138   /**
2139    * Returns the default Table.ColumnOrder.  This defaults to
2140    * {@link Database#DEFAULT_COLUMN_ORDER}, but can be overridden using the system
2141    * property {@value com.healthmarketscience.jackcess.Database#COLUMN_ORDER_PROPERTY}.
2142    * @usage _advanced_method_
2143    */
2144   public static Table.ColumnOrder getDefaultColumnOrder()
2145   {
2146     return getEnumSystemProperty(Table.ColumnOrder.class, COLUMN_ORDER_PROPERTY,
2147                                  DEFAULT_COLUMN_ORDER);
2148   }
2149 
2150   /**
2151    * Returns the default enforce foreign-keys policy.  This defaults to
2152    * {@code true}, but can be overridden using the system
2153    * property {@value com.healthmarketscience.jackcess.Database#FK_ENFORCE_PROPERTY}.
2154    * @usage _advanced_method_
2155    */
2156   public static boolean getDefaultEnforceForeignKeys()
2157   {
2158     String prop = System.getProperty(FK_ENFORCE_PROPERTY);
2159     return ((prop == null) || Boolean.TRUE.toString().equalsIgnoreCase(prop));
2160   }
2161 
2162   /**
2163    * Returns the default allow auto number insert policy.  This defaults to
2164    * {@code false}, but can be overridden using the system
2165    * property {@value com.healthmarketscience.jackcess.Database#ALLOW_AUTONUM_INSERT_PROPERTY}.
2166    * @usage _advanced_method_
2167    */
2168   public static boolean getDefaultAllowAutoNumberInsert()
2169   {
2170     String prop = System.getProperty(ALLOW_AUTONUM_INSERT_PROPERTY);
2171     return ((prop != null) && Boolean.TRUE.toString().equalsIgnoreCase(prop));
2172   }
2173 
2174   /**
2175    * Returns the default enable expression evaluation policy.  This defaults to
2176    * {@code true}, but can be overridden using the system
2177    * property {@value com.healthmarketscience.jackcess.Database#ENABLE_EXPRESSION_EVALUATION_PROPERTY}.
2178    * @usage _advanced_method_
2179    */
2180   public static boolean getDefaultEvaluateExpressions()
2181   {
2182     String prop = System.getProperty(ENABLE_EXPRESSION_EVALUATION_PROPERTY);
2183     return ((prop == null) || Boolean.TRUE.toString().equalsIgnoreCase(prop));
2184   }
2185 
2186   /**
2187    * Returns the default DateTimeType.  This defaults to
2188    * {@link DateTimeType#LOCAL_DATE_TIME}, but can be overridden using the system
2189    * property {@value com.healthmarketscience.jackcess.Database#DATE_TIME_TYPE_PROPERTY}.
2190    * @usage _advanced_method_
2191    */
2192   public static DateTimeType getDefaultDateTimeType() {
2193     return getEnumSystemProperty(DateTimeType.class, DATE_TIME_TYPE_PROPERTY,
2194                                  DateTimeType.LOCAL_DATE_TIME);
2195   }
2196 
2197   /**
2198    * Copies the given db InputStream to the given channel using the most
2199    * efficient means possible.
2200    */
2201   protected static void transferDbFrom(FileChannel channel, InputStream in)
2202     throws IOException
2203   {
2204     ReadableByteChannel readChannel = Channels.newChannel(in);
2205     if(!BROKEN_NIO) {
2206       // sane implementation
2207       channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE);
2208     } else {
2209       // do things the hard way for broken vms
2210       ByteBuffer bb = ByteBuffer.allocate(8096);
2211       while(readChannel.read(bb) >= 0) {
2212         bb.flip();
2213         channel.write(bb);
2214         bb.clear();
2215       }
2216     }
2217   }
2218 
2219   /**
2220    * Returns the password mask retrieved from the given header page and
2221    * format, or {@code null} if this format does not use a password mask.
2222    * @usage _advanced_method_
2223    */
2224   public static byte[] getPasswordMask(ByteBuffer buffer, JetFormat format)
2225   {
2226     // get extra password mask if necessary (the extra password mask is
2227     // generated from the database creation date stored in the header)
2228     int pwdMaskPos = format.OFFSET_HEADER_DATE;
2229     if(pwdMaskPos < 0) {
2230       return null;
2231     }
2232 
2233     buffer.position(pwdMaskPos);
2234     double dateVal = Double.longBitsToDouble(buffer.getLong());
2235 
2236     byte[] pwdMask = new byte[4];
2237     PageChannel.wrap(pwdMask).putInt((int)dateVal);
2238 
2239     return pwdMask;
2240   }
2241 
2242   protected static InputStream getResourceAsStream(String resourceName)
2243     throws IOException
2244   {
2245     InputStream stream = DatabaseImpl.class.getClassLoader()
2246       .getResourceAsStream(resourceName);
2247 
2248     if(stream == null) {
2249 
2250       stream = Thread.currentThread().getContextClassLoader()
2251         .getResourceAsStream(resourceName);
2252 
2253       if(stream == null) {
2254         throw new IOException("Could not load jackcess resource " +
2255                               resourceName);
2256       }
2257     }
2258 
2259     return stream;
2260   }
2261 
2262   private static boolean isTableType(Short objType) {
2263     return(TYPE_TABLE.equals(objType) || isAnyLinkedTableType(objType));
2264   }
2265 
2266   public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) {
2267     return FILE_FORMAT_DETAILS.get(fileFormat);
2268   }
2269 
2270   private static void addFileFormatDetails(
2271       FileFormat fileFormat, String emptyFileName, JetFormat format)
2272   {
2273     String emptyFile =
2274       ((emptyFileName != null) ?
2275        RESOURCE_PATH + emptyFileName + fileFormat.getFileExtension() : null);
2276     FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format));
2277   }
2278 
2279   private static String getName(Path file) {
2280     if(file == null) {
2281       return "<UNKNOWN.DB>";
2282     }
2283     return file.getFileName().toString();
2284   }
2285 
2286   private String withErrorContext(String msg) {
2287     return withErrorContext(msg, getName());
2288   }
2289 
2290   private static String withErrorContext(String msg, String dbName) {
2291     return msg + " (Db=" + dbName + ")";
2292   }
2293 
2294   private static <E extends Enum<E>> E getEnumSystemProperty(
2295       Class<E> enumClass, String propName, E defaultValue)
2296   {
2297     String prop = System.getProperty(propName);
2298     if(prop != null) {
2299       prop = prop.trim().toUpperCase();
2300       if(!prop.isEmpty()) {
2301         return Enum.valueOf(enumClass, prop);
2302       }
2303     }
2304     return defaultValue;
2305   }
2306 
2307   private static boolean isAnyLinkedTableType(Short type) {
2308     return (TYPE_LINKED_TABLE.equals(type) ||
2309             TYPE_LINKED_ODBC_TABLE.equals(type));
2310   }
2311 
2312   /**
2313    * Utility class for storing table page number and actual name.
2314    */
2315   private static class TableInfo implements TableMetaData
2316   {
2317     public final Integer pageNumber;
2318     public final String tableName;
2319     public final int flags;
2320     public final Short tableType;
2321 
2322     private TableInfo(Integer newPageNumber, String newTableName, int newFlags,
2323                       Short newTableType) {
2324       pageNumber = newPageNumber;
2325       tableName = newTableName;
2326       flags = newFlags;
2327       tableType = newTableType;
2328     }
2329 
2330     @Override
2331     public Type getType() {
2332       return Type.LOCAL;
2333     }
2334 
2335     @Override
2336     public String getName() {
2337       return tableName;
2338     }
2339 
2340     @Override
2341     public boolean isLinked() {
2342       return false;
2343     }
2344 
2345     @Override
2346     public boolean isSystem() {
2347       return isSystemObject(flags);
2348     }
2349 
2350     @Override
2351     public String getLinkedTableName() {
2352       return null;
2353     }
2354 
2355     @Override
2356     public String getLinkedDbName() {
2357       return null;
2358     }
2359 
2360     @Override
2361     public String getConnectionName() {
2362       return null;
2363     }
2364 
2365     @Override
2366     public Table open(Database db) throws IOException {
2367       return ((DatabaseImpl)db).getTable(this, true);
2368     }
2369 
2370     @Override
2371     public TableDefinition getTableDefinition(Database db) throws IOException {
2372       return null;
2373     }
2374 
2375     @Override
2376     public String toString() {
2377       ToStringBuilder sb = CustomToStringStyle.valueBuilder("TableMetaData")
2378         .append("name", getName());
2379         if(isSystem()) {
2380           sb.append("isSystem", isSystem());
2381         }
2382         if(isLinked()) {
2383           sb.append("isLinked", isLinked())
2384             .append("linkedTableName", getLinkedTableName())
2385             .append("linkedDbName", getLinkedDbName())
2386             .append("connectionName", maskPassword(getConnectionName()));
2387         }
2388         return sb.toString();
2389     }
2390 
2391     private static String maskPassword(String connectionName) {
2392       return ((connectionName != null) ?
2393               ODBC_PWD_PATTERN.matcher(connectionName).replaceAll("PWD=XXXXXX") :
2394               null);
2395     }
2396   }
2397 
2398   /**
2399    * Utility class for storing linked table info
2400    */
2401   private static class LinkedTableInfo extends TableInfo
2402   {
2403     private final String _linkedDbName;
2404     private final String _linkedTableName;
2405 
2406     private LinkedTableInfo(Integer newPageNumber, String newTableName,
2407                             int newFlags, Short newTableType,
2408                             String newLinkedDbName,
2409                             String newLinkedTableName) {
2410       super(newPageNumber, newTableName, newFlags, newTableType);
2411       _linkedDbName = newLinkedDbName;
2412       _linkedTableName = newLinkedTableName;
2413     }
2414 
2415     @Override
2416     public Type getType() {
2417       return Type.LINKED;
2418     }
2419 
2420     @Override
2421     public boolean isLinked() {
2422       return true;
2423     }
2424 
2425     @Override
2426     public String getLinkedTableName() {
2427       return _linkedTableName;
2428     }
2429 
2430     @Override
2431     public String getLinkedDbName() {
2432       return _linkedDbName;
2433     }
2434   }
2435 
2436   /**
2437    * Utility class for storing linked ODBC table info
2438    */
2439   private static class LinkedODBCTableInfo extends TableInfo
2440   {
2441     private final String _linkedTableName;
2442     private final String _connectionName;
2443 
2444     private LinkedODBCTableInfo(Integer newPageNumber, String newTableName,
2445                                 int newFlags, Short newTableType,
2446                                 String connectName,
2447                                 String newLinkedTableName) {
2448       super(newPageNumber, newTableName, newFlags, newTableType);
2449       _linkedTableName = newLinkedTableName;
2450       _connectionName = connectName;
2451     }
2452 
2453     @Override
2454     public Type getType() {
2455       return Type.LINKED_ODBC;
2456     }
2457 
2458     @Override
2459     public boolean isLinked() {
2460       return true;
2461     }
2462 
2463     @Override
2464     public String getLinkedTableName() {
2465       return _linkedTableName;
2466     }
2467 
2468     @Override
2469     public String getConnectionName() {
2470       return _connectionName;
2471     }
2472 
2473     @Override
2474     public Table open(Database db) {
2475       return null;
2476     }
2477 
2478     @Override
2479     public TableDefinition getTableDefinition(Database db) throws IOException {
2480       return (((pageNumber != null) && (pageNumber > 0)) ?
2481               ((DatabaseImpl)db).getTable(this, true) :
2482               null);
2483     }
2484   }
2485 
2486   /**
2487    * Table iterator for this database, unmodifiable.
2488    */
2489   private class TableIterator implements Iterator<Table>
2490   {
2491     private final Iterator<String> _tableNameIter;
2492 
2493     private TableIterator(Set<String> tableNames) {
2494       _tableNameIter = tableNames.iterator();
2495     }
2496 
2497     @Override
2498     public boolean hasNext() {
2499       return _tableNameIter.hasNext();
2500     }
2501 
2502     @Override
2503     public Table next() {
2504       if(!hasNext()) {
2505         throw new NoSuchElementException();
2506       }
2507       try {
2508         return getTable(_tableNameIter.next(), true);
2509       } catch(IOException e) {
2510         throw new RuntimeIOException(e);
2511       }
2512     }
2513   }
2514 
2515   /**
2516    * Utility class for handling table lookups.
2517    */
2518   private abstract class TableFinder
2519   {
2520     public Integer findObjectId(Integer parentId, String name)
2521       throws IOException
2522     {
2523       Cursor cur = findRow(parentId, name);
2524       if(cur == null) {
2525         return null;
2526       }
2527       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2528       return (Integer)cur.getCurrentRowValue(idCol);
2529     }
2530 
2531     public Row getObjectRow(Integer parentId, String name,
2532                             Collection<String> columns)
2533       throws IOException
2534     {
2535       Cursor cur = findRow(parentId, name);
2536       return ((cur != null) ? cur.getCurrentRow(columns) : null);
2537     }
2538 
2539     public Row getObjectRow(
2540         Integer objectId, Collection<String> columns)
2541       throws IOException
2542     {
2543       Cursor cur = findRow(objectId);
2544       return ((cur != null) ? cur.getCurrentRow(columns) : null);
2545     }
2546 
2547     public void getTableNames(Set<String> tableNames,
2548                               boolean normalTables,
2549                               boolean systemTables,
2550                               boolean linkedTables)
2551       throws IOException
2552     {
2553       for(Row row : getTableNamesCursor().newIterable().setColumnNames(
2554               SYSTEM_CATALOG_COLUMNS)) {
2555 
2556         String tableName = row.getString(CAT_COL_NAME);
2557         int flags = row.getInt(CAT_COL_FLAGS);
2558         Short type = row.getShort(CAT_COL_TYPE);
2559         int parentId = row.getInt(CAT_COL_PARENT_ID);
2560 
2561         if(parentId != _tableParentId) {
2562           continue;
2563         }
2564 
2565         if(TYPE_TABLE.equals(type)) {
2566           if(!isSystemObject(flags)) {
2567             if(normalTables) {
2568               tableNames.add(tableName);
2569             }
2570           } else if(systemTables) {
2571             tableNames.add(tableName);
2572           }
2573         } else if(linkedTables && isAnyLinkedTableType(type)) {
2574           tableNames.add(tableName);
2575         }
2576       }
2577     }
2578 
2579     public boolean isLinkedTable(Table table) throws IOException
2580     {
2581       for(Row row : getTableNamesCursor().newIterable().setColumnNames(
2582               SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS)) {
2583         Short type = row.getShort(CAT_COL_TYPE);
2584         String linkedDbName = row.getString(CAT_COL_DATABASE);
2585         String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
2586 
2587         if(TYPE_LINKED_TABLE.equals(type) &&
2588            matchesLinkedTable(table, linkedTableName, linkedDbName)) {
2589           return true;
2590         }
2591       }
2592       return false;
2593     }
2594 
2595     public int getNextFreeSyntheticId() throws IOException {
2596       int maxSynthId = findMaxSyntheticId();
2597       if(maxSynthId >= -1) {
2598         // bummer, no more ids available
2599         throw new IllegalStateException(withErrorContext(
2600                 "Too many database objects!"));
2601       }
2602       return maxSynthId + 1;
2603     }
2604 
2605     public Iterator<TableMetaData> iterateTableMetaData() throws IOException {
2606       return new Iterator<TableMetaData>() {
2607         private final Iterator<Row> _iter =
2608           getTableNamesCursor().newIterable().setColumnNames(
2609               SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS).iterator();
2610         private TableMetaData _next;
2611 
2612         @Override
2613         public boolean hasNext() {
2614           if((_next == null) && _iter.hasNext()) {
2615             _next = nextTableMetaData(_iter);
2616           }
2617           return (_next != null);
2618         }
2619 
2620         @Override
2621         public TableMetaData next() {
2622           if(!hasNext()) {
2623             throw new NoSuchElementException();
2624           }
2625 
2626           TableMetaData next = _next;
2627           _next = null;
2628           return next;
2629         }
2630       };
2631     }
2632 
2633     private TableMetaData nextTableMetaData(Iterator<Row> detailIter) {
2634 
2635       while(detailIter.hasNext()) {
2636         Row row = detailIter.next();
2637 
2638         Short type = row.getShort(CAT_COL_TYPE);
2639         if(!isTableType(type)) {
2640           continue;
2641         }
2642 
2643         int parentId = row.getInt(CAT_COL_PARENT_ID);
2644         if(parentId != _tableParentId) {
2645           continue;
2646         }
2647 
2648         String realName = row.getString(CAT_COL_NAME);
2649 
2650         return createTableInfo(realName, type, row);
2651       }
2652 
2653       return null;
2654     }
2655 
2656     protected abstract Cursor findRow(Integer parentId, String name)
2657       throws IOException;
2658 
2659     protected abstract Cursor findRow(Integer objectId)
2660       throws IOException;
2661 
2662     protected abstract Cursor getTableNamesCursor() throws IOException;
2663 
2664     public abstract TableInfo lookupTable(String tableName)
2665       throws IOException;
2666 
2667     protected abstract int findMaxSyntheticId() throws IOException;
2668   }
2669 
2670   /**
2671    * Normal table lookup handler, using catalog table index.
2672    */
2673   private final class DefaultTableFinder extends TableFinder
2674   {
2675     private final IndexCursor _systemCatalogCursor;
2676     private IndexCursor _systemCatalogIdCursor;
2677 
2678     private DefaultTableFinder(IndexCursor systemCatalogCursor) {
2679       _systemCatalogCursor = systemCatalogCursor;
2680     }
2681 
2682     private void initIdCursor() throws IOException {
2683       if(_systemCatalogIdCursor == null) {
2684         _systemCatalogIdCursor = _systemCatalog.newCursor()
2685           .setIndexByColumnNames(CAT_COL_ID)
2686           .toIndexCursor();
2687       }
2688     }
2689 
2690     @Override
2691     protected Cursor findRow(Integer parentId, String name)
2692       throws IOException
2693     {
2694       return (_systemCatalogCursor.findFirstRowByEntry(parentId, name) ?
2695               _systemCatalogCursor : null);
2696     }
2697 
2698     @Override
2699     protected Cursor findRow(Integer objectId) throws IOException
2700     {
2701       initIdCursor();
2702       return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ?
2703               _systemCatalogIdCursor : null);
2704     }
2705 
2706     @Override
2707     public TableInfo lookupTable(String tableName) throws IOException {
2708 
2709       if(findRow(_tableParentId, tableName) == null) {
2710         return null;
2711       }
2712 
2713       Row row = _systemCatalogCursor.getCurrentRow(
2714           SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS);
2715       Short type = row.getShort(CAT_COL_TYPE);
2716 
2717       if(!isTableType(type)) {
2718         return null;
2719       }
2720 
2721       String realName = row.getString(CAT_COL_NAME);
2722 
2723       return createTableInfo(realName, type, row);
2724     }
2725 
2726     @Override
2727     protected Cursor getTableNamesCursor() throws IOException {
2728       return _systemCatalogCursor.getIndex().newCursor()
2729         .setStartEntry(_tableParentId, IndexData.MIN_VALUE)
2730         .setEndEntry(_tableParentId, IndexData.MAX_VALUE)
2731         .toIndexCursor();
2732     }
2733 
2734     @Override
2735     protected int findMaxSyntheticId() throws IOException {
2736       initIdCursor();
2737       _systemCatalogIdCursor.reset();
2738 
2739       // synthetic ids count up from min integer.  so the current, highest,
2740       // in-use synthetic id is the max id < 0.
2741       _systemCatalogIdCursor.findClosestRowByEntry(0);
2742       if(!_systemCatalogIdCursor.moveToPreviousRow()) {
2743         return Integer.MIN_VALUE;
2744       }
2745       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2746       return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol);
2747     }
2748   }
2749 
2750   /**
2751    * Fallback table lookup handler, using catalog table scans.
2752    */
2753   private final class FallbackTableFinder extends TableFinder
2754   {
2755     private final Cursor _systemCatalogCursor;
2756 
2757     private FallbackTableFinder(Cursor systemCatalogCursor) {
2758       _systemCatalogCursor = systemCatalogCursor;
2759     }
2760 
2761     @Override
2762     protected Cursor findRow(Integer parentId, String name)
2763       throws IOException
2764     {
2765       Map<String,Object> rowPat = new HashMap<String,Object>();
2766       rowPat.put(CAT_COL_PARENT_ID, parentId);
2767       rowPat.put(CAT_COL_NAME, name);
2768       return (_systemCatalogCursor.findFirstRow(rowPat) ?
2769               _systemCatalogCursor : null);
2770     }
2771 
2772     @Override
2773     protected Cursor findRow(Integer objectId) throws IOException
2774     {
2775       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2776       return (_systemCatalogCursor.findFirstRow(idCol, objectId) ?
2777               _systemCatalogCursor : null);
2778     }
2779 
2780     @Override
2781     public TableInfo lookupTable(String tableName) {
2782 
2783       for(Row row : _systemCatalogCursor.newIterable().setColumnNames(
2784               SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS)) {
2785 
2786         Short type = row.getShort(CAT_COL_TYPE);
2787         if(!isTableType(type)) {
2788           continue;
2789         }
2790 
2791         int parentId = row.getInt(CAT_COL_PARENT_ID);
2792         if(parentId != _tableParentId) {
2793           continue;
2794         }
2795 
2796         String realName = row.getString(CAT_COL_NAME);
2797         if(!tableName.equalsIgnoreCase(realName)) {
2798           continue;
2799         }
2800 
2801         return createTableInfo(realName, type, row);
2802       }
2803 
2804       return null;
2805     }
2806 
2807     @Override
2808     protected Cursor getTableNamesCursor() {
2809       return _systemCatalogCursor;
2810     }
2811 
2812     @Override
2813     protected int findMaxSyntheticId() throws IOException {
2814       // find max id < 0
2815       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2816       _systemCatalogCursor.reset();
2817       int curMaxSynthId = Integer.MIN_VALUE;
2818       while(_systemCatalogCursor.moveToNextRow()) {
2819         int id = (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
2820         if((id > curMaxSynthId) && (id < 0)) {
2821           curMaxSynthId = id;
2822         }
2823       }
2824       return curMaxSynthId;
2825     }
2826   }
2827 
2828   /**
2829    * WeakReference for a Table which holds the table pageNumber (for later
2830    * cache purging).
2831    */
2832   private static final class WeakTableReference extends WeakReference<TableImpl>
2833   {
2834     private final Integer _pageNumber;
2835 
2836     private WeakTableReference(Integer pageNumber, TableImpl table,
2837                                ReferenceQueue<TableImpl> queue) {
2838       super(table, queue);
2839       _pageNumber = pageNumber;
2840     }
2841 
2842     public Integer getPageNumber() {
2843       return _pageNumber;
2844     }
2845   }
2846 
2847   /**
2848    * Cache of currently in-use tables, allows re-use of existing tables.
2849    */
2850   private static final class TableCache
2851   {
2852     private final Map<Integer,WeakTableReference> _tables =
2853       new HashMap<Integer,WeakTableReference>();
2854     private final ReferenceQueue<TableImpl> _queue =
2855       new ReferenceQueue<TableImpl>();
2856 
2857     public TableImpl get(Integer pageNumber) {
2858       WeakTableReference ref = _tables.get(pageNumber);
2859       return ((ref != null) ? ref.get() : null);
2860     }
2861 
2862     public TableImplf="../../../../com/healthmarketscience/jackcess/impl/TableImpl.html#TableImpl">TableImpl put(TableImpl table) {
2863       purgeOldRefs();
2864 
2865       Integer pageNumber = table.getTableDefPageNumber();
2866       WeakTableReference ref = new WeakTableReference(
2867           pageNumber, table, _queue);
2868       _tables.put(pageNumber, ref);
2869 
2870       return table;
2871     }
2872 
2873     private void purgeOldRefs() {
2874       WeakTableReference oldRef = null;
2875       while((oldRef = (WeakTableReference)_queue.poll()) != null) {
2876         _tables.remove(oldRef.getPageNumber());
2877       }
2878     }
2879   }
2880 
2881   /**
2882    * Internal details for each FileForrmat
2883    * @usage _advanced_class_
2884    */
2885   public static final class FileFormatDetails
2886   {
2887     private final String _emptyFile;
2888     private final JetFormat _format;
2889 
2890     private FileFormatDetails(String emptyFile, JetFormat format) {
2891       _emptyFile = emptyFile;
2892       _format = format;
2893     }
2894 
2895     public String getEmptyFilePath() {
2896       return _emptyFile;
2897     }
2898 
2899     public JetFormat getFormat() {
2900       return _format;
2901     }
2902   }
2903 }