Java tutorial
/* Copyright (c) 2005 Health Market Science, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA You can contact Health Market Science at info@healthmarketscience.com or at the following address: Health Market Science 2700 Horizon Drive Suite 200 King of Prussia, PA 19406 */ package com.healthmarketscience.jackcess.impl; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.TimeZone; import java.util.TreeSet; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.Cursor; import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.IndexCursor; import com.healthmarketscience.jackcess.PropertyMap; import com.healthmarketscience.jackcess.Relationship; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.RuntimeIOException; import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.impl.query.QueryImpl; import com.healthmarketscience.jackcess.query.Query; import com.healthmarketscience.jackcess.util.CaseInsensitiveColumnMatcher; import com.healthmarketscience.jackcess.util.ColumnValidatorFactory; import com.healthmarketscience.jackcess.util.ErrorHandler; import com.healthmarketscience.jackcess.util.LinkResolver; import com.healthmarketscience.jackcess.util.SimpleColumnValidatorFactory; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author Tim McCune * @usage _general_class_ */ public class DatabaseImpl implements Database { private static final Log LOG = LogFactory.getLog(DatabaseImpl.class); /** this is the default "userId" used if we cannot find existing info. this seems to be some standard "Admin" userId for access files */ private static final byte[] SYS_DEFAULT_SID = new byte[2]; static { SYS_DEFAULT_SID[0] = (byte) 0xA6; SYS_DEFAULT_SID[1] = (byte) 0x33; } /** the default value for the resource path used to load classpath * resources. */ public static final String DEFAULT_RESOURCE_PATH = "com/healthmarketscience/jackcess/"; /** the resource path to be used when loading classpath resources */ static final String RESOURCE_PATH = System.getProperty(RESOURCE_PATH_PROPERTY, DEFAULT_RESOURCE_PATH); /** whether or not this jvm has "broken" nio support */ static final boolean BROKEN_NIO = Boolean.TRUE.toString() .equalsIgnoreCase(System.getProperty(BROKEN_NIO_PROPERTY)); /** additional internal details about each FileFormat */ private static final Map<Database.FileFormat, FileFormatDetails> FILE_FORMAT_DETAILS = new EnumMap<Database.FileFormat, FileFormatDetails>( Database.FileFormat.class); static { addFileFormatDetails(FileFormat.V1997, null, JetFormat.VERSION_3); addFileFormatDetails(FileFormat.V2000, "empty", JetFormat.VERSION_4); addFileFormatDetails(FileFormat.V2003, "empty2003", JetFormat.VERSION_4); addFileFormatDetails(FileFormat.V2007, "empty2007", JetFormat.VERSION_12); addFileFormatDetails(FileFormat.V2010, "empty2010", JetFormat.VERSION_14); addFileFormatDetails(FileFormat.MSISAM, null, JetFormat.VERSION_MSISAM); } /** System catalog always lives on page 2 */ private static final int PAGE_SYSTEM_CATALOG = 2; /** Name of the system catalog */ private static final String TABLE_SYSTEM_CATALOG = "MSysObjects"; /** this is the access control bit field for created tables. the value used is equivalent to full access (Visual Basic DAO PermissionEnum constant: dbSecFullAccess) */ private static final Integer SYS_FULL_ACCESS_ACM = 1048575; /** ACE table column name of the actual access control entry */ private static final String ACE_COL_ACM = "ACM"; /** ACE table column name of the inheritable attributes flag */ private static final String ACE_COL_F_INHERITABLE = "FInheritable"; /** ACE table column name of the relevant objectId */ private static final String ACE_COL_OBJECT_ID = "ObjectId"; /** ACE table column name of the relevant userId */ private static final String ACE_COL_SID = "SID"; /** Relationship table column name of the column count */ private static final String REL_COL_COLUMN_COUNT = "ccolumn"; /** Relationship table column name of the flags */ private static final String REL_COL_FLAGS = "grbit"; /** Relationship table column name of the index of the columns */ private static final String REL_COL_COLUMN_INDEX = "icolumn"; /** Relationship table column name of the "to" column name */ private static final String REL_COL_TO_COLUMN = "szColumn"; /** Relationship table column name of the "to" table name */ private static final String REL_COL_TO_TABLE = "szObject"; /** Relationship table column name of the "from" column name */ private static final String REL_COL_FROM_COLUMN = "szReferencedColumn"; /** Relationship table column name of the "from" table name */ private static final String REL_COL_FROM_TABLE = "szReferencedObject"; /** Relationship table column name of the relationship */ private static final String REL_COL_NAME = "szRelationship"; /** System catalog column name of the page on which system object definitions are stored */ private static final String CAT_COL_ID = "Id"; /** System catalog column name of the name of a system object */ private static final String CAT_COL_NAME = "Name"; private static final String CAT_COL_OWNER = "Owner"; /** System catalog column name of a system object's parent's id */ private static final String CAT_COL_PARENT_ID = "ParentId"; /** System catalog column name of the type of a system object */ private static final String CAT_COL_TYPE = "Type"; /** System catalog column name of the date a system object was created */ private static final String CAT_COL_DATE_CREATE = "DateCreate"; /** System catalog column name of the date a system object was updated */ private static final String CAT_COL_DATE_UPDATE = "DateUpdate"; /** System catalog column name of the flags column */ private static final String CAT_COL_FLAGS = "Flags"; /** System catalog column name of the properties column */ static final String CAT_COL_PROPS = "LvProp"; /** System catalog column name of the remote database */ private static final String CAT_COL_DATABASE = "Database"; /** System catalog column name of the remote table name */ private static final String CAT_COL_FOREIGN_NAME = "ForeignName"; /** top-level parentid for a database */ private static final int DB_PARENT_ID = 0xF000000; /** the maximum size of any of the included "empty db" resources */ private static final long MAX_EMPTYDB_SIZE = 350000L; /** this object is a "system" object */ static final int SYSTEM_OBJECT_FLAG = 0x80000000; /** this object is another type of "system" object */ static final int ALT_SYSTEM_OBJECT_FLAG = 0x02; /** this object is hidden */ public static final int HIDDEN_OBJECT_FLAG = 0x08; /** all flags which seem to indicate some type of system object */ static final int SYSTEM_OBJECT_FLAGS = SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG; /** read-only channel access mode */ public static final String RO_CHANNEL_MODE = "r"; /** read/write channel access mode */ public static final String RW_CHANNEL_MODE = "rw"; /** Name of the system object that is the parent of all tables */ private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; /** Name of the system object that is the parent of all databases */ private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases"; /** Name of the system object that is the parent of all relationships */ private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = "Relationships"; /** Name of the table that contains system access control entries */ private static final String TABLE_SYSTEM_ACES = "MSysACEs"; /** Name of the table that contains table relationships */ private static final String TABLE_SYSTEM_RELATIONSHIPS = "MSysRelationships"; /** Name of the table that contains queries */ private static final String TABLE_SYSTEM_QUERIES = "MSysQueries"; /** Name of the table that contains complex type information */ private static final String TABLE_SYSTEM_COMPLEX_COLS = "MSysComplexColumns"; /** Name of the main database properties object */ private static final String OBJECT_NAME_DB_PROPS = "MSysDb"; /** Name of the summary properties object */ private static final String OBJECT_NAME_SUMMARY_PROPS = "SummaryInfo"; /** Name of the user-defined properties object */ private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined"; /** System object type for table definitions */ static final Short TYPE_TABLE = 1; /** System object type for query definitions */ private static final Short TYPE_QUERY = 5; /** System object type for linked table definitions */ private static final Short TYPE_LINKED_TABLE = 6; /** max number of table lookups to cache */ private static final int MAX_CACHED_LOOKUP_TABLES = 50; /** the columns to read when reading system catalog normally */ private static Collection<String> SYSTEM_CATALOG_COLUMNS = new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, CAT_COL_FLAGS, CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME)); /** the columns to read when finding table names */ private static Collection<String> SYSTEM_CATALOG_TABLE_NAME_COLUMNS = new HashSet<String>( Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID, CAT_COL_FLAGS, CAT_COL_PARENT_ID)); /** the columns to read when getting object propertyes */ private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS = new HashSet<String>( Arrays.asList(CAT_COL_ID, CAT_COL_PROPS)); /** the File of the database */ private final File _file; /** Buffer to hold database pages */ private ByteBuffer _buffer; /** ID of the Tables system object */ private Integer _tableParentId; /** Format that the containing database is in */ private final JetFormat _format; /** * Cache map of UPPERCASE table names to page numbers containing their * definition and their stored table name (max size * MAX_CACHED_LOOKUP_TABLES). */ private final Map<String, TableInfo> _tableLookup = new LinkedHashMap<String, TableInfo>() { private static final long serialVersionUID = 0L; @Override protected boolean removeEldestEntry(Map.Entry<String, TableInfo> e) { return (size() > MAX_CACHED_LOOKUP_TABLES); } }; /** set of table names as stored in the mdb file, created on demand */ private Set<String> _tableNames; /** Reads and writes database pages */ private final PageChannel _pageChannel; /** System catalog table */ private TableImpl _systemCatalog; /** utility table finder */ private TableFinder _tableFinder; /** System access control entries table (initialized on first use) */ private TableImpl _accessControlEntries; /** System relationships table (initialized on first use) */ private TableImpl _relationships; /** System queries table (initialized on first use) */ private TableImpl _queries; /** System complex columns table (initialized on first use) */ private TableImpl _complexCols; /** SIDs to use for the ACEs added for new tables */ private final List<byte[]> _newTableSIDs = new ArrayList<byte[]>(); /** optional error handler to use when row errors are encountered */ private ErrorHandler _dbErrorHandler; /** the file format of the database */ private FileFormat _fileFormat; /** charset to use when handling text */ private Charset _charset; /** timezone to use when handling dates */ private TimeZone _timeZone; /** language sort order to be used for textual columns */ private ColumnImpl.SortOrder _defaultSortOrder; /** default code page to be used for textual columns (in some dbs) */ private Short _defaultCodePage; /** the ordering used for table columns */ private Table.ColumnOrder _columnOrder; /** whether or not enforcement of foreign-keys is enabled */ private boolean _enforceForeignKeys; /** factory for ColumnValidators */ private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; /** cache of in-use tables */ private final TableCache _tableCache = new TableCache(); /** handler for reading/writing properteies */ private PropertyMaps.Handler _propsHandler; /** ID of the Databases system object */ private Integer _dbParentId; /** core database properties */ private PropertyMaps _dbPropMaps; /** summary properties */ private PropertyMaps _summaryPropMaps; /** user-defined properties */ private PropertyMaps _userDefPropMaps; /** linked table resolver */ private LinkResolver _linkResolver; /** any linked databases which have been opened */ private Map<String, Database> _linkedDbs; /** shared state used when enforcing foreign keys */ private final FKEnforcer.SharedState _fkEnforcerSharedState = FKEnforcer.initSharedState(); /** Calendar for use interpreting dates/times in Columns */ private Calendar _calendar; /** * Open an existing Database. If the existing file is not writeable or the * readOnly flag is {@code true}, the file will be opened read-only. * @param mdbFile File containing the database * @param readOnly iff {@code true}, force opening file in read-only * mode * @param channel pre-opened FileChannel. if provided explicitly, it will * not be closed by this Database instance * @param autoSync whether or not to enable auto-syncing on write. if * {@code true}, writes will be immediately flushed to disk. * This leaves the database in a (fairly) consistent state * on each write, but can be very inefficient for many * updates. if {@code false}, flushing to disk happens at * the jvm's leisure, which can be much faster, but may * leave the database in an inconsistent state if failures * are encountered during writing. Writes may be flushed at * any time using {@link #flush}. * @param charset Charset to use, if {@code null}, uses default * @param timeZone TimeZone to use, if {@code null}, uses default * @param provider CodecProvider for handling page encoding/decoding, may be * {@code null} if no special encoding is necessary * @usage _advanced_method_ */ public static DatabaseImpl open(File mdbFile, boolean readOnly, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone, CodecProvider provider) throws IOException { boolean closeChannel = false; if (channel == null) { if (!mdbFile.exists() || !mdbFile.canRead()) { throw new FileNotFoundException("given file does not exist: " + mdbFile); } // force read-only for non-writable files readOnly |= !mdbFile.canWrite(); // open file channel channel = openChannel(mdbFile, readOnly); closeChannel = true; } boolean success = false; try { if (!readOnly) { // verify that format supports writing JetFormat jetFormat = JetFormat.getFormat(channel); if (jetFormat.READ_ONLY) { throw new IOException("jet format '" + jetFormat + "' does not support writing"); } } DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, null, charset, timeZone, provider); success = true; return db; } finally { if (!success && closeChannel) { // something blew up, shutdown the channel (quietly) ByteUtil.closeQuietly(channel); } } } /** * Create a new Database for the given fileFormat * @param fileFormat version of new database. * @param mdbFile Location to write the new database to. <b>If this file * already exists, it will be overwritten.</b> * @param channel pre-opened FileChannel. if provided explicitly, it will * not be closed by this Database instance * @param autoSync whether or not to enable auto-syncing on write. if * {@code true}, writes will be immediately flushed to disk. * This leaves the database in a (fairly) consistent state * on each write, but can be very inefficient for many * updates. if {@code false}, flushing to disk happens at * the jvm's leisure, which can be much faster, but may * leave the database in an inconsistent state if failures * are encountered during writing. Writes may be flushed at * any time using {@link #flush}. * @param charset Charset to use, if {@code null}, uses default * @param timeZone TimeZone to use, if {@code null}, uses default * @usage _advanced_method_ */ public static DatabaseImpl create(FileFormat fileFormat, File mdbFile, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone) throws IOException { FileFormatDetails details = getFileFormatDetails(fileFormat); if (details.getFormat().READ_ONLY) { throw new IOException("file format " + fileFormat + " does not support writing"); } boolean closeChannel = false; if (channel == null) { channel = openChannel(mdbFile, false); closeChannel = true; } boolean success = false; try { channel.truncate(0); transferFrom(channel, getResourceAsStream(details.getEmptyFilePath())); channel.force(true); DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync, fileFormat, charset, timeZone, null); success = true; return db; } finally { if (!success && closeChannel) { // something blew up, shutdown the channel (quietly) ByteUtil.closeQuietly(channel); } } } /** * Package visible only to support unit tests via DatabaseTest.openChannel(). * @param mdbFile file to open * @param readOnly true if read-only * @return a FileChannel on the given file. * @exception FileNotFoundException * if the mode is <tt>"r"</tt> but the given file object does * not denote an existing regular file, or if the mode begins * with <tt>"rw"</tt> but the given file object does not denote * an existing, writable regular file and a new regular file of * that name cannot be created, or if some other error occurs * while opening or creating the file */ static FileChannel openChannel(final File mdbFile, final boolean readOnly) throws FileNotFoundException { final String mode = (readOnly ? RO_CHANNEL_MODE : RW_CHANNEL_MODE); return new RandomAccessFile(mdbFile, mode).getChannel(); } /** * Create a new database by reading it in from a FileChannel. * @param file the File to which the channel is connected * @param channel File channel of the database. This needs to be a * FileChannel instead of a ReadableByteChannel because we need to * randomly jump around to various points in the file. * @param autoSync whether or not to enable auto-syncing on write. if * {@code true}, writes will be immediately flushed to disk. * This leaves the database in a (fairly) consistent state * on each write, but can be very inefficient for many * updates. if {@code false}, flushing to disk happens at * the jvm's leisure, which can be much faster, but may * leave the database in an inconsistent state if failures * are encountered during writing. Writes may be flushed at * any time using {@link #flush}. * @param fileFormat version of new database (if known) * @param charset Charset to use, if {@code null}, uses default * @param timeZone TimeZone to use, if {@code null}, uses default */ protected DatabaseImpl(File file, FileChannel channel, boolean closeChannel, boolean autoSync, FileFormat fileFormat, Charset charset, TimeZone timeZone, CodecProvider provider) throws IOException { _file = file; _format = JetFormat.getFormat(channel); _charset = ((charset == null) ? getDefaultCharset(_format) : charset); _columnOrder = getDefaultColumnOrder(); _enforceForeignKeys = getDefaultEnforceForeignKeys(); _fileFormat = fileFormat; _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); if (provider == null) { provider = DefaultCodecProvider.INSTANCE; } // note, it's slighly sketchy to pass ourselves along partially // constructed, but only our _format and _pageChannel refs should be // needed _pageChannel.initialize(this, provider); _buffer = _pageChannel.createPageBuffer(); readSystemCatalog(); } public File getFile() { return _file; } /** * @usage _advanced_method_ */ public PageChannel getPageChannel() { return _pageChannel; } /** * @usage _advanced_method_ */ public JetFormat getFormat() { return _format; } /** * @return The system catalog table * @usage _advanced_method_ */ public TableImpl getSystemCatalog() { return _systemCatalog; } /** * @return The system Access Control Entries table (loaded on demand) * @usage _advanced_method_ */ public TableImpl getAccessControlEntries() throws IOException { if (_accessControlEntries == null) { _accessControlEntries = getSystemTable(TABLE_SYSTEM_ACES); if (_accessControlEntries == null) { throw new IOException("Could not find system table " + TABLE_SYSTEM_ACES); } } return _accessControlEntries; } /** * @return the complex column system table (loaded on demand) * @usage _advanced_method_ */ public TableImpl getSystemComplexColumns() throws IOException { if (_complexCols == null) { _complexCols = getSystemTable(TABLE_SYSTEM_COMPLEX_COLS); if (_complexCols == null) { throw new IOException("Could not find system table " + TABLE_SYSTEM_COMPLEX_COLS); } } return _complexCols; } public ErrorHandler getErrorHandler() { return ((_dbErrorHandler != null) ? _dbErrorHandler : ErrorHandler.DEFAULT); } public void setErrorHandler(ErrorHandler newErrorHandler) { _dbErrorHandler = newErrorHandler; } public LinkResolver getLinkResolver() { return ((_linkResolver != null) ? _linkResolver : LinkResolver.DEFAULT); } public void setLinkResolver(LinkResolver newLinkResolver) { _linkResolver = newLinkResolver; } public Map<String, Database> getLinkedDatabases() { return ((_linkedDbs == null) ? Collections.<String, Database>emptyMap() : Collections.unmodifiableMap(_linkedDbs)); } public TimeZone getTimeZone() { return _timeZone; } public void setTimeZone(TimeZone newTimeZone) { if (newTimeZone == null) { newTimeZone = getDefaultTimeZone(); } _timeZone = newTimeZone; // clear cached calendar when timezone is changed _calendar = null; } public Charset getCharset() { return _charset; } public void setCharset(Charset newCharset) { if (newCharset == null) { newCharset = getDefaultCharset(getFormat()); } _charset = newCharset; } public Table.ColumnOrder getColumnOrder() { return _columnOrder; } public void setColumnOrder(Table.ColumnOrder newColumnOrder) { if (newColumnOrder == null) { newColumnOrder = getDefaultColumnOrder(); } _columnOrder = newColumnOrder; } public boolean isEnforceForeignKeys() { return _enforceForeignKeys; } public void setEnforceForeignKeys(Boolean newEnforceForeignKeys) { if (newEnforceForeignKeys == null) { newEnforceForeignKeys = getDefaultEnforceForeignKeys(); } _enforceForeignKeys = newEnforceForeignKeys; } public ColumnValidatorFactory getColumnValidatorFactory() { return _validatorFactory; } public void setColumnValidatorFactory(ColumnValidatorFactory newFactory) { if (newFactory == null) { newFactory = SimpleColumnValidatorFactory.INSTANCE; } _validatorFactory = newFactory; } /** * @usage _advanced_method_ */ FKEnforcer.SharedState getFKEnforcerSharedState() { return _fkEnforcerSharedState; } /** * @usage _advanced_method_ */ Calendar getCalendar() { if (_calendar == null) { _calendar = Calendar.getInstance(_timeZone); } return _calendar; } /** * @returns the current handler for reading/writing properties, creating if * necessary */ private PropertyMaps.Handler getPropsHandler() { if (_propsHandler == null) { _propsHandler = new PropertyMaps.Handler(this); } return _propsHandler; } public FileFormat getFileFormat() throws IOException { if (_fileFormat == null) { Map<String, FileFormat> possibleFileFormats = getFormat().getPossibleFileFormats(); if (possibleFileFormats.size() == 1) { // single possible format (null key), easy enough _fileFormat = possibleFileFormats.get(null); } else { // need to check the "AccessVersion" property String accessVersion = (String) getDatabaseProperties().getValue(PropertyMap.ACCESS_VERSION_PROP); _fileFormat = possibleFileFormats.get(accessVersion); if (_fileFormat == null) { throw new IllegalStateException("Could not determine FileFormat"); } } } return _fileFormat; } /** * @return a (possibly cached) page ByteBuffer for internal use. the * returned buffer should be released using * {@link #releaseSharedBuffer} when no longer in use */ private ByteBuffer takeSharedBuffer() { // we try to re-use a single shared _buffer, but occassionally, it may be // needed by multiple operations at the same time (e.g. loading a // secondary table while loading a primary table). this method ensures // that we don't corrupt the _buffer, but instead force the second caller // to use a new buffer. if (_buffer != null) { ByteBuffer curBuffer = _buffer; _buffer = null; return curBuffer; } return _pageChannel.createPageBuffer(); } /** * Relinquishes use of a page ByteBuffer returned by * {@link #takeSharedBuffer}. */ private void releaseSharedBuffer(ByteBuffer buffer) { // we always stuff the returned buffer back into _buffer. it doesn't // really matter if multiple values over-write, at the end of the day, we // just need one shared buffer _buffer = buffer; } /** * @return the currently configured database default language sort order for * textual columns * @usage _intermediate_method_ */ public ColumnImpl.SortOrder getDefaultSortOrder() throws IOException { if (_defaultSortOrder == null) { initRootPageInfo(); } return _defaultSortOrder; } /** * @return the currently configured database default code page for textual * data (may not be relevant to all database versions) * @usage _intermediate_method_ */ public short getDefaultCodePage() throws IOException { if (_defaultCodePage == null) { initRootPageInfo(); } return _defaultCodePage; } /** * Reads various config info from the db page 0. */ private void initRootPageInfo() throws IOException { ByteBuffer buffer = takeSharedBuffer(); try { _pageChannel.readPage(buffer, 0); _defaultSortOrder = ColumnImpl.readSortOrder(buffer, _format.OFFSET_SORT_ORDER, _format); _defaultCodePage = buffer.getShort(_format.OFFSET_CODE_PAGE); } finally { releaseSharedBuffer(buffer); } } /** * @return a PropertyMaps instance decoded from the given bytes (always * returns non-{@code null} result). * @usage _intermediate_method_ */ public PropertyMaps readProperties(byte[] propsBytes, int objectId, RowIdImpl rowId) throws IOException { return getPropsHandler().read(propsBytes, objectId, rowId); } /** * Read the system catalog */ private void readSystemCatalog() throws IOException { _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG, SYSTEM_OBJECT_FLAGS); try { _tableFinder = new DefaultTableFinder( _systemCatalog.newCursor().setIndexByColumnNames(CAT_COL_PARENT_ID, CAT_COL_NAME) .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE).toIndexCursor()); } catch (IllegalArgumentException e) { LOG.info("Could not find expected index on table " + _systemCatalog.getName()); // use table scan instead _tableFinder = new FallbackTableFinder( _systemCatalog.newCursor().setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE).toCursor()); } _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID, SYSTEM_OBJECT_NAME_TABLES); if (_tableParentId == null) { throw new IOException("Did not find required parent table id"); } if (LOG.isDebugEnabled()) { LOG.debug("Finished reading system catalog. Tables: " + getTableNames()); } } public Set<String> getTableNames() throws IOException { if (_tableNames == null) { Set<String> tableNames = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); _tableFinder.getTableNames(tableNames, false); _tableNames = tableNames; } return _tableNames; } public Set<String> getSystemTableNames() throws IOException { Set<String> sysTableNames = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); _tableFinder.getTableNames(sysTableNames, true); return sysTableNames; } public Iterator<Table> iterator() { return new TableIterator(); } public TableImpl getTable(String name) throws IOException { return getTable(name, false); } /** * @param tableDefPageNumber the page number of a table definition * @return The table, or null if it doesn't exist * @usage _advanced_method_ */ public TableImpl getTable(int tableDefPageNumber) throws IOException { // first, check for existing table TableImpl table = _tableCache.get(tableDefPageNumber); if (table != null) { return table; } // lookup table info from system catalog Row objectRow = _tableFinder.getObjectRow(tableDefPageNumber, SYSTEM_CATALOG_COLUMNS); if (objectRow == null) { return null; } String name = objectRow.getString(CAT_COL_NAME); int flags = objectRow.getInt(CAT_COL_FLAGS); return readTable(name, tableDefPageNumber, flags); } /** * @param name Table name * @param includeSystemTables whether to consider returning a system table * @return The table, or null if it doesn't exist */ private TableImpl getTable(String name, boolean includeSystemTables) throws IOException { TableInfo tableInfo = lookupTable(name); if ((tableInfo == null) || (tableInfo.pageNumber == null)) { return null; } if (!includeSystemTables && isSystemObject(tableInfo.flags)) { return null; } if (tableInfo.isLinked()) { if (_linkedDbs == null) { _linkedDbs = new HashMap<String, Database>(); } String linkedDbName = ((LinkedTableInfo) tableInfo).linkedDbName; String linkedTableName = ((LinkedTableInfo) tableInfo).linkedTableName; Database linkedDb = _linkedDbs.get(linkedDbName); if (linkedDb == null) { linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName); _linkedDbs.put(linkedDbName, linkedDb); } return ((DatabaseImpl) linkedDb).getTable(linkedTableName, includeSystemTables); } return readTable(tableInfo.tableName, tableInfo.pageNumber, tableInfo.flags); } /** * Create a new table in this database * @param name Name of the table to create * @param columns List of Columns in the table * @usage _general_method_ */ public void createTable(String name, List<ColumnBuilder> columns) throws IOException { createTable(name, columns, null); } /** * Create a new table in this database * @param name Name of the table to create * @param columns List of Columns in the table * @param indexes List of IndexBuilders describing indexes for the table * @usage _general_method_ */ public void createTable(String name, List<ColumnBuilder> columns, List<IndexBuilder> indexes) throws IOException { if (lookupTable(name) != null) { throw new IllegalArgumentException("Cannot create table with name of existing table"); } new TableCreator(this, name, columns, indexes).createTable(); } public void createLinkedTable(String name, String linkedDbName, String linkedTableName) throws IOException { if (lookupTable(name) != null) { throw new IllegalArgumentException("Cannot create linked table with name of existing table"); } validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); validateIdentifierName(linkedDbName, DataType.MEMO.getMaxSize(), "linked database"); validateIdentifierName(linkedTableName, getFormat().MAX_TABLE_NAME_LENGTH, "linked table"); getPageChannel().startWrite(); try { int linkedTableId = _tableFinder.getNextFreeSyntheticId(); addNewTable(name, linkedTableId, TYPE_LINKED_TABLE, linkedDbName, linkedTableName); } finally { getPageChannel().finishWrite(); } } /** * Adds a newly created table to the relevant internal database structures. */ void addNewTable(String name, int tdefPageNumber, Short type, String linkedDbName, String linkedTableName) throws IOException { //Add this table to our internal list. addTable(name, Integer.valueOf(tdefPageNumber), type, linkedDbName, linkedTableName); //Add this table to system tables addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, linkedTableName); addToAccessControlEntries(tdefPageNumber); } public List<Relationship> getRelationships(Table table1, Table table2) throws IOException { return getRelationships((TableImpl) table1, (TableImpl) table2); } public List<Relationship> getRelationships(TableImpl table1, TableImpl table2) throws IOException { int nameCmp = table1.getName().compareTo(table2.getName()); if (nameCmp == 0) { throw new IllegalArgumentException("Must provide two different tables"); } if (nameCmp > 0) { // we "order" the two tables given so that we will return a collection // of relationships in the same order regardless of whether we are given // (TableFoo, TableBar) or (TableBar, TableFoo). TableImpl tmp = table1; table1 = table2; table2 = tmp; } return getRelationshipsImpl(table1, table2, true); } public List<Relationship> getRelationships(Table table) throws IOException { if (table == null) { throw new IllegalArgumentException("Must provide a table"); } // since we are getting relationships specific to certain table include // all tables return getRelationshipsImpl((TableImpl) table, null, true); } public List<Relationship> getRelationships() throws IOException { return getRelationshipsImpl(null, null, false); } public List<Relationship> getSystemRelationships() throws IOException { return getRelationshipsImpl(null, null, true); } private List<Relationship> getRelationshipsImpl(TableImpl table1, TableImpl table2, boolean includeSystemTables) throws IOException { // the relationships table does not get loaded until first accessed if (_relationships == null) { _relationships = getSystemTable(TABLE_SYSTEM_RELATIONSHIPS); if (_relationships == null) { throw new IOException("Could not find system relationships table"); } } List<Relationship> relationships = new ArrayList<Relationship>(); if (table1 != null) { Cursor cursor = createCursorWithOptionalIndex(_relationships, REL_COL_FROM_TABLE, table1.getName()); collectRelationships(cursor, table1, table2, relationships, includeSystemTables); cursor = createCursorWithOptionalIndex(_relationships, REL_COL_TO_TABLE, table1.getName()); collectRelationships(cursor, table2, table1, relationships, includeSystemTables); } else { collectRelationships(new CursorBuilder(_relationships).toCursor(), null, null, relationships, includeSystemTables); } return relationships; } public List<Query> getQueries() throws IOException { // the queries table does not get loaded until first accessed if (_queries == null) { _queries = getSystemTable(TABLE_SYSTEM_QUERIES); if (_queries == null) { throw new IOException("Could not find system queries table"); } } // find all the queries from the system catalog List<Row> queryInfo = new ArrayList<Row>(); Map<Integer, List<QueryImpl.Row>> queryRowMap = new HashMap<Integer, List<QueryImpl.Row>>(); for (Row row : CursorImpl.createCursor(_systemCatalog).newIterable() .setColumnNames(SYSTEM_CATALOG_COLUMNS)) { String name = row.getString(CAT_COL_NAME); if (name != null && TYPE_QUERY.equals(row.get(CAT_COL_TYPE))) { queryInfo.add(row); Integer id = row.getInt(CAT_COL_ID); queryRowMap.put(id, new ArrayList<QueryImpl.Row>()); } } // find all the query rows for (Row row : CursorImpl.createCursor(_queries)) { QueryImpl.Row queryRow = new QueryImpl.Row(row); List<QueryImpl.Row> queryRows = queryRowMap.get(queryRow.objectId); if (queryRows == null) { LOG.warn("Found rows for query with id " + queryRow.objectId + " missing from system catalog"); continue; } queryRows.add(queryRow); } // lastly, generate all the queries List<Query> queries = new ArrayList<Query>(); for (Row row : queryInfo) { String name = row.getString(CAT_COL_NAME); Integer id = row.getInt(CAT_COL_ID); int flags = row.getInt(CAT_COL_FLAGS); List<QueryImpl.Row> queryRows = queryRowMap.get(id); queries.add(QueryImpl.create(flags, name, queryRows, id)); } return queries; } public TableImpl getSystemTable(String tableName) throws IOException { return getTable(tableName, true); } public PropertyMap getDatabaseProperties() throws IOException { if (_dbPropMaps == null) { _dbPropMaps = getPropertiesForDbObject(OBJECT_NAME_DB_PROPS); } return _dbPropMaps.getDefault(); } public PropertyMap getSummaryProperties() throws IOException { if (_summaryPropMaps == null) { _summaryPropMaps = getPropertiesForDbObject(OBJECT_NAME_SUMMARY_PROPS); } return _summaryPropMaps.getDefault(); } public PropertyMap getUserDefinedProperties() throws IOException { if (_userDefPropMaps == null) { _userDefPropMaps = getPropertiesForDbObject(OBJECT_NAME_USERDEF_PROPS); } return _userDefPropMaps.getDefault(); } /** * @return the PropertyMaps for the object with the given id * @usage _advanced_method_ */ public PropertyMaps getPropertiesForObject(int objectId) throws IOException { Row objectRow = _tableFinder.getObjectRow(objectId, SYSTEM_CATALOG_PROPS_COLUMNS); byte[] propsBytes = null; RowIdImpl rowId = null; if (objectRow != null) { propsBytes = objectRow.getBytes(CAT_COL_PROPS); rowId = (RowIdImpl) objectRow.getId(); } return readProperties(propsBytes, objectId, rowId); } /** * @return property group for the given "database" object */ private PropertyMaps getPropertiesForDbObject(String dbName) throws IOException { if (_dbParentId == null) { // need the parent if of the databases objects _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, SYSTEM_OBJECT_NAME_DATABASES); if (_dbParentId == null) { throw new IOException("Did not find required parent db id"); } } Row objectRow = _tableFinder.getObjectRow(_dbParentId, dbName, SYSTEM_CATALOG_PROPS_COLUMNS); byte[] propsBytes = null; int objectId = -1; RowIdImpl rowId = null; if (objectRow != null) { propsBytes = objectRow.getBytes(CAT_COL_PROPS); objectId = objectRow.getInt(CAT_COL_ID); rowId = (RowIdImpl) objectRow.getId(); } return readProperties(propsBytes, objectId, rowId); } public String getDatabasePassword() throws IOException { ByteBuffer buffer = takeSharedBuffer(); try { _pageChannel.readPage(buffer, 0); byte[] pwdBytes = new byte[_format.SIZE_PASSWORD]; buffer.position(_format.OFFSET_PASSWORD); buffer.get(pwdBytes); // de-mask password using extra password mask if necessary (the extra // password mask is generated from the database creation date stored in // the header) byte[] pwdMask = getPasswordMask(buffer, _format); if (pwdMask != null) { for (int i = 0; i < pwdBytes.length; ++i) { pwdBytes[i] ^= pwdMask[i % pwdMask.length]; } } boolean hasPassword = false; for (int i = 0; i < pwdBytes.length; ++i) { if (pwdBytes[i] != 0) { hasPassword = true; break; } } if (!hasPassword) { return null; } String pwd = ColumnImpl.decodeUncompressedText(pwdBytes, getCharset()); // remove any trailing null chars int idx = pwd.indexOf('\0'); if (idx >= 0) { pwd = pwd.substring(0, idx); } return pwd; } finally { releaseSharedBuffer(buffer); } } /** * Finds the relationships matching the given from and to tables from the * given cursor and adds them to the given list. */ private void collectRelationships(Cursor cursor, TableImpl fromTable, TableImpl toTable, List<Relationship> relationships, boolean includeSystemTables) throws IOException { String fromTableName = ((fromTable != null) ? fromTable.getName() : null); String toTableName = ((toTable != null) ? toTable.getName() : null); for (Row row : cursor) { String fromName = row.getString(REL_COL_FROM_TABLE); String toName = row.getString(REL_COL_TO_TABLE); if (((fromTableName == null) || fromTableName.equalsIgnoreCase(fromName)) && ((toTableName == null) || toTableName.equalsIgnoreCase(toName))) { String relName = row.getString(REL_COL_NAME); // found more info for a relationship. see if we already have some // info for this relationship Relationship rel = null; for (Relationship tmp : relationships) { if (tmp.getName().equalsIgnoreCase(relName)) { rel = tmp; break; } } TableImpl relFromTable = fromTable; if (relFromTable == null) { relFromTable = getTable(fromName, includeSystemTables); if (relFromTable == null) { // invalid table or ignoring system tables, just ignore continue; } } TableImpl relToTable = toTable; if (relToTable == null) { relToTable = getTable(toName, includeSystemTables); if (relToTable == null) { // invalid table or ignoring system tables, just ignore continue; } } if (rel == null) { // new relationship int numCols = row.getInt(REL_COL_COLUMN_COUNT); int flags = row.getInt(REL_COL_FLAGS); rel = new RelationshipImpl(relName, relFromTable, relToTable, flags, numCols); relationships.add(rel); } // add column info int colIdx = row.getInt(REL_COL_COLUMN_INDEX); ColumnImpl fromCol = relFromTable.getColumn(row.getString(REL_COL_FROM_COLUMN)); ColumnImpl toCol = relToTable.getColumn(row.getString(REL_COL_TO_COLUMN)); rel.getFromColumns().set(colIdx, fromCol); rel.getToColumns().set(colIdx, toCol); } } } /** * Add a new table to the system catalog * @param name Table name * @param pageNumber Page number that contains the table definition */ private void addToSystemCatalog(String name, int pageNumber, Short type, String linkedDbName, String linkedTableName) throws IOException { Object[] catalogRow = new Object[_systemCatalog.getColumnCount()]; int idx = 0; Date creationTime = new Date(); for (Iterator<ColumnImpl> iter = _systemCatalog.getColumns().iterator(); iter.hasNext(); idx++) { ColumnImpl col = iter.next(); if (CAT_COL_ID.equals(col.getName())) { catalogRow[idx] = Integer.valueOf(pageNumber); } else if (CAT_COL_NAME.equals(col.getName())) { catalogRow[idx] = name; } else if (CAT_COL_TYPE.equals(col.getName())) { catalogRow[idx] = type; } else if (CAT_COL_DATE_CREATE.equals(col.getName()) || CAT_COL_DATE_UPDATE.equals(col.getName())) { catalogRow[idx] = creationTime; } else if (CAT_COL_PARENT_ID.equals(col.getName())) { catalogRow[idx] = _tableParentId; } else if (CAT_COL_FLAGS.equals(col.getName())) { catalogRow[idx] = Integer.valueOf(0); } else if (CAT_COL_OWNER.equals(col.getName())) { byte[] owner = new byte[2]; catalogRow[idx] = owner; owner[0] = (byte) 0xcf; owner[1] = (byte) 0x5f; } else if (CAT_COL_DATABASE.equals(col.getName())) { catalogRow[idx] = linkedDbName; } else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) { catalogRow[idx] = linkedTableName; } } _systemCatalog.addRow(catalogRow); } /** * Add a new table to the system's access control entries * @param pageNumber Page number that contains the table definition */ private void addToAccessControlEntries(int pageNumber) throws IOException { if (_newTableSIDs.isEmpty()) { initNewTableSIDs(); } TableImpl acEntries = getAccessControlEntries(); ColumnImpl acmCol = acEntries.getColumn(ACE_COL_ACM); ColumnImpl inheritCol = acEntries.getColumn(ACE_COL_F_INHERITABLE); ColumnImpl objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID); ColumnImpl sidCol = acEntries.getColumn(ACE_COL_SID); // construct a collection of ACE entries mimicing those of our parent, the // "Tables" system object List<Object[]> aceRows = new ArrayList<Object[]>(_newTableSIDs.size()); for (byte[] sid : _newTableSIDs) { Object[] aceRow = new Object[acEntries.getColumnCount()]; acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM); inheritCol.setRowValue(aceRow, Boolean.FALSE); objIdCol.setRowValue(aceRow, Integer.valueOf(pageNumber)); sidCol.setRowValue(aceRow, sid); aceRows.add(aceRow); } acEntries.addRows(aceRows); } /** * Determines the collection of SIDs which need to be added to new tables. */ private void initNewTableSIDs() throws IOException { // search for ACEs matching the tableParentId. use the index on the // objectId column if found (should be there) Cursor cursor = createCursorWithOptionalIndex(getAccessControlEntries(), ACE_COL_OBJECT_ID, _tableParentId); for (Row row : cursor) { Integer objId = row.getInt(ACE_COL_OBJECT_ID); if (_tableParentId.equals(objId)) { _newTableSIDs.add(row.getBytes(ACE_COL_SID)); } } if (_newTableSIDs.isEmpty()) { // if all else fails, use the hard-coded default _newTableSIDs.add(SYS_DEFAULT_SID); } } /** * Reads a table with the given name from the given pageNumber. */ private TableImpl readTable(String name, int pageNumber, int flags) throws IOException { // first, check for existing table TableImpl table = _tableCache.get(pageNumber); if (table != null) { return table; } ByteBuffer buffer = takeSharedBuffer(); try { // need to load table from db _pageChannel.readPage(buffer, pageNumber); byte pageType = buffer.get(0); if (pageType != PageTypes.TABLE_DEF) { throw new IOException( "Looking for " + name + " at page " + pageNumber + ", but page type is " + pageType); } return _tableCache.put(new TableImpl(this, buffer, pageNumber, name, flags)); } finally { releaseSharedBuffer(buffer); } } /** * Creates a Cursor restricted to the given column value if possible (using * an existing index), otherwise a simple table cursor. */ private static Cursor createCursorWithOptionalIndex(TableImpl table, String colName, Object colValue) throws IOException { try { return table.newCursor().setIndexByColumns(table.getColumn(colName)).setSpecificEntry(colValue) .toCursor(); } catch (IllegalArgumentException e) { LOG.info("Could not find expected index on table " + table.getName()); } // use table scan instead return CursorImpl.createCursor(table); } public void flush() throws IOException { if (_linkedDbs != null) { for (Database linkedDb : _linkedDbs.values()) { linkedDb.flush(); } } _pageChannel.flush(); } public void close() throws IOException { if (_linkedDbs != null) { for (Database linkedDb : _linkedDbs.values()) { linkedDb.close(); } } _pageChannel.close(); } /** * Validates an identifier name. * @usage _advanced_method_ */ public static void validateIdentifierName(String name, int maxLength, String identifierType) { if ((name == null) || (name.trim().length() == 0)) { throw new IllegalArgumentException(identifierType + " must have non-empty name"); } if (name.length() > maxLength) { throw new IllegalArgumentException( identifierType + " name is longer than max length of " + maxLength + ": " + name); } } @Override public String toString() { return ToStringBuilder.reflectionToString(this); } /** * Adds a table to the _tableLookup and resets the _tableNames set */ private void addTable(String tableName, Integer pageNumber, Short type, String linkedDbName, String linkedTableName) { _tableLookup.put(toLookupName(tableName), createTableInfo(tableName, pageNumber, 0, type, linkedDbName, linkedTableName)); // clear this, will be created next time needed _tableNames = null; } /** * Creates a TableInfo instance appropriate for the given table data. */ private static TableInfo createTableInfo(String tableName, Integer pageNumber, int flags, Short type, String linkedDbName, String linkedTableName) { if (TYPE_LINKED_TABLE.equals(type)) { return new LinkedTableInfo(pageNumber, tableName, flags, linkedDbName, linkedTableName); } return new TableInfo(pageNumber, tableName, flags); } /** * @return the tableInfo of the given table, if any */ private TableInfo lookupTable(String tableName) throws IOException { String lookupTableName = toLookupName(tableName); TableInfo tableInfo = _tableLookup.get(lookupTableName); if (tableInfo != null) { return tableInfo; } tableInfo = _tableFinder.lookupTable(tableName); if (tableInfo != null) { // cache for later _tableLookup.put(lookupTableName, tableInfo); } return tableInfo; } /** * @return a string usable in the _tableLookup map. */ public static String toLookupName(String name) { return ((name != null) ? name.toUpperCase() : null); } /** * @return {@code true} if the given flags indicate that an object is some * sort of system object, {@code false} otherwise. */ private static boolean isSystemObject(int flags) { return ((flags & SYSTEM_OBJECT_FLAGS) != 0); } /** * Returns the default TimeZone. This is normally the platform default * TimeZone as returned by {@link TimeZone#getDefault}, but can be * overridden using the system property * {@value com.healthmarketscience.jackcess.Database#TIMEZONE_PROPERTY}. * @usage _advanced_method_ */ public static TimeZone getDefaultTimeZone() { String tzProp = System.getProperty(TIMEZONE_PROPERTY); if (tzProp != null) { tzProp = tzProp.trim(); if (tzProp.length() > 0) { return TimeZone.getTimeZone(tzProp); } } // use system default return TimeZone.getDefault(); } /** * Returns the default Charset for the given JetFormat. This may or may not * be platform specific, depending on the format, but can be overridden * using a system property composed of the prefix * {@value com.healthmarketscience.jackcess.Database#CHARSET_PROPERTY_PREFIX} * followed by the JetFormat version to which the charset should apply, * e.g. {@code "com.healthmarketscience.jackcess.charset.VERSION_3"}. * @usage _advanced_method_ */ public static Charset getDefaultCharset(JetFormat format) { String csProp = System.getProperty(CHARSET_PROPERTY_PREFIX + format); if (csProp != null) { csProp = csProp.trim(); if (csProp.length() > 0) { return Charset.forName(csProp); } } // use format default return format.CHARSET; } /** * Returns the default Table.ColumnOrder. This defaults to * {@link Database#DEFAULT_COLUMN_ORDER}, but can be overridden using the system * property {@value com.healthmarketscience.jackcess.Database#COLUMN_ORDER_PROPERTY}. * @usage _advanced_method_ */ public static Table.ColumnOrder getDefaultColumnOrder() { String coProp = System.getProperty(COLUMN_ORDER_PROPERTY); if (coProp != null) { coProp = coProp.trim(); if (coProp.length() > 0) { return Table.ColumnOrder.valueOf(coProp); } } // use default order return DEFAULT_COLUMN_ORDER; } /** * Returns the default enforce foreign-keys policy. This defaults to * {@code true}, but can be overridden using the system * property {@value com.healthmarketscience.jackcess.Database#FK_ENFORCE_PROPERTY}. * @usage _advanced_method_ */ public static boolean getDefaultEnforceForeignKeys() { String prop = System.getProperty(FK_ENFORCE_PROPERTY); if (prop != null) { return Boolean.TRUE.toString().equalsIgnoreCase(prop); } return true; } /** * Copies the given InputStream to the given channel using the most * efficient means possible. */ private static void transferFrom(FileChannel channel, InputStream in) throws IOException { ReadableByteChannel readChannel = Channels.newChannel(in); if (!BROKEN_NIO) { // sane implementation channel.transferFrom(readChannel, 0, MAX_EMPTYDB_SIZE); } else { // do things the hard way for broken vms ByteBuffer bb = ByteBuffer.allocate(8096); while (readChannel.read(bb) >= 0) { bb.flip(); channel.write(bb); bb.clear(); } } } /** * Returns the password mask retrieved from the given header page and * format, or {@code null} if this format does not use a password mask. */ static byte[] getPasswordMask(ByteBuffer buffer, JetFormat format) { // get extra password mask if necessary (the extra password mask is // generated from the database creation date stored in the header) int pwdMaskPos = format.OFFSET_HEADER_DATE; if (pwdMaskPos < 0) { return null; } buffer.position(pwdMaskPos); double dateVal = Double.longBitsToDouble(buffer.getLong()); byte[] pwdMask = new byte[4]; PageChannel.wrap(pwdMask).putInt((int) dateVal); return pwdMask; } static InputStream getResourceAsStream(String resourceName) throws IOException { InputStream stream = DatabaseImpl.class.getClassLoader().getResourceAsStream(resourceName); if (stream == null) { stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName); if (stream == null) { throw new IOException("Could not load jackcess resource " + resourceName); } } return stream; } private static boolean isTableType(Short objType) { return (TYPE_TABLE.equals(objType) || TYPE_LINKED_TABLE.equals(objType)); } public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) { return FILE_FORMAT_DETAILS.get(fileFormat); } private static void addFileFormatDetails(FileFormat fileFormat, String emptyFileName, JetFormat format) { String emptyFile = ((emptyFileName != null) ? RESOURCE_PATH + emptyFileName + fileFormat.getFileExtension() : null); FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format)); } /** * Utility class for storing table page number and actual name. */ private static class TableInfo { public final Integer pageNumber; public final String tableName; public final int flags; private TableInfo(Integer newPageNumber, String newTableName, int newFlags) { pageNumber = newPageNumber; tableName = newTableName; flags = newFlags; } public boolean isLinked() { return false; } } /** * Utility class for storing linked table info */ private static class LinkedTableInfo extends TableInfo { private final String linkedDbName; private final String linkedTableName; private LinkedTableInfo(Integer newPageNumber, String newTableName, int newFlags, String newLinkedDbName, String newLinkedTableName) { super(newPageNumber, newTableName, newFlags); linkedDbName = newLinkedDbName; linkedTableName = newLinkedTableName; } @Override public boolean isLinked() { return true; } } /** * Table iterator for this database, unmodifiable. */ private class TableIterator implements Iterator<Table> { private Iterator<String> _tableNameIter; private TableIterator() { try { _tableNameIter = getTableNames().iterator(); } catch (IOException e) { throw new RuntimeIOException(e); } } public boolean hasNext() { return _tableNameIter.hasNext(); } public void remove() { throw new UnsupportedOperationException(); } public Table next() { if (!hasNext()) { throw new NoSuchElementException(); } try { return getTable(_tableNameIter.next()); } catch (IOException e) { throw new RuntimeIOException(e); } } } /** * Utility class for handling table lookups. */ private abstract class TableFinder { public Integer findObjectId(Integer parentId, String name) throws IOException { Cursor cur = findRow(parentId, name); if (cur == null) { return null; } ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (Integer) cur.getCurrentRowValue(idCol); } public Row getObjectRow(Integer parentId, String name, Collection<String> columns) throws IOException { Cursor cur = findRow(parentId, name); return ((cur != null) ? cur.getCurrentRow(columns) : null); } public Row getObjectRow(Integer objectId, Collection<String> columns) throws IOException { Cursor cur = findRow(objectId); return ((cur != null) ? cur.getCurrentRow(columns) : null); } public void getTableNames(Set<String> tableNames, boolean systemTables) throws IOException { for (Row row : getTableNamesCursor().newIterable().setColumnNames(SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) { String tableName = row.getString(CAT_COL_NAME); int flags = row.getInt(CAT_COL_FLAGS); Short type = row.getShort(CAT_COL_TYPE); int parentId = row.getInt(CAT_COL_PARENT_ID); if ((parentId == _tableParentId) && isTableType(type) && (isSystemObject(flags) == systemTables)) { tableNames.add(tableName); } } } protected abstract Cursor findRow(Integer parentId, String name) throws IOException; protected abstract Cursor findRow(Integer objectId) throws IOException; protected abstract Cursor getTableNamesCursor() throws IOException; public abstract TableInfo lookupTable(String tableName) throws IOException; protected abstract int findMaxSyntheticId() throws IOException; public int getNextFreeSyntheticId() throws IOException { int maxSynthId = findMaxSyntheticId(); if (maxSynthId >= -1) { // bummer, no more ids available throw new IllegalStateException("Too many database objects!"); } return maxSynthId + 1; } } /** * Normal table lookup handler, using catalog table index. */ private final class DefaultTableFinder extends TableFinder { private final IndexCursor _systemCatalogCursor; private IndexCursor _systemCatalogIdCursor; private DefaultTableFinder(IndexCursor systemCatalogCursor) { _systemCatalogCursor = systemCatalogCursor; } private void initIdCursor() throws IOException { if (_systemCatalogIdCursor == null) { _systemCatalogIdCursor = _systemCatalog.newCursor().setIndexByColumnNames(CAT_COL_ID) .toIndexCursor(); } } @Override protected Cursor findRow(Integer parentId, String name) throws IOException { return (_systemCatalogCursor.findFirstRowByEntry(parentId, name) ? _systemCatalogCursor : null); } @Override protected Cursor findRow(Integer objectId) throws IOException { initIdCursor(); return (_systemCatalogIdCursor.findFirstRowByEntry(objectId) ? _systemCatalogIdCursor : null); } @Override public TableInfo lookupTable(String tableName) throws IOException { if (findRow(_tableParentId, tableName) == null) { return null; } Row row = _systemCatalogCursor.getCurrentRow(SYSTEM_CATALOG_COLUMNS); Integer pageNumber = row.getInt(CAT_COL_ID); String realName = row.getString(CAT_COL_NAME); int flags = row.getInt(CAT_COL_FLAGS); Short type = row.getShort(CAT_COL_TYPE); if (!isTableType(type)) { return null; } String linkedDbName = row.getString(CAT_COL_DATABASE); String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME); return createTableInfo(realName, pageNumber, flags, type, linkedDbName, linkedTableName); } @Override protected Cursor getTableNamesCursor() throws IOException { return _systemCatalogCursor.getIndex().newCursor().setStartEntry(_tableParentId, IndexData.MIN_VALUE) .setEndEntry(_tableParentId, IndexData.MAX_VALUE).toIndexCursor(); } @Override protected int findMaxSyntheticId() throws IOException { initIdCursor(); _systemCatalogIdCursor.reset(); // synthetic ids count up from min integer. so the current, highest, // in-use synthetic id is the max id < 0. _systemCatalogIdCursor.findClosestRowByEntry(0); if (!_systemCatalogIdCursor.moveToPreviousRow()) { return Integer.MIN_VALUE; } ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (Integer) _systemCatalogIdCursor.getCurrentRowValue(idCol); } } /** * Fallback table lookup handler, using catalog table scans. */ private final class FallbackTableFinder extends TableFinder { private final Cursor _systemCatalogCursor; private FallbackTableFinder(Cursor systemCatalogCursor) { _systemCatalogCursor = systemCatalogCursor; } @Override protected Cursor findRow(Integer parentId, String name) throws IOException { Map<String, Object> rowPat = new HashMap<String, Object>(); rowPat.put(CAT_COL_PARENT_ID, parentId); rowPat.put(CAT_COL_NAME, name); return (_systemCatalogCursor.findFirstRow(rowPat) ? _systemCatalogCursor : null); } @Override protected Cursor findRow(Integer objectId) throws IOException { ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); return (_systemCatalogCursor.findFirstRow(idCol, objectId) ? _systemCatalogCursor : null); } @Override public TableInfo lookupTable(String tableName) throws IOException { for (Row row : _systemCatalogCursor.newIterable().setColumnNames(SYSTEM_CATALOG_TABLE_NAME_COLUMNS)) { Short type = row.getShort(CAT_COL_TYPE); if (!isTableType(type)) { continue; } int parentId = row.getInt(CAT_COL_PARENT_ID); if (parentId != _tableParentId) { continue; } String realName = row.getString(CAT_COL_NAME); if (!tableName.equalsIgnoreCase(realName)) { continue; } Integer pageNumber = row.getInt(CAT_COL_ID); int flags = row.getInt(CAT_COL_FLAGS); String linkedDbName = row.getString(CAT_COL_DATABASE); String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME); return createTableInfo(realName, pageNumber, flags, type, linkedDbName, linkedTableName); } return null; } @Override protected Cursor getTableNamesCursor() throws IOException { return _systemCatalogCursor; } @Override protected int findMaxSyntheticId() throws IOException { // find max id < 0 ColumnImpl idCol = _systemCatalog.getColumn(CAT_COL_ID); _systemCatalogCursor.reset(); int curMaxSynthId = Integer.MIN_VALUE; while (_systemCatalogCursor.moveToNextRow()) { int id = (Integer) _systemCatalogCursor.getCurrentRowValue(idCol); if ((id > curMaxSynthId) && (id < 0)) { curMaxSynthId = id; } } return curMaxSynthId; } } /** * WeakReference for a Table which holds the table pageNumber (for later * cache purging). */ private static final class WeakTableReference extends WeakReference<TableImpl> { private final Integer _pageNumber; private WeakTableReference(Integer pageNumber, TableImpl table, ReferenceQueue<TableImpl> queue) { super(table, queue); _pageNumber = pageNumber; } public Integer getPageNumber() { return _pageNumber; } } /** * Cache of currently in-use tables, allows re-use of existing tables. */ private static final class TableCache { private final Map<Integer, WeakTableReference> _tables = new HashMap<Integer, WeakTableReference>(); private final ReferenceQueue<TableImpl> _queue = new ReferenceQueue<TableImpl>(); public TableImpl get(Integer pageNumber) { WeakTableReference ref = _tables.get(pageNumber); return ((ref != null) ? ref.get() : null); } public TableImpl put(TableImpl table) { purgeOldRefs(); Integer pageNumber = table.getTableDefPageNumber(); WeakTableReference ref = new WeakTableReference(pageNumber, table, _queue); _tables.put(pageNumber, ref); return table; } private void purgeOldRefs() { WeakTableReference oldRef = null; while ((oldRef = (WeakTableReference) _queue.poll()) != null) { _tables.remove(oldRef.getPageNumber()); } } } /** * Internal details for each FileForrmat * @usage _advanced_class_ */ public static final class FileFormatDetails { private final String _emptyFile; private final JetFormat _format; private FileFormatDetails(String emptyFile, JetFormat format) { _emptyFile = emptyFile; _format = format; } public String getEmptyFilePath() { return _emptyFile; } public JetFormat getFormat() { return _format; } } }