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