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