View Javadoc

1   /*
2   Copyright (c) 2005 Health Market Science, Inc.
3   
4   This library is free software; you can redistribute it and/or
5   modify it under the terms of the GNU Lesser General Public
6   License as published by the Free Software Foundation; either
7   version 2.1 of the License, or (at your option) any later version.
8   
9   This library is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  Lesser General Public License for more details.
13  
14  You should have received a copy of the GNU Lesser General Public
15  License along with this library; if not, write to the Free Software
16  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
17  USA
18  
19  You can contact Health Market Science at info@healthmarketscience.com
20  or at the following address:
21  
22  Health Market Science
23  2700 Horizon Drive
24  Suite 200
25  King of Prussia, PA 19406
26  */
27  
28  package com.healthmarketscience.jackcess;
29  
30  import java.io.BufferedReader;
31  import java.io.Closeable;
32  import java.io.File;
33  import java.io.FileNotFoundException;
34  import java.io.FileReader;
35  import java.io.Flushable;
36  import java.io.IOException;
37  import java.io.RandomAccessFile;
38  import java.nio.ByteBuffer;
39  import java.nio.channels.Channels;
40  import java.nio.channels.FileChannel;
41  import java.sql.ResultSet;
42  import java.sql.ResultSetMetaData;
43  import java.sql.SQLException;
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.Collection;
47  import java.util.ConcurrentModificationException;
48  import java.util.Date;
49  import java.util.HashMap;
50  import java.util.HashSet;
51  import java.util.Iterator;
52  import java.util.LinkedList;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.NoSuchElementException;
56  import java.util.Set;
57  
58  import org.apache.commons.lang.builder.ToStringBuilder;
59  import org.apache.commons.logging.Log;
60  import org.apache.commons.logging.LogFactory;
61  
62  /**
63   * An Access database.
64   * <p>
65   * There is now experimental, optional support for large indexes (disabled by
66   * default).  This optional support can be enabled via a few different means:
67   * <ul>
68   * <li>Setting the system property {@value #USE_BIG_INDEX_PROPERTY} to
69   *     {@code "true"} will enable "large" index support accross the jvm</li>
70   * <li>Calling {@link #setUseBigIndex} with {@code true} on a Database
71   *     instance will enable "large" index support for all tables subsequently
72   *     created from that instance</li>
73   * <li>Calling {@link #getTable(String,boolean)} can selectively
74   *     enable/disable "large" index support on a per-table basis (overriding
75   *     any Database or system property setting)</li>
76   * </ul>
77   *
78   * @author Tim McCune
79   */
80  public class Database
81    implements Iterable<Table>, Closeable, Flushable
82  {
83    
84    private static final Log LOG = LogFactory.getLog(Database.class);
85  
86    /** this is the default "userId" used if we cannot find existing info.  this
87        seems to be some standard "Admin" userId for access files */
88    private static final byte[] SYS_DEFAULT_SID = new byte[2];
89    static {
90      SYS_DEFAULT_SID[0] = (byte) 0xA6;
91      SYS_DEFAULT_SID[1] = (byte) 0x33;
92    }
93  
94    /** default value for the auto-sync value ({@code true}).  this is slower,
95        but leaves more chance of a useable database in the face of failures. */
96    public static final boolean DEFAULT_AUTO_SYNC = true;
97  
98    /** system property which can be used to make big index support the
99        default. */
100   public static final String USE_BIG_INDEX_PROPERTY =
101     "com.healthmarketscience.jackcess.bigIndex";
102   
103   /** Batch commit size for copying other result sets into this database */
104   private static final int COPY_TABLE_BATCH_SIZE = 200;
105   
106   /** System catalog always lives on page 2 */
107   private static final int PAGE_SYSTEM_CATALOG = 2;
108   /** Name of the system catalog */
109   private static final String TABLE_SYSTEM_CATALOG = "MSysObjects";
110 
111   /** this is the access control bit field for created tables.  the value used
112       is equivalent to full access (Visual Basic DAO PermissionEnum constant:
113       dbSecFullAccess) */
114   private static final Integer SYS_FULL_ACCESS_ACM = 1048575;
115 
116   /** ACE table column name of the actual access control entry */
117   private static final String ACE_COL_ACM = "ACM";
118   /** ACE table column name of the inheritable attributes flag */
119   private static final String ACE_COL_F_INHERITABLE = "FInheritable";
120   /** ACE table column name of the relevant objectId */
121   private static final String ACE_COL_OBJECT_ID = "ObjectId";
122   /** ACE table column name of the relevant userId */
123   private static final String ACE_COL_SID = "SID";
124 
125   /** Relationship table column name of the column count */
126   private static final String REL_COL_COLUMN_COUNT = "ccolumn";
127   /** Relationship table column name of the flags */
128   private static final String REL_COL_FLAGS = "grbit";
129   /** Relationship table column name of the index of the columns */
130   private static final String REL_COL_COLUMN_INDEX = "icolumn";
131   /** Relationship table column name of the "to" column name */
132   private static final String REL_COL_TO_COLUMN = "szColumn";
133   /** Relationship table column name of the "to" table name */
134   private static final String REL_COL_TO_TABLE = "szObject";
135   /** Relationship table column name of the "from" column name */
136   private static final String REL_COL_FROM_COLUMN = "szReferencedColumn";
137   /** Relationship table column name of the "from" table name */
138   private static final String REL_COL_FROM_TABLE = "szReferencedObject";
139   /** Relationship table column name of the relationship */
140   private static final String REL_COL_NAME = "szRelationship";
141   
142   /** System catalog column name of the page on which system object definitions
143       are stored */
144   private static final String CAT_COL_ID = "Id";
145   /** System catalog column name of the name of a system object */
146   private static final String CAT_COL_NAME = "Name";
147   private static final String CAT_COL_OWNER = "Owner";
148   /** System catalog column name of a system object's parent's id */
149   private static final String CAT_COL_PARENT_ID = "ParentId";
150   /** System catalog column name of the type of a system object */
151   private static final String CAT_COL_TYPE = "Type";
152   /** System catalog column name of the date a system object was created */
153   private static final String CAT_COL_DATE_CREATE = "DateCreate";
154   /** System catalog column name of the date a system object was updated */
155   private static final String CAT_COL_DATE_UPDATE = "DateUpdate";
156   /** System catalog column name of the flags column */
157   private static final String CAT_COL_FLAGS = "Flags";
158   
159   /** Empty database template for creating new databases */
160   private static final String EMPTY_MDB = "com/healthmarketscience/jackcess/empty.mdb";
161   /** Prefix for column or table names that are reserved words */
162   private static final String ESCAPE_PREFIX = "x";
163   /** Prefix that flags system tables */
164   private static final String PREFIX_SYSTEM = "MSys";
165   /** Name of the system object that is the parent of all tables */
166   private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables";
167   /** Name of the table that contains system access control entries */
168   private static final String TABLE_SYSTEM_ACES = "MSysACEs";
169   /** Name of the table that contains table relationships */
170   private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships";
171   /** System object type for table definitions */
172   private static final Short TYPE_TABLE = (short) 1;
173 
174   /** the columns to read when reading system catalog initially */
175   private static Collection<String> SYSTEM_CATALOG_COLUMNS =
176     new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID));
177   
178   
179   /**
180    * All of the reserved words in Access that should be escaped when creating
181    * table or column names
182    */
183   private static final Set<String> RESERVED_WORDS = new HashSet<String>();
184   static {
185     //Yup, there's a lot.
186     RESERVED_WORDS.addAll(Arrays.asList(
187        "add", "all", "alphanumeric", "alter", "and", "any", "application", "as",
188        "asc", "assistant", "autoincrement", "avg", "between", "binary", "bit",
189        "boolean", "by", "byte", "char", "character", "column", "compactdatabase",
190        "constraint", "container", "count", "counter", "create", "createdatabase",
191        "createfield", "creategroup", "createindex", "createobject", "createproperty",
192        "createrelation", "createtabledef", "createuser", "createworkspace",
193        "currency", "currentuser", "database", "date", "datetime", "delete",
194        "desc", "description", "disallow", "distinct", "distinctrow", "document",
195        "double", "drop", "echo", "else", "end", "eqv", "error", "exists", "exit",
196        "false", "field", "fields", "fillcache", "float", "float4", "float8",
197        "foreign", "form", "forms", "from", "full", "function", "general",
198        "getobject", "getoption", "gotopage", "group", "group by", "guid", "having",
199        "idle", "ieeedouble", "ieeesingle", "if", "ignore", "imp", "in", "index",
200        "indexes", "inner", "insert", "inserttext", "int", "integer", "integer1",
201        "integer2", "integer4", "into", "is", "join", "key", "lastmodified", "left",
202        "level", "like", "logical", "logical1", "long", "longbinary", "longtext",
203        "macro", "match", "max", "min", "mod", "memo", "module", "money", "move",
204        "name", "newpassword", "no", "not", "null", "number", "numeric", "object",
205        "oleobject", "off", "on", "openrecordset", "option", "or", "order", "outer",
206        "owneraccess", "parameter", "parameters", "partial", "percent", "pivot",
207        "primary", "procedure", "property", "queries", "query", "quit", "real",
208        "recalc", "recordset", "references", "refresh", "refreshlink",
209        "registerdatabase", "relation", "repaint", "repairdatabase", "report",
210        "reports", "requery", "right", "screen", "section", "select", "set",
211        "setfocus", "setoption", "short", "single", "smallint", "some", "sql",
212        "stdev", "stdevp", "string", "sum", "table", "tabledef", "tabledefs",
213        "tableid", "text", "time", "timestamp", "top", "transform", "true", "type",
214        "union", "unique", "update", "user", "value", "values", "var", "varp",
215        "varbinary", "varchar", "where", "with", "workspace", "xor", "year", "yes",
216        "yesno"
217     ));
218   }
219   
220   /** Buffer to hold database pages */
221   private ByteBuffer _buffer;
222   /** ID of the Tables system object */
223   private Integer _tableParentId;
224   /** Format that the containing database is in */
225   private final JetFormat _format;
226   /**
227    * Map of UPPERCASE table names to page numbers containing their definition
228    * and their stored table name.
229    */
230   private Map<String, TableInfo> _tableLookup =
231     new HashMap<String, TableInfo>();
232   /** set of table names as stored in the mdb file, created on demand */
233   private Set<String> _tableNames;
234   /** Reads and writes database pages */
235   private final PageChannel _pageChannel;
236   /** System catalog table */
237   private Table _systemCatalog;
238   /** System access control entries table */
239   private Table _accessControlEntries;
240   /** page number of the system relationships table */
241   private Integer _relationshipsPageNumber;
242   /** System relationships table (initialized on first use) */
243   private Table _relationships;
244   /** SIDs to use for the ACEs added for new tables */
245   private final List<byte[]> _newTableSIDs = new ArrayList<byte[]>();
246   /** for now, "big index support" is optional */
247   private boolean _useBigIndex;
248   
249   /**
250    * Open an existing Database.  If the existing file is not writeable, the
251    * file will be opened read-only.  Auto-syncing is enabled for the returned
252    * Database.
253    * @param mdbFile File containing the database
254    */
255   public static Database open(File mdbFile) throws IOException {
256     return open(mdbFile, false);
257   }
258   
259   /**
260    * Open an existing Database.  If the existing file is not writeable or the
261    * readOnly flag is <code>true</code>, the file will be opened read-only.
262    * Auto-syncing is enabled for the returned Database.
263    * @param mdbFile File containing the database
264    * @param readOnly iff <code>true</code>, force opening file in read-only
265    *                 mode
266    */
267   public static Database open(File mdbFile, boolean readOnly)
268     throws IOException
269   {
270     return open(mdbFile, readOnly, DEFAULT_AUTO_SYNC);
271   }
272   
273   /**
274    * Open an existing Database.  If the existing file is not writeable or the
275    * readOnly flag is <code>true</code>, the file will be opened read-only.
276    * @param mdbFile File containing the database
277    * @param readOnly iff <code>true</code>, force opening file in read-only
278    *                 mode
279    * @param autoSync whether or not to enable auto-syncing on write.  if
280    *                 {@code true}, writes will be immediately flushed to disk.
281    *                 This leaves the database in a (fairly) consistent state
282    *                 on each write, but can be very inefficient for many
283    *                 updates.  if {@code false}, flushing to disk happens at
284    *                 the jvm's leisure, which can be much faster, but may
285    *                 leave the database in an inconsistent state if failures
286    *                 are encountered during writing.
287    */
288   public static Database open(File mdbFile, boolean readOnly, boolean autoSync)
289     throws IOException
290   {    
291     if(!mdbFile.exists() || !mdbFile.canRead()) {
292       throw new FileNotFoundException("given file does not exist: " + mdbFile);
293     }
294     return new Database(openChannel(mdbFile,
295                                     (!mdbFile.canWrite() || readOnly)),
296                         autoSync);
297   }
298   
299   /**
300    * Create a new Database
301    * @param mdbFile Location to write the new database to.  <b>If this file
302    *    already exists, it will be overwritten.</b>
303    */
304   public static Database create(File mdbFile) throws IOException {
305     return create(mdbFile, DEFAULT_AUTO_SYNC);
306   }
307   
308   /**
309    * Create a new Database
310    * @param mdbFile Location to write the new database to.  <b>If this file
311    *    already exists, it will be overwritten.</b>
312    * @param autoSync whether or not to enable auto-syncing on write.  if
313    *                 {@code true}, writes will be immediately flushed to disk.
314    *                 This leaves the database in a (fairly) consistent state
315    *                 on each write, but can be very inefficient for many
316    *                 updates.  if {@code false}, flushing to disk happens at
317    *                 the jvm's leisure, which can be much faster, but may
318    *                 leave the database in an inconsistent state if failures
319    *                 are encountered during writing.
320    */
321   public static Database create(File mdbFile, boolean autoSync)
322     throws IOException
323   {    
324     FileChannel channel = openChannel(mdbFile, false);
325     channel.truncate(0);
326     channel.transferFrom(Channels.newChannel(
327         Thread.currentThread().getContextClassLoader().getResourceAsStream(
328             EMPTY_MDB)), 0, Integer.MAX_VALUE);
329     return new Database(channel, autoSync);
330   }
331   
332   private static FileChannel openChannel(File mdbFile, boolean readOnly)
333     throws FileNotFoundException
334   {
335     String mode = (readOnly ? "r" : "rw");
336     return new RandomAccessFile(mdbFile, mode).getChannel();
337   }
338   
339   /**
340    * Create a new database by reading it in from a FileChannel.
341    * @param channel File channel of the database.  This needs to be a
342    *    FileChannel instead of a ReadableByteChannel because we need to
343    *    randomly jump around to various points in the file.
344    */
345   protected Database(FileChannel channel, boolean autoSync) throws IOException
346   {
347     _format = JetFormat.getFormat(channel);
348     _pageChannel = new PageChannel(channel, _format, autoSync);
349     // note, it's slighly sketchy to pass ourselves along partially
350     // constructed, but only our _format and _pageChannel refs should be
351     // needed
352     _pageChannel.initialize(this);
353     _buffer = _pageChannel.createPageBuffer();
354     readSystemCatalog();
355   }
356   
357   public PageChannel getPageChannel() {
358     return _pageChannel;
359   }
360 
361   public JetFormat getFormat() {
362     return _format;
363   }
364   
365   /**
366    * @return The system catalog table
367    */
368   public Table getSystemCatalog() {
369     return _systemCatalog;
370   }
371   
372   /**
373    * @return The system Access Control Entries table
374    */
375   public Table getAccessControlEntries() {
376     return _accessControlEntries;
377   }
378 
379   /**
380    * Whether or not big index support is enabled for tables.
381    */
382   public boolean doUseBigIndex() {
383     return _useBigIndex;
384   }
385 
386   /**
387    * Set whether or not big index support is enabled for tables.
388    */
389   public void setUseBigIndex(boolean useBigIndex) {
390     _useBigIndex = useBigIndex;
391   }
392   
393   /**
394    * Read the system catalog
395    */
396   private void readSystemCatalog() throws IOException {
397     _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
398                                defaultUseBigIndex());
399     for(Map<String,Object> row :
400           Cursor.createCursor(_systemCatalog).iterable(
401               SYSTEM_CATALOG_COLUMNS))
402     {
403       String name = (String) row.get(CAT_COL_NAME);
404       if (name != null && TYPE_TABLE.equals(row.get(CAT_COL_TYPE))) {
405         if (!name.startsWith(PREFIX_SYSTEM)) {
406           addTable((String) row.get(CAT_COL_NAME), (Integer) row.get(CAT_COL_ID));
407         } else if(TABLE_SYSTEM_ACES.equals(name)) {
408           int pageNumber = (Integer)row.get(CAT_COL_ID);
409           _accessControlEntries = readTable(TABLE_SYSTEM_ACES, pageNumber,
410                                             defaultUseBigIndex());
411         } else if(TABLE_SYSTEM_RELATIONSHIPS.equals(name)) {
412           _relationshipsPageNumber = (Integer)row.get(CAT_COL_ID);
413         }
414       } else if (SYSTEM_OBJECT_NAME_TABLES.equals(name)) {
415         _tableParentId = (Integer) row.get(CAT_COL_ID);
416       }
417     }
418 
419     // check for required system values
420     if(_accessControlEntries == null) {
421       throw new IOException("Did not find required " + TABLE_SYSTEM_ACES +
422                             " table");
423     }
424     if(_tableParentId == null) {
425       throw new IOException("Did not find required parent table id");
426     }
427     
428     if (LOG.isDebugEnabled()) {
429       LOG.debug("Finished reading system catalog.  Tables: " +
430                 getTableNames());
431     }
432   }
433   
434   /**
435    * @return The names of all of the user tables (String)
436    */
437   public Set<String> getTableNames() {
438     if(_tableNames == null) {
439       _tableNames = new HashSet<String>();
440       for(TableInfo tableInfo : _tableLookup.values()) {
441         _tableNames.add(tableInfo.tableName);
442       }
443     }
444     return _tableNames;
445   }
446 
447   /**
448    * @return an unmodifiable Iterator of the user Tables in this Database.
449    * @throws IllegalStateException if an IOException is thrown by one of the
450    *         operations, the actual exception will be contained within
451    * @throws ConcurrentModificationException if a table is added to the
452    *         database while an Iterator is in use.
453    */
454   public Iterator<Table> iterator() {
455     return new TableIterator();
456   }
457 
458   /**
459    * @param name Table name
460    * @return The table, or null if it doesn't exist
461    */
462   public Table getTable(String name) throws IOException {
463     return getTable(name, defaultUseBigIndex());
464   }
465   
466   /**
467    * @param name Table name
468    * @param useBigIndex whether or not "big index support" should be enabled
469    *                    for the table (this value will override any other
470    *                    settings)
471    * @return The table, or null if it doesn't exist
472    */
473   public Table getTable(String name, boolean useBigIndex) throws IOException {
474 
475     TableInfo tableInfo = lookupTable(name);
476     
477     if ((tableInfo == null) || (tableInfo.pageNumber == null)) {
478       return null;
479     }
480 
481     return readTable(tableInfo.tableName, tableInfo.pageNumber, useBigIndex);
482   }
483   
484   /**
485    * Create a new table in this database
486    * @param name Name of the table to create
487    * @param columns List of Columns in the table
488    */
489   public void createTable(String name, List<Column> columns)
490     throws IOException
491   {
492     if(getTable(name) != null) {
493       throw new IllegalArgumentException(
494           "Cannot create table with name of existing table");
495     }
496     if(columns.isEmpty()) {
497       throw new IllegalArgumentException(
498           "Cannot create table with no columns");
499     }
500 
501     Set<String> colNames = new HashSet<String>();
502     // next, validate the column definitions
503     for(Column column : columns) {
504       column.validate(_format);
505       if(!colNames.add(column.getName().toUpperCase())) {
506         throw new IllegalArgumentException("duplicate column name: " +
507                                            column.getName());
508       }
509     }
510 
511     if(Table.countAutoNumberColumns(columns) > 1) {
512       throw new IllegalArgumentException(
513           "Can have at most one AutoNumber column per table");
514     }
515     
516     //Write the tdef page to disk.
517     int tdefPageNumber = Table.writeTableDefinition(columns, _pageChannel,
518                                                     _format);
519     
520     //Add this table to our internal list.
521     addTable(name, Integer.valueOf(tdefPageNumber));
522     
523     //Add this table to system tables
524     addToSystemCatalog(name, tdefPageNumber);
525     addToAccessControlEntries(tdefPageNumber);    
526   }
527 
528   /**
529    * Finds all the relationships in the database between the given tables.
530    */
531   public List<Relationship> getRelationships(Table table1, Table table2)
532     throws IOException
533   {
534     // the relationships table does not get loaded until first accessed
535     if(_relationships == null) {
536       if(_relationshipsPageNumber == null) {
537         throw new IOException("Could not find system relationships table");
538       }
539       _relationships = readTable(TABLE_SYSTEM_RELATIONSHIPS,
540                                  _relationshipsPageNumber,
541                                  defaultUseBigIndex());
542     }
543 
544     int nameCmp = table1.getName().compareTo(table2.getName());
545     if(nameCmp == 0) {
546       throw new IllegalArgumentException("Must provide two different tables");
547     }
548     if(nameCmp > 0) {
549       // we "order" the two tables given so that we will return a collection
550       // of relationships in the same order regardless of whether we are given
551       // (TableFoo, TableBar) or (TableBar, TableFoo).
552       Table tmp = table1;
553       table1 = table2;
554       table2 = tmp;
555     }
556       
557 
558     List<Relationship> relationships = new ArrayList<Relationship>();
559     Cursor cursor = createCursorWithOptionalIndex(
560         _relationships, REL_COL_FROM_TABLE, table1.getName());
561     collectRelationships(cursor, table1, table2, relationships);
562     cursor = createCursorWithOptionalIndex(
563         _relationships, REL_COL_TO_TABLE, table1.getName());
564     collectRelationships(cursor, table2, table1, relationships);
565     
566     return relationships;
567   }
568 
569   /**
570    * Finds the relationships matching the given from and to tables from the
571    * given cursor and adds them to the given list.
572    */
573   private void collectRelationships(
574       Cursor cursor, Table fromTable, Table toTable,
575       List<Relationship> relationships)
576   {
577     for(Map<String,Object> row : cursor) {
578       String fromName = (String)row.get(REL_COL_FROM_TABLE);
579       String toName = (String)row.get(REL_COL_TO_TABLE);
580       
581       if(fromTable.getName().equals(fromName) &&
582          toTable.getName().equals(toName))
583       {
584 
585         String relName = (String)row.get(REL_COL_NAME);
586         
587         // found more info for a relationship.  see if we already have some
588         // info for this relationship
589         Relationship rel = null;
590         for(Relationship tmp : relationships) {
591           if(tmp.getName().equals(relName)) {
592             rel = tmp;
593             break;
594           }
595         }
596 
597         if(rel == null) {
598           // new relationship
599           int numCols = (Integer)row.get(REL_COL_COLUMN_COUNT);
600           int flags = (Integer)row.get(REL_COL_FLAGS);
601           rel = new Relationship(relName, fromTable, toTable,
602                                  flags, numCols);
603           relationships.add(rel);
604         }
605 
606         // add column info
607         int colIdx = (Integer)row.get(REL_COL_COLUMN_INDEX);
608         Column fromCol = fromTable.getColumn(
609             (String)row.get(REL_COL_FROM_COLUMN));
610         Column toCol = toTable.getColumn(
611             (String)row.get(REL_COL_TO_COLUMN));
612 
613         rel.getFromColumns().set(colIdx, fromCol);
614         rel.getToColumns().set(colIdx, toCol);
615       }
616     }    
617   }
618   
619   /**
620    * Add a new table to the system catalog
621    * @param name Table name
622    * @param pageNumber Page number that contains the table definition
623    */
624   private void addToSystemCatalog(String name, int pageNumber)
625     throws IOException
626   {
627     Object[] catalogRow = new Object[_systemCatalog.getColumnCount()];
628     int idx = 0;
629     Date creationTime = new Date();
630     for (Iterator<Column> iter = _systemCatalog.getColumns().iterator();
631          iter.hasNext(); idx++)
632     {
633       Column col = iter.next();
634       if (CAT_COL_ID.equals(col.getName())) {
635         catalogRow[idx] = Integer.valueOf(pageNumber);
636       } else if (CAT_COL_NAME.equals(col.getName())) {
637         catalogRow[idx] = name;
638       } else if (CAT_COL_TYPE.equals(col.getName())) {
639         catalogRow[idx] = TYPE_TABLE;
640       } else if (CAT_COL_DATE_CREATE.equals(col.getName()) ||
641                  CAT_COL_DATE_UPDATE.equals(col.getName())) {
642         catalogRow[idx] = creationTime;
643       } else if (CAT_COL_PARENT_ID.equals(col.getName())) {
644         catalogRow[idx] = _tableParentId;
645       } else if (CAT_COL_FLAGS.equals(col.getName())) {
646         catalogRow[idx] = Integer.valueOf(0);
647       } else if (CAT_COL_OWNER.equals(col.getName())) {
648         byte[] owner = new byte[2];
649         catalogRow[idx] = owner;
650         owner[0] = (byte) 0xcf;
651         owner[1] = (byte) 0x5f;
652       }
653     }
654     _systemCatalog.addRow(catalogRow);
655   }
656   
657   /**
658    * Add a new table to the system's access control entries
659    * @param pageNumber Page number that contains the table definition
660    */
661   private void addToAccessControlEntries(int pageNumber) throws IOException {
662     
663     if(_newTableSIDs.isEmpty()) {
664       initNewTableSIDs();
665     }
666 
667     Column acmCol = _accessControlEntries.getColumn(ACE_COL_ACM);
668     Column inheritCol = _accessControlEntries.getColumn(ACE_COL_F_INHERITABLE);
669     Column objIdCol = _accessControlEntries.getColumn(ACE_COL_OBJECT_ID);
670     Column sidCol = _accessControlEntries.getColumn(ACE_COL_SID);
671 
672     // construct a collection of ACE entries mimicing those of our parent, the
673     // "Tables" system object
674     List<Object[]> aceRows = new ArrayList<Object[]>(_newTableSIDs.size());
675     for(byte[] sid : _newTableSIDs) {
676       Object[] aceRow = new Object[_accessControlEntries.getColumnCount()];
677       aceRow[acmCol.getColumnIndex()] = SYS_FULL_ACCESS_ACM;
678       aceRow[inheritCol.getColumnIndex()] = Boolean.FALSE;
679       aceRow[objIdCol.getColumnIndex()] = Integer.valueOf(pageNumber);
680       aceRow[sidCol.getColumnIndex()] = sid;
681       aceRows.add(aceRow);
682     }
683     _accessControlEntries.addRows(aceRows);  
684   }
685 
686   /**
687    * Determines the collection of SIDs which need to be added to new tables.
688    */
689   private void initNewTableSIDs() throws IOException
690   {
691     // search for ACEs matching the tableParentId.  use the index on the
692     // objectId column if found (should be there)
693     Cursor cursor = createCursorWithOptionalIndex(
694         _accessControlEntries, ACE_COL_OBJECT_ID, _tableParentId);
695     
696     for(Map<String, Object> row : cursor) {
697       Integer objId = (Integer)row.get(ACE_COL_OBJECT_ID);
698       if(_tableParentId.equals(objId)) {
699         _newTableSIDs.add((byte[])row.get(ACE_COL_SID));
700       }
701     }
702 
703     if(_newTableSIDs.isEmpty()) {
704       // if all else fails, use the hard-coded default
705       _newTableSIDs.add(SYS_DEFAULT_SID);
706     }
707   }
708 
709   /**
710    * Reads a table with the given name from the given pageNumber.
711    */
712   private Table readTable(String name, int pageNumber, boolean useBigIndex)
713     throws IOException
714   {
715     _pageChannel.readPage(_buffer, pageNumber);
716     byte pageType = _buffer.get(0);
717     if (pageType != PageTypes.TABLE_DEF) {
718       throw new IOException("Looking for " + name + " at page " + pageNumber +
719                             ", but page type is " + pageType);
720     }
721     return new Table(this, _buffer, pageNumber, name, useBigIndex);
722   }
723 
724   /**
725    * Creates a Cursor restricted to the given column value if possible (using
726    * an existing index), otherwise a simple table cursor.
727    */
728   private static Cursor createCursorWithOptionalIndex(
729       Table table, String colName, Object colValue)
730     throws IOException
731   {
732     try {
733       return new CursorBuilder(table)
734         .setIndexByColumns(table.getColumn(colName))
735         .setSpecificEntry(colValue)
736         .toCursor();
737     } catch(IllegalArgumentException e) {
738       LOG.info("Could not find expected index on table " + table.getName());
739     }
740     // use table scan instead
741     return Cursor.createCursor(table);
742   }
743   
744   /**
745    * Copy an existing JDBC ResultSet into a new table in this database
746    * @param name Name of the new table to create
747    * @param source ResultSet to copy from
748    */
749   public void copyTable(String name, ResultSet source)
750     throws SQLException, IOException
751   {
752     copyTable(name, source, SimpleImportFilter.INSTANCE);
753   }
754   
755   /**
756    * Copy an existing JDBC ResultSet into a new table in this database
757    * @param name Name of the new table to create
758    * @param source ResultSet to copy from
759    * @param filter valid import filter
760    */
761   public void copyTable(String name, ResultSet source, ImportFilter filter)
762     throws SQLException, IOException
763   {
764     ResultSetMetaData md = source.getMetaData();
765     List<Column> columns = new LinkedList<Column>();
766     for (int i = 1; i <= md.getColumnCount(); i++) {
767       Column column = new Column();
768       column.setName(escape(md.getColumnName(i)));
769       int lengthInUnits = md.getColumnDisplaySize(i);
770       column.setSQLType(md.getColumnType(i), lengthInUnits);
771       DataType type = column.getType();
772       // we check for isTrueVariableLength here to avoid setting the length
773       // for a NUMERIC column, which pretends to be var-len, even though it
774       // isn't
775       if(type.isTrueVariableLength() && !type.isLongValue()) {
776         column.setLengthInUnits((short)lengthInUnits);
777       }
778       if(type.getHasScalePrecision()) {
779         int scale = md.getScale(i);
780         int precision = md.getPrecision(i);
781         if(type.isValidScale(scale)) {
782           column.setScale((byte)scale);
783         }
784         if(type.isValidPrecision(precision)) {
785           column.setPrecision((byte)precision);
786         }
787       }
788       columns.add(column);
789     }
790     createTable(escape(name), filter.filterColumns(columns, md));
791     Table table = getTable(escape(name));
792     List<Object[]> rows = new ArrayList<Object[]>(COPY_TABLE_BATCH_SIZE);
793     while (source.next()) {
794       Object[] row = new Object[md.getColumnCount()];
795       for (int i = 0; i < row.length; i++) {
796         row[i] = source.getObject(i + 1);
797       }
798       rows.add(filter.filterRow(row));
799       if (rows.size() == COPY_TABLE_BATCH_SIZE) {
800         table.addRows(rows);
801         rows.clear();
802       }
803     }
804     if (rows.size() > 0) {
805       table.addRows(rows);
806     }
807   }
808   
809   /**
810    * Copy a delimited text file into a new table in this database
811    * @param name Name of the new table to create
812    * @param f Source file to import
813    * @param delim Regular expression representing the delimiter string.
814    */
815   public void importFile(String name, File f, String delim)
816     throws IOException
817   {
818     importFile(name, f, delim, SimpleImportFilter.INSTANCE);
819   }
820 
821   /**
822    * Copy a delimited text file into a new table in this database
823    * @param name Name of the new table to create
824    * @param f Source file to import
825    * @param delim Regular expression representing the delimiter string.
826    * @param filter valid import filter
827    */
828   public void importFile(String name, File f, String delim,
829                          ImportFilter filter)
830     throws IOException
831   {
832     BufferedReader in = null;
833     try {
834       in = new BufferedReader(new FileReader(f));
835       importReader(name, in, delim, filter);
836     } finally {
837       if (in != null) {
838         try {
839           in.close();
840         } catch (IOException ex) {
841           LOG.warn("Could not close file " + f.getAbsolutePath(), ex);
842         }
843       }
844     }
845   }
846 
847   /**
848    * Copy a delimited text file into a new table in this database
849    * @param name Name of the new table to create
850    * @param in Source reader to import
851    * @param delim Regular expression representing the delimiter string.
852    */
853   public void importReader(String name, BufferedReader in, String delim)
854     throws IOException
855   {
856     importReader(name, in, delim, SimpleImportFilter.INSTANCE);
857   }
858   
859   /**
860    * Copy a delimited text file into a new table in this database
861    * @param name Name of the new table to create
862    * @param in Source reader to import
863    * @param delim Regular expression representing the delimiter string.
864    * @param filter valid import filter
865    */
866   public void importReader(String name, BufferedReader in, String delim,
867                            ImportFilter filter)
868     throws IOException
869   {
870     String line = in.readLine();
871     if (line == null || line.trim().length() == 0) {
872       return;
873     }
874 
875     String tableName = escape(name);
876     int counter = 0;
877     while(getTable(tableName) != null) {
878       tableName = escape(name + (counter++));
879     }
880 
881     List<Column> columns = new LinkedList<Column>();
882     String[] columnNames = line.split(delim);
883       
884     for (int i = 0; i < columnNames.length; i++) {
885       columns.add(new ColumnBuilder(escape(columnNames[i]), DataType.TEXT)
886                   .setLength((short)DataType.TEXT.getMaxSize())
887                   .toColumn());
888     }
889 
890     try {
891       createTable(tableName, filter.filterColumns(columns, null));
892       Table table = getTable(tableName);
893       List<Object[]> rows = new ArrayList<Object[]>(COPY_TABLE_BATCH_SIZE);
894       
895       while ((line = in.readLine()) != null)
896       {
897         // 
898         // Handle the situation where the end of the line
899         // may have null fields.  We always want to add the
900         // same number of columns to the table each time.
901         //
902         Object[] data = Table.dupeRow(line.split(delim), columnNames.length);
903         rows.add(filter.filterRow(data));
904         if (rows.size() == COPY_TABLE_BATCH_SIZE) {
905           table.addRows(rows);
906           rows.clear();
907         }
908       }
909       if (rows.size() > 0) {
910         table.addRows(rows);
911       }
912     } catch(SQLException e) {
913       throw (IOException)new IOException(e.getMessage()).initCause(e);
914     }
915   }
916 
917   /**
918    * Flushes any current changes to the database file to disk.
919    */
920   public void flush() throws IOException {
921     _pageChannel.flush();
922   }
923   
924   /**
925    * Close the database file
926    */
927   public void close() throws IOException {
928     _pageChannel.close();
929   }
930   
931   /**
932    * @return A table or column name escaped for Access
933    */
934   private String escape(String s) {
935     if (isReservedWord(s)) {
936       return ESCAPE_PREFIX + s; 
937     }
938     return s;
939   }
940 
941   /**
942    * @return {@code true} if the given string is a reserved word,
943    *         {@code false} otherwise
944    */
945   public static boolean isReservedWord(String s) {
946     return RESERVED_WORDS.contains(s.toLowerCase());
947   }
948   
949   @Override
950   public String toString() {
951     return ToStringBuilder.reflectionToString(this);
952   }
953 
954   /**
955    * Adds a table to the _tableLookup and resets the _tableNames set
956    */
957   private void addTable(String tableName, Integer pageNumber)
958   {
959     _tableLookup.put(toLookupTableName(tableName),
960                      new TableInfo(pageNumber, tableName));
961     // clear this, will be created next time needed
962     _tableNames = null;
963   }
964 
965   /**
966    * @returns the tableInfo of the given table, if any
967    */
968   private TableInfo lookupTable(String tableName) {
969     return _tableLookup.get(toLookupTableName(tableName));
970   }
971 
972   /**
973    * @return a string usable in the _tableLookup map.
974    */
975   private String toLookupTableName(String tableName) {
976     return ((tableName != null) ? tableName.toUpperCase() : null);
977   }
978 
979   /**
980    * Returns {@code true} if "big index support" has been enabled explicity on
981    * the this Database or via a system property, {@code false} otherwise.
982    */
983   public boolean defaultUseBigIndex() {
984     return doUseBigIndex() || Boolean.getBoolean(USE_BIG_INDEX_PROPERTY);
985   }
986   
987   /**
988    * Utility class for storing table page number and actual name.
989    */
990   private static class TableInfo
991   {
992     public final Integer pageNumber;
993     public final String tableName;
994 
995     private TableInfo(Integer newPageNumber,
996                       String newTableName) {
997       pageNumber = newPageNumber;
998       tableName = newTableName;
999     }
1000   }
1001 
1002   /**
1003    * Table iterator for this database, unmodifiable.
1004    */
1005   private class TableIterator implements Iterator<Table>
1006   {
1007     private Iterator<String> _tableNameIter;
1008 
1009     private TableIterator() {
1010       _tableNameIter = getTableNames().iterator();
1011     }
1012 
1013     public boolean hasNext() {
1014       return _tableNameIter.hasNext();
1015     }
1016 
1017     public void remove() {
1018       throw new UnsupportedOperationException();
1019     }
1020 
1021     public Table next() {
1022       if(!hasNext()) {
1023         throw new NoSuchElementException();
1024       }
1025       try {
1026         return getTable(_tableNameIter.next());
1027       } catch(IOException e) {
1028         throw new IllegalStateException(e);
1029       }
1030     }
1031   }
1032   
1033 }