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