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