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    * @returns 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     if(prop != null) {
2160       return Boolean.TRUE.toString().equalsIgnoreCase(prop);
2161     }
2162     return true;
2163   }
2164 
2165   /**
2166    * Returns the default allow auto number insert policy.  This defaults to
2167    * {@code false}, but can be overridden using the system
2168    * property {@value com.healthmarketscience.jackcess.Database#ALLOW_AUTONUM_INSERT_PROPERTY}.
2169    * @usage _advanced_method_
2170    */
2171   public static boolean getDefaultAllowAutoNumberInsert()
2172   {
2173     String prop = System.getProperty(ALLOW_AUTONUM_INSERT_PROPERTY);
2174     if(prop != null) {
2175       return Boolean.TRUE.toString().equalsIgnoreCase(prop);
2176     }
2177     return false;
2178   }
2179 
2180   /**
2181    * Returns the default enable expression evaluation policy.  This defaults to
2182    * {@code true}, but can be overridden using the system
2183    * property {@value com.healthmarketscience.jackcess.Database#ENABLE_EXPRESSION_EVALUATION_PROPERTY}.
2184    * @usage _advanced_method_
2185    */
2186   public static boolean getDefaultEvaluateExpressions()
2187   {
2188     String prop = System.getProperty(ENABLE_EXPRESSION_EVALUATION_PROPERTY);
2189     if(prop != null) {
2190       return Boolean.TRUE.toString().equalsIgnoreCase(prop);
2191     }
2192     return true;
2193   }
2194 
2195   /**
2196    * Returns the default DateTimeType.  This defaults to
2197    * {@link DateTimeType#LOCAL_DATE_TIME}, but can be overridden using the system
2198    * property {@value com.healthmarketscience.jackcess.Database#DATE_TIME_TYPE_PROPERTY}.
2199    * @usage _advanced_method_
2200    */
2201   public static DateTimeType getDefaultDateTimeType() {
2202     return getEnumSystemProperty(DateTimeType.class, DATE_TIME_TYPE_PROPERTY,
2203                                  DateTimeType.LOCAL_DATE_TIME);
2204   }
2205 
2206   /**
2207    * Copies the given db InputStream to the given channel using the most
2208    * efficient means possible.
2209    */
2210   protected static void transferDbFrom(FileChannel channel, InputStream in)
2211     throws IOException
2212   {
2213     ReadableByteChannel readChannel = Channels.newChannel(in);
2214     if(!BROKEN_NIO) {
2215       // sane implementation
2216       channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE);
2217     } else {
2218       // do things the hard way for broken vms
2219       ByteBuffer bb = ByteBuffer.allocate(8096);
2220       while(readChannel.read(bb) >= 0) {
2221         bb.flip();
2222         channel.write(bb);
2223         bb.clear();
2224       }
2225     }
2226   }
2227 
2228   /**
2229    * Returns the password mask retrieved from the given header page and
2230    * format, or {@code null} if this format does not use a password mask.
2231    * @usage _advanced_method_
2232    */
2233   public static byte[] getPasswordMask(ByteBuffer buffer, JetFormat format)
2234   {
2235     // get extra password mask if necessary (the extra password mask is
2236     // generated from the database creation date stored in the header)
2237     int pwdMaskPos = format.OFFSET_HEADER_DATE;
2238     if(pwdMaskPos < 0) {
2239       return null;
2240     }
2241 
2242     buffer.position(pwdMaskPos);
2243     double dateVal = Double.longBitsToDouble(buffer.getLong());
2244 
2245     byte[] pwdMask = new byte[4];
2246     PageChannel.wrap(pwdMask).putInt((int)dateVal);
2247 
2248     return pwdMask;
2249   }
2250 
2251   protected static InputStream getResourceAsStream(String resourceName)
2252     throws IOException
2253   {
2254     InputStream stream = DatabaseImpl.class.getClassLoader()
2255       .getResourceAsStream(resourceName);
2256 
2257     if(stream == null) {
2258 
2259       stream = Thread.currentThread().getContextClassLoader()
2260         .getResourceAsStream(resourceName);
2261 
2262       if(stream == null) {
2263         throw new IOException("Could not load jackcess resource " +
2264                               resourceName);
2265       }
2266     }
2267 
2268     return stream;
2269   }
2270 
2271   private static boolean isTableType(Short objType) {
2272     return(TYPE_TABLE.equals(objType) || isAnyLinkedTableType(objType));
2273   }
2274 
2275   public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) {
2276     return FILE_FORMAT_DETAILS.get(fileFormat);
2277   }
2278 
2279   private static void addFileFormatDetails(
2280       FileFormat fileFormat, String emptyFileName, JetFormat format)
2281   {
2282     String emptyFile =
2283       ((emptyFileName != null) ?
2284        RESOURCE_PATH + emptyFileName + fileFormat.getFileExtension() : null);
2285     FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format));
2286   }
2287 
2288   private static String getName(Path file) {
2289     if(file == null) {
2290       return "<UNKNOWN.DB>";
2291     }
2292     return file.getFileName().toString();
2293   }
2294 
2295   private String withErrorContext(String msg) {
2296     return withErrorContext(msg, getName());
2297   }
2298 
2299   private static String withErrorContext(String msg, String dbName) {
2300     return msg + " (Db=" + dbName + ")";
2301   }
2302 
2303   private static <E extends Enum<E>> E getEnumSystemProperty(
2304       Class<E> enumClass, String propName, E defaultValue)
2305   {
2306     String prop = System.getProperty(propName);
2307     if(prop != null) {
2308       prop = prop.trim().toUpperCase();
2309       if(!prop.isEmpty()) {
2310         return Enum.valueOf(enumClass, prop);
2311       }
2312     }
2313     return defaultValue;
2314   }
2315 
2316   private static boolean isAnyLinkedTableType(Short type) {
2317     return (TYPE_LINKED_TABLE.equals(type) ||
2318             TYPE_LINKED_ODBC_TABLE.equals(type));
2319   }
2320 
2321   /**
2322    * Utility class for storing table page number and actual name.
2323    */
2324   private static class TableInfo implements TableMetaData
2325   {
2326     public final Integer pageNumber;
2327     public final String tableName;
2328     public final int flags;
2329     public final Short tableType;
2330 
2331     private TableInfo(Integer newPageNumber, String newTableName, int newFlags,
2332                       Short newTableType) {
2333       pageNumber = newPageNumber;
2334       tableName = newTableName;
2335       flags = newFlags;
2336       tableType = newTableType;
2337     }
2338 
2339     @Override
2340     public Type getType() {
2341       return Type.LOCAL;
2342     }
2343 
2344     @Override
2345     public String getName() {
2346       return tableName;
2347     }
2348 
2349     @Override
2350     public boolean isLinked() {
2351       return false;
2352     }
2353 
2354     @Override
2355     public boolean isSystem() {
2356       return isSystemObject(flags);
2357     }
2358 
2359     @Override
2360     public String getLinkedTableName() {
2361       return null;
2362     }
2363 
2364     @Override
2365     public String getLinkedDbName() {
2366       return null;
2367     }
2368 
2369     @Override
2370     public String getConnectionName() {
2371       return null;
2372     }
2373 
2374     @Override
2375     public Table open(Database db) throws IOException {
2376       return ((DatabaseImpl)db).getTable(this, true);
2377     }
2378 
2379     @Override
2380     public TableDefinition getTableDefinition(Database db) throws IOException {
2381       return null;
2382     }
2383 
2384     @Override
2385     public String toString() {
2386       ToStringBuilder sb = CustomToStringStyle.valueBuilder("TableMetaData")
2387         .append("name", getName());
2388         if(isSystem()) {
2389           sb.append("isSystem", isSystem());
2390         }
2391         if(isLinked()) {
2392           sb.append("isLinked", isLinked())
2393             .append("linkedTableName", getLinkedTableName())
2394             .append("linkedDbName", getLinkedDbName())
2395             .append("connectionName", maskPassword(getConnectionName()));
2396         }
2397         return sb.toString();
2398     }
2399 
2400     private static String maskPassword(String connectionName) {
2401       return ((connectionName != null) ?
2402               ODBC_PWD_PATTERN.matcher(connectionName).replaceAll("PWD=XXXXXX") :
2403               null);
2404     }
2405   }
2406 
2407   /**
2408    * Utility class for storing linked table info
2409    */
2410   private static class LinkedTableInfo extends TableInfo
2411   {
2412     private final String _linkedDbName;
2413     private final String _linkedTableName;
2414 
2415     private LinkedTableInfo(Integer newPageNumber, String newTableName,
2416                             int newFlags, Short newTableType,
2417                             String newLinkedDbName,
2418                             String newLinkedTableName) {
2419       super(newPageNumber, newTableName, newFlags, newTableType);
2420       _linkedDbName = newLinkedDbName;
2421       _linkedTableName = newLinkedTableName;
2422     }
2423 
2424     @Override
2425     public Type getType() {
2426       return Type.LINKED;
2427     }
2428 
2429     @Override
2430     public boolean isLinked() {
2431       return true;
2432     }
2433 
2434     @Override
2435     public String getLinkedTableName() {
2436       return _linkedTableName;
2437     }
2438 
2439     @Override
2440     public String getLinkedDbName() {
2441       return _linkedDbName;
2442     }
2443   }
2444 
2445   /**
2446    * Utility class for storing linked ODBC table info
2447    */
2448   private static class LinkedODBCTableInfo extends TableInfo
2449   {
2450     private final String _linkedTableName;
2451     private final String _connectionName;
2452 
2453     private LinkedODBCTableInfo(Integer newPageNumber, String newTableName,
2454                                 int newFlags, Short newTableType,
2455                                 String connectName,
2456                                 String newLinkedTableName) {
2457       super(newPageNumber, newTableName, newFlags, newTableType);
2458       _linkedTableName = newLinkedTableName;
2459       _connectionName = connectName;
2460     }
2461 
2462     @Override
2463     public Type getType() {
2464       return Type.LINKED_ODBC;
2465     }
2466 
2467     @Override
2468     public boolean isLinked() {
2469       return true;
2470     }
2471 
2472     @Override
2473     public String getLinkedTableName() {
2474       return _linkedTableName;
2475     }
2476 
2477     @Override
2478     public String getConnectionName() {
2479       return _connectionName;
2480     }
2481 
2482     @Override
2483     public Table open(Database db) throws IOException {
2484       return null;
2485     }
2486 
2487     @Override
2488     public TableDefinition getTableDefinition(Database db) throws IOException {
2489       return (((pageNumber != null) && (pageNumber > 0)) ?
2490               ((DatabaseImpl)db).getTable(this, true) :
2491               null);
2492     }
2493   }
2494 
2495   /**
2496    * Table iterator for this database, unmodifiable.
2497    */
2498   private class TableIterator implements Iterator<Table>
2499   {
2500     private Iterator<String> _tableNameIter;
2501 
2502     private TableIterator(Set<String> tableNames) {
2503       _tableNameIter = tableNames.iterator();
2504     }
2505 
2506     @Override
2507     public boolean hasNext() {
2508       return _tableNameIter.hasNext();
2509     }
2510 
2511     @Override
2512     public Table next() {
2513       if(!hasNext()) {
2514         throw new NoSuchElementException();
2515       }
2516       try {
2517         return getTable(_tableNameIter.next(), true);
2518       } catch(IOException e) {
2519         throw new RuntimeIOException(e);
2520       }
2521     }
2522   }
2523 
2524   /**
2525    * Utility class for handling table lookups.
2526    */
2527   private abstract class TableFinder
2528   {
2529     public Integer findObjectId(Integer parentId, String name)
2530       throws IOException
2531     {
2532       Cursor cur = findRow(parentId, name);
2533       if(cur == null) {
2534         return null;
2535       }
2536       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2537       return (Integer)cur.getCurrentRowValue(idCol);
2538     }
2539 
2540     public Row getObjectRow(Integer parentId, String name,
2541                             Collection<String> columns)
2542       throws IOException
2543     {
2544       Cursor cur = findRow(parentId, name);
2545       return ((cur != null) ? cur.getCurrentRow(columns) : null);
2546     }
2547 
2548     public Row getObjectRow(
2549         Integer objectId, Collection<String> columns)
2550       throws IOException
2551     {
2552       Cursor cur = findRow(objectId);
2553       return ((cur != null) ? cur.getCurrentRow(columns) : null);
2554     }
2555 
2556     public void getTableNames(Set<String> tableNames,
2557                               boolean normalTables,
2558                               boolean systemTables,
2559                               boolean linkedTables)
2560       throws IOException
2561     {
2562       for(Row row : getTableNamesCursor().newIterable().setColumnNames(
2563               SYSTEM_CATALOG_COLUMNS)) {
2564 
2565         String tableName = row.getString(CAT_COL_NAME);
2566         int flags = row.getInt(CAT_COL_FLAGS);
2567         Short type = row.getShort(CAT_COL_TYPE);
2568         int parentId = row.getInt(CAT_COL_PARENT_ID);
2569 
2570         if(parentId != _tableParentId) {
2571           continue;
2572         }
2573 
2574         if(TYPE_TABLE.equals(type)) {
2575           if(!isSystemObject(flags)) {
2576             if(normalTables) {
2577               tableNames.add(tableName);
2578             }
2579           } else if(systemTables) {
2580             tableNames.add(tableName);
2581           }
2582         } else if(linkedTables && isAnyLinkedTableType(type)) {
2583           tableNames.add(tableName);
2584         }
2585       }
2586     }
2587 
2588     public boolean isLinkedTable(Table table) throws IOException
2589     {
2590       for(Row row : getTableNamesCursor().newIterable().setColumnNames(
2591               SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS)) {
2592         Short type = row.getShort(CAT_COL_TYPE);
2593         String linkedDbName = row.getString(CAT_COL_DATABASE);
2594         String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
2595 
2596         if(TYPE_LINKED_TABLE.equals(type) &&
2597            matchesLinkedTable(table, linkedTableName, linkedDbName)) {
2598           return true;
2599         }
2600       }
2601       return false;
2602     }
2603 
2604     public int getNextFreeSyntheticId() throws IOException {
2605       int maxSynthId = findMaxSyntheticId();
2606       if(maxSynthId >= -1) {
2607         // bummer, no more ids available
2608         throw new IllegalStateException(withErrorContext(
2609                 "Too many database objects!"));
2610       }
2611       return maxSynthId + 1;
2612     }
2613 
2614     public Iterator<TableMetaData> iterateTableMetaData() throws IOException {
2615       return new Iterator<TableMetaData>() {
2616         private final Iterator<Row> _iter =
2617           getTableNamesCursor().newIterable().setColumnNames(
2618               SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS).iterator();
2619         private TableMetaData _next;
2620 
2621         @Override
2622         public boolean hasNext() {
2623           if((_next == null) && _iter.hasNext()) {
2624             _next = nextTableMetaData(_iter);
2625           }
2626           return (_next != null);
2627         }
2628 
2629         @Override
2630         public TableMetaData next() {
2631           if(!hasNext()) {
2632             throw new NoSuchElementException();
2633           }
2634 
2635           TableMetaData next = _next;
2636           _next = null;
2637           return next;
2638         }
2639       };
2640     }
2641 
2642     private TableMetaData nextTableMetaData(Iterator<Row> detailIter) {
2643 
2644       while(detailIter.hasNext()) {
2645         Row row = detailIter.next();
2646 
2647         Short type = row.getShort(CAT_COL_TYPE);
2648         if(!isTableType(type)) {
2649           continue;
2650         }
2651 
2652         int parentId = row.getInt(CAT_COL_PARENT_ID);
2653         if(parentId != _tableParentId) {
2654           continue;
2655         }
2656 
2657         String realName = row.getString(CAT_COL_NAME);
2658 
2659         return createTableInfo(realName, type, row);
2660       }
2661 
2662       return null;
2663     }
2664 
2665     protected abstract Cursor findRow(Integer parentId, String name)
2666       throws IOException;
2667 
2668     protected abstract Cursor findRow(Integer objectId)
2669       throws IOException;
2670 
2671     protected abstract Cursor getTableNamesCursor() throws IOException;
2672 
2673     public abstract TableInfo lookupTable(String tableName)
2674       throws IOException;
2675 
2676     protected abstract int findMaxSyntheticId() throws IOException;
2677   }
2678 
2679   /**
2680    * Normal table lookup handler, using catalog table index.
2681    */
2682   private final class DefaultTableFinder extends TableFinder
2683   {
2684     private final IndexCursor _systemCatalogCursor;
2685     private IndexCursor _systemCatalogIdCursor;
2686 
2687     private DefaultTableFinder(IndexCursor systemCatalogCursor) {
2688       _systemCatalogCursor = systemCatalogCursor;
2689     }
2690 
2691     private void initIdCursor() throws IOException {
2692       if(_systemCatalogIdCursor == null) {
2693         _systemCatalogIdCursor = _systemCatalog.newCursor()
2694           .setIndexByColumnNames(CAT_COL_ID)
2695           .toIndexCursor();
2696       }
2697     }
2698 
2699     @Override
2700     protected Cursor findRow(Integer parentId, String name)
2701       throws IOException
2702     {
2703       return (_systemCatalogCursor.findFirstRowByEntry(parentId, name) ?
2704               _systemCatalogCursor : null);
2705     }
2706 
2707     @Override
2708     protected Cursor findRow(Integer objectId) throws IOException
2709     {
2710       initIdCursor();
2711       return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ?
2712               _systemCatalogIdCursor : null);
2713     }
2714 
2715     @Override
2716     public TableInfo lookupTable(String tableName) throws IOException {
2717 
2718       if(findRow(_tableParentId, tableName) == null) {
2719         return null;
2720       }
2721 
2722       Row row = _systemCatalogCursor.getCurrentRow(
2723           SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS);
2724       Short type = row.getShort(CAT_COL_TYPE);
2725 
2726       if(!isTableType(type)) {
2727         return null;
2728       }
2729 
2730       String realName = row.getString(CAT_COL_NAME);
2731 
2732       return createTableInfo(realName, type, row);
2733     }
2734 
2735     @Override
2736     protected Cursor getTableNamesCursor() throws IOException {
2737       return _systemCatalogCursor.getIndex().newCursor()
2738         .setStartEntry(_tableParentId, IndexData.MIN_VALUE)
2739         .setEndEntry(_tableParentId, IndexData.MAX_VALUE)
2740         .toIndexCursor();
2741     }
2742 
2743     @Override
2744     protected int findMaxSyntheticId() throws IOException {
2745       initIdCursor();
2746       _systemCatalogIdCursor.reset();
2747 
2748       // synthetic ids count up from min integer.  so the current, highest,
2749       // in-use synthetic id is the max id < 0.
2750       _systemCatalogIdCursor.findClosestRowByEntry(0);
2751       if(!_systemCatalogIdCursor.moveToPreviousRow()) {
2752         return Integer.MIN_VALUE;
2753       }
2754       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2755       return (Integer)_systemCatalogIdCursor.getCurrentRowValue(idCol);
2756     }
2757   }
2758 
2759   /**
2760    * Fallback table lookup handler, using catalog table scans.
2761    */
2762   private final class FallbackTableFinder extends TableFinder
2763   {
2764     private final Cursor _systemCatalogCursor;
2765 
2766     private FallbackTableFinder(Cursor systemCatalogCursor) {
2767       _systemCatalogCursor = systemCatalogCursor;
2768     }
2769 
2770     @Override
2771     protected Cursor findRow(Integer parentId, String name)
2772       throws IOException
2773     {
2774       Map<String,Object> rowPat = new HashMap<String,Object>();
2775       rowPat.put(CAT_COL_PARENT_ID, parentId);
2776       rowPat.put(CAT_COL_NAME, name);
2777       return (_systemCatalogCursor.findFirstRow(rowPat) ?
2778               _systemCatalogCursor : null);
2779     }
2780 
2781     @Override
2782     protected Cursor findRow(Integer objectId) throws IOException
2783     {
2784       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2785       return (_systemCatalogCursor.findFirstRow(idCol, objectId) ?
2786               _systemCatalogCursor : null);
2787     }
2788 
2789     @Override
2790     public TableInfo lookupTable(String tableName) throws IOException {
2791 
2792       for(Row row : _systemCatalogCursor.newIterable().setColumnNames(
2793               SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS)) {
2794 
2795         Short type = row.getShort(CAT_COL_TYPE);
2796         if(!isTableType(type)) {
2797           continue;
2798         }
2799 
2800         int parentId = row.getInt(CAT_COL_PARENT_ID);
2801         if(parentId != _tableParentId) {
2802           continue;
2803         }
2804 
2805         String realName = row.getString(CAT_COL_NAME);
2806         if(!tableName.equalsIgnoreCase(realName)) {
2807           continue;
2808         }
2809 
2810         return createTableInfo(realName, type, row);
2811       }
2812 
2813       return null;
2814     }
2815 
2816     @Override
2817     protected Cursor getTableNamesCursor() throws IOException {
2818       return _systemCatalogCursor;
2819     }
2820 
2821     @Override
2822     protected int findMaxSyntheticId() throws IOException {
2823       // find max id < 0
2824       ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID);
2825       _systemCatalogCursor.reset();
2826       int curMaxSynthId = Integer.MIN_VALUE;
2827       while(_systemCatalogCursor.moveToNextRow()) {
2828         int id = (Integer)_systemCatalogCursor.getCurrentRowValue(idCol);
2829         if((id > curMaxSynthId) && (id < 0)) {
2830           curMaxSynthId = id;
2831         }
2832       }
2833       return curMaxSynthId;
2834     }
2835   }
2836 
2837   /**
2838    * WeakReference for a Table which holds the table pageNumber (for later
2839    * cache purging).
2840    */
2841   private static final class WeakTableReference extends WeakReference<TableImpl>
2842   {
2843     private final Integer _pageNumber;
2844 
2845     private WeakTableReference(Integer pageNumber, TableImpl table,
2846                                ReferenceQueue<TableImpl> queue) {
2847       super(table, queue);
2848       _pageNumber = pageNumber;
2849     }
2850 
2851     public Integer getPageNumber() {
2852       return _pageNumber;
2853     }
2854   }
2855 
2856   /**
2857    * Cache of currently in-use tables, allows re-use of existing tables.
2858    */
2859   private static final class TableCache
2860   {
2861     private final Map<Integer,WeakTableReference> _tables =
2862       new HashMap<Integer,WeakTableReference>();
2863     private final ReferenceQueue<TableImpl> _queue =
2864       new ReferenceQueue<TableImpl>();
2865 
2866     public TableImpl get(Integer pageNumber) {
2867       WeakTableReference ref = _tables.get(pageNumber);
2868       return ((ref != null) ? ref.get() : null);
2869     }
2870 
2871     public TableImplf="../../../../com/healthmarketscience/jackcess/impl/TableImpl.html#TableImpl">TableImpl put(TableImpl table) {
2872       purgeOldRefs();
2873 
2874       Integer pageNumber = table.getTableDefPageNumber();
2875       WeakTableReference ref = new WeakTableReference(
2876           pageNumber, table, _queue);
2877       _tables.put(pageNumber, ref);
2878 
2879       return table;
2880     }
2881 
2882     private void purgeOldRefs() {
2883       WeakTableReference oldRef = null;
2884       while((oldRef = (WeakTableReference)_queue.poll()) != null) {
2885         _tables.remove(oldRef.getPageNumber());
2886       }
2887     }
2888   }
2889 
2890   /**
2891    * Internal details for each FileForrmat
2892    * @usage _advanced_class_
2893    */
2894   public static final class FileFormatDetails
2895   {
2896     private final String _emptyFile;
2897     private final JetFormat _format;
2898 
2899     private FileFormatDetails(String emptyFile, JetFormat format) {
2900       _emptyFile = emptyFile;
2901       _format = format;
2902     }
2903 
2904     public String getEmptyFilePath() {
2905       return _emptyFile;
2906     }
2907 
2908     public JetFormat getFormat() {
2909       return _format;
2910     }
2911   }
2912 }