From ef469cef28bc91041802f46f287ab1d6a615fc57 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sun, 3 Mar 2013 03:43:37 +0000 Subject: [PATCH] move Table internals to TableImpl git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jackcess-2@669 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../healthmarketscience/jackcess/Column.java | 26 +- .../healthmarketscience/jackcess/Cursor.java | 38 +- .../jackcess/CursorBuilder.java | 4 +- .../jackcess/Database.java | 7 +- .../jackcess/DatabaseImpl.java | 47 +- .../jackcess/DebugErrorHandler.java | 10 +- .../jackcess/ErrorHandler.java | 19 +- .../jackcess/FKEnforcer.java | 4 +- .../healthmarketscience/jackcess/Index.java | 8 +- .../jackcess/IndexCursor.java | 10 +- .../jackcess/IndexData.java | 8 +- .../jackcess/ReplacementErrorHandler.java | 6 +- .../healthmarketscience/jackcess/Table.java | 2373 +--------------- .../jackcess/TableCreator.java | 4 +- .../jackcess/TableImpl.java | 2412 +++++++++++++++++ .../jackcess/UsageMap.java | 4 +- .../complex/AttachmentColumnInfo.java | 6 +- .../jackcess/complex/ComplexColumnInfo.java | 15 +- .../complex/MultiValueColumnInfo.java | 6 +- .../complex/UnsupportedColumnInfo.java | 6 +- .../complex/VersionHistoryColumnInfo.java | 6 +- 21 files changed, 2564 insertions(+), 2455 deletions(-) create mode 100644 src/java/com/healthmarketscience/jackcess/TableImpl.java diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index d6f71c3..4a8ac47 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -187,7 +187,7 @@ public class Column implements Comparable { /** owning table */ - private final Table _table; + private final TableImpl _table; /** Whether or not the column is of variable length */ private boolean _variableLength; /** Whether or not the column is an autonumber column */ @@ -236,7 +236,7 @@ public class Column implements Comparable { /** * Only used by unit tests */ - Column(boolean testing, Table table) { + Column(boolean testing, TableImpl table) { if(!testing) { throw new IllegalArgumentException(); } @@ -250,7 +250,7 @@ public class Column implements Comparable { * @param offset Offset in the buffer at which the column definition starts * @usage _advanced_method_ */ - public Column(Table table, ByteBuffer buffer, int offset, int displayIndex) + public Column(TableImpl table, ByteBuffer buffer, int offset, int displayIndex) throws IOException { _table = table; @@ -327,7 +327,7 @@ public class Column implements Comparable { /** * @usage _general_method_ */ - public Table getTable() { + public TableImpl getTable() { return _table; } @@ -933,8 +933,8 @@ public class Column implements Comparable { { getPageChannel().readPage(lvalPage, pageNum); - short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat()); - short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat()); + short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); + short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat()); if((rowEnd - rowStart) != length) { throw new IOException("Unexpected lval row length"); @@ -953,8 +953,8 @@ public class Column implements Comparable { lvalPage.clear(); getPageChannel().readPage(lvalPage, pageNum); - short rowStart = Table.findRowStart(lvalPage, rowNum, getFormat()); - short rowEnd = Table.findRowEnd(lvalPage, rowNum, getFormat()); + short rowStart = TableImpl.findRowStart(lvalPage, rowNum, getFormat()); + short rowEnd = TableImpl.findRowEnd(lvalPage, rowNum, getFormat()); // read next page information lvalPage.position(rowStart); @@ -1325,7 +1325,7 @@ public class Column implements Comparable { case LONG_VALUE_TYPE_OTHER_PAGE: lvalPage = getLongValuePage(value.length, lvalBufferH); firstLvalPageNum = lvalBufferH.getPageNumber(); - firstLvalRow = (byte)Table.addDataPageRow(lvalPage, value.length, + firstLvalRow = (byte)TableImpl.addDataPageRow(lvalPage, value.length, getFormat(), 0); lvalPage.put(value); getPageChannel().writePage(lvalPage, firstLvalPageNum); @@ -1363,7 +1363,7 @@ public class Column implements Comparable { } // add row to this page - byte lvalRow = (byte)Table.addDataPageRow(lvalPage, chunkLength + 4, + byte lvalRow = (byte)TableImpl.addDataPageRow(lvalPage, chunkLength + 4, getFormat(), 0); // write next page info (we'll always be writing into row 0 for @@ -1437,7 +1437,7 @@ public class Column implements Comparable { ByteBuffer lvalPage = null; if(lvalBufferH.getPageNumber() != PageChannel.INVALID_PAGE_NUMBER) { lvalPage = lvalBufferH.getPage(getPageChannel()); - if(Table.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) { + if(TableImpl.rowFitsOnDataPage(dataLength, lvalPage, getFormat())) { // the current page has space return lvalPage; } @@ -2066,7 +2066,7 @@ public class Column implements Comparable { int position = buffer.position(); buffer.put(col.getType().getValue()); - buffer.putInt(Table.MAGIC_TABLE_NUMBER); //constant magic number + buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number buffer.putShort(columnNumber); //Column Number if (col.isVariableLength()) { if(!col.getType().isLongValue()) { @@ -2118,7 +2118,7 @@ public class Column implements Comparable { } } for (Column col : columns) { - Table.writeName(buffer, col.getName(), creator.getCharset()); + TableImpl.writeName(buffer, col.getName(), creator.getCharset()); } } diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index 042241a..162e799 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -34,7 +34,7 @@ import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; -import com.healthmarketscience.jackcess.Table.RowState; +import com.healthmarketscience.jackcess.TableImpl.RowState; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -75,7 +75,7 @@ public abstract class Cursor implements Iterable> /** identifier for this cursor */ private final Id _id; /** owning table */ - private final Table _table; + private final TableImpl _table; /** State used for reading the table rows */ private final RowState _rowState; /** the first (exclusive) row id for this cursor */ @@ -89,7 +89,7 @@ public abstract class Cursor implements Iterable> /** ColumnMatcher to be used when matching column values */ protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE; - protected Cursor(Id id, Table table, Position firstPos, Position lastPos) { + protected Cursor(Id id, TableImpl table, Position firstPos, Position lastPos) { _id = id; _table = table; _rowState = _table.createRowState(); @@ -103,7 +103,7 @@ public abstract class Cursor implements Iterable> * Creates a normal, un-indexed cursor for the given table. * @param table the table over which this cursor will traverse */ - public static Cursor createCursor(Table table) { + public static Cursor createCursor(TableImpl table) { return new TableScanCursor(table); } @@ -118,7 +118,7 @@ public abstract class Cursor implements Iterable> * @param index index for the table which will define traversal order as * well as enhance certain lookups */ - public static Cursor createIndexCursor(Table table, Index index) + public static Cursor createIndexCursor(TableImpl table, Index index) throws IOException { return IndexCursor.createCursor(table, index); @@ -140,7 +140,7 @@ public abstract class Cursor implements Iterable> * @param endRow the last row of data for the cursor (inclusive), or * {@code null} for the last entry */ - public static Cursor createIndexCursor(Table table, Index index, + public static Cursor createIndexCursor(TableImpl table, Index index, Object[] startRow, Object[] endRow) throws IOException { @@ -165,7 +165,7 @@ public abstract class Cursor implements Iterable> * the last entry * @param endInclusive whether or not endRow is inclusive or exclusive */ - public static Cursor createIndexCursor(Table table, Index index, + public static Cursor createIndexCursor(TableImpl table, Index index, Object[] startRow, boolean startInclusive, Object[] endRow, @@ -188,7 +188,7 @@ public abstract class Cursor implements Iterable> * @param rowPattern pattern to be used to find the row * @return the matching row or {@code null} if a match could not be found. */ - public static Map findRow(Table table, + public static Map findRow(TableImpl table, Map rowPattern) throws IOException { @@ -216,7 +216,7 @@ public abstract class Cursor implements Iterable> * desired row * @return the matching row or {@code null} if a match could not be found. */ - public static Object findValue(Table table, Column column, + public static Object findValue(TableImpl table, Column column, Column columnPattern, Object valuePattern) throws IOException { @@ -240,7 +240,7 @@ public abstract class Cursor implements Iterable> * @param rowPattern pattern to be used to find the row * @return the matching row or {@code null} if a match could not be found. */ - public static Map findRow(Table table, Index index, + public static Map findRow(TableImpl table, Index index, Map rowPattern) throws IOException { @@ -269,7 +269,7 @@ public abstract class Cursor implements Iterable> * desired row * @return the matching row or {@code null} if a match could not be found. */ - public static Object findValue(Table table, Index index, Column column, + public static Object findValue(TableImpl table, Index index, Column column, Column columnPattern, Object valuePattern) throws IOException { @@ -284,7 +284,7 @@ public abstract class Cursor implements Iterable> return _id; } - public Table getTable() { + public TableImpl getTable() { return _table; } @@ -438,7 +438,7 @@ public abstract class Cursor implements Iterable> { // we need to ensure that the "deleted" flag has been read for this row // (or re-read if the table has been recently modified) - Table.positionAtRowData(_rowState, _curPos.getRowId()); + TableImpl.positionAtRowData(_rowState, _curPos.getRowId()); return _rowState.isDeleted(); } @@ -826,7 +826,7 @@ public abstract class Cursor implements Iterable> _rowState.reset(); _prevPos = _curPos; _curPos = findAnotherPosition(_rowState, _curPos, moveForward); - Table.positionAtRowHeader(_rowState, _curPos.getRowId()); + TableImpl.positionAtRowHeader(_rowState, _curPos.getRowId()); return(!_curPos.equals(getDirHandler(moveForward).getEndPosition())); } @@ -1330,7 +1330,7 @@ public abstract class Cursor implements Iterable> /** Cursor over the pages that this table owns */ private final UsageMap.PageCursor _ownedPagesCursor; - private TableScanCursor(Table table) { + private TableScanCursor(TableImpl table) { super(new Id(table, null), table, FIRST_SCAN_POSITION, LAST_SCAN_POSITION); _ownedPagesCursor = table.getOwnedPagesCursor(); @@ -1376,7 +1376,7 @@ public abstract class Cursor implements Iterable> // figure out how many rows are left on this page so we can find the // next row RowId curRowId = curPos.getRowId(); - Table.positionAtRowHeader(rowState, curRowId); + TableImpl.positionAtRowHeader(rowState, curRowId); int currentRowNumber = curRowId.getRowNumber(); // loop until we find the next valid row or run out of pages @@ -1384,14 +1384,14 @@ public abstract class Cursor implements Iterable> currentRowNumber = handler.getAnotherRowNumber(currentRowNumber); curRowId = new RowId(curRowId.getPageNumber(), currentRowNumber); - Table.positionAtRowHeader(rowState, curRowId); + TableImpl.positionAtRowHeader(rowState, curRowId); if(!rowState.isValid()) { // load next page curRowId = new RowId(handler.getAnotherPageNumber(), RowId.INVALID_ROW_NUMBER); - Table.positionAtRowHeader(rowState, curRowId); + TableImpl.positionAtRowHeader(rowState, curRowId); if(!rowState.isHeaderPageNumberValid()) { //No more owned pages. No more rows. @@ -1486,7 +1486,7 @@ public abstract class Cursor implements Iterable> private final String _tableName; private final String _indexName; - protected Id(Table table, Index index) { + protected Id(TableImpl table, Index index) { _tableName = table.getName(); _indexName = ((index != null) ? index.getName() : null); } diff --git a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java index 4e955d0..f0d7348 100644 --- a/src/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -278,9 +278,9 @@ public class CursorBuilder { { Cursor cursor = null; if(_index == null) { - cursor = Cursor.createCursor(_table); + cursor = Cursor.createCursor((TableImpl)_table); } else { - cursor = Cursor.createIndexCursor(_table, _index, + cursor = Cursor.createIndexCursor((TableImpl)_table, _index, _startRow, _startRowInclusive, _endRow, _endRowInclusive); } diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index ca16047..7857a87 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -40,8 +40,13 @@ import com.healthmarketscience.jackcess.query.Query; * the data via the relevant {@link Table}. When a Database instance is no * longer useful, it should always be closed ({@link #close}) to avoid * corruption. + *

+ * Note, Database instances (and all the related objects) are not + * thread-safe. However, separate Database instances (and their respective + * objects) can be used by separate threads without a problem. * * @author James Ahlborn + * @usage _general_class_ */ public abstract class Database implements Iterable, Closeable, Flushable { @@ -109,7 +114,7 @@ public abstract class Database implements Iterable
, Closeable, Flushable */ public static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() { public Object handleRowError(Column column, byte[] columnData, - Table.RowState rowState, Exception error) + Location location, Exception error) throws IOException { // really can only be RuntimeException or IOException diff --git a/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java b/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java index 2aa0208..cbb5c7f 100644 --- a/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java +++ b/src/java/com/healthmarketscience/jackcess/DatabaseImpl.java @@ -300,17 +300,17 @@ public class DatabaseImpl extends Database /** Reads and writes database pages */ private final PageChannel _pageChannel; /** System catalog table */ - private Table _systemCatalog; + private TableImpl _systemCatalog; /** utility table finder */ private TableFinder _tableFinder; /** System access control entries table (initialized on first use) */ - private Table _accessControlEntries; + private TableImpl _accessControlEntries; /** System relationships table (initialized on first use) */ - private Table _relationships; + private TableImpl _relationships; /** System queries table (initialized on first use) */ - private Table _queries; + private TableImpl _queries; /** System complex columns table (initialized on first use) */ - private Table _complexCols; + private TableImpl _complexCols; /** SIDs to use for the ACEs added for new tables */ private final List _newTableSIDs = new ArrayList(); /** optional error handler to use when row errors are encountered */ @@ -566,12 +566,12 @@ public class DatabaseImpl extends Database } @Override - public Table getSystemCatalog() { + public TableImpl getSystemCatalog() { return _systemCatalog; } @Override - public Table getAccessControlEntries() throws IOException { + public TableImpl getAccessControlEntries() throws IOException { if(_accessControlEntries == null) { _accessControlEntries = getSystemTable(TABLE_SYSTEM_ACES); if(_accessControlEntries == null) { @@ -587,7 +587,7 @@ public class DatabaseImpl extends Database * @return the complex column system table (loaded on demand) * @usage _advanced_method_ */ - public Table getSystemComplexColumns() throws IOException { + public TableImpl getSystemComplexColumns() throws IOException { if(_complexCols == null) { _complexCols = getSystemTable(TABLE_SYSTEM_COMPLEX_COLS); if(_complexCols == null) { @@ -881,7 +881,7 @@ public class DatabaseImpl extends Database } @Override - public Table getTable(String name) throws IOException { + public TableImpl getTable(String name) throws IOException { return getTable(name, false); } @@ -890,10 +890,10 @@ public class DatabaseImpl extends Database * @return The table, or null if it doesn't exist * @usage _advanced_method_ */ - public Table getTable(int tableDefPageNumber) throws IOException { + public TableImpl getTable(int tableDefPageNumber) throws IOException { // first, check for existing table - Table table = _tableCache.get(tableDefPageNumber); + TableImpl table = _tableCache.get(tableDefPageNumber); if(table != null) { return table; } @@ -916,7 +916,7 @@ public class DatabaseImpl extends Database * @param includeSystemTables whether to consider returning a system table * @return The table, or null if it doesn't exist */ - private Table getTable(String name, boolean includeSystemTables) + private TableImpl getTable(String name, boolean includeSystemTables) throws IOException { TableInfo tableInfo = lookupTable(name); @@ -1115,7 +1115,7 @@ public class DatabaseImpl extends Database } @Override - public Table getSystemTable(String tableName) throws IOException + public TableImpl getSystemTable(String tableName) throws IOException { return getTable(tableName, true); } @@ -1382,11 +1382,11 @@ public class DatabaseImpl extends Database /** * Reads a table with the given name from the given pageNumber. */ - private Table readTable(String name, int pageNumber, int flags) + private TableImpl readTable(String name, int pageNumber, int flags) throws IOException { // first, check for existing table - Table table = _tableCache.get(pageNumber); + TableImpl table = _tableCache.get(pageNumber); if(table != null) { return table; } @@ -1402,7 +1402,7 @@ public class DatabaseImpl extends Database ", but page type is " + pageType); } return _tableCache.put( - new Table(this, buffer, pageNumber, name, flags)); + new TableImpl(this, buffer, pageNumber, name, flags)); } finally { releaseSharedBuffer(buffer); } @@ -1413,7 +1413,7 @@ public class DatabaseImpl extends Database * an existing index), otherwise a simple table cursor. */ private static Cursor createCursorWithOptionalIndex( - Table table, String colName, Object colValue) + TableImpl table, String colName, Object colValue) throws IOException { try { @@ -2041,12 +2041,12 @@ public class DatabaseImpl extends Database * WeakReference for a Table which holds the table pageNumber (for later * cache purging). */ - private static final class WeakTableReference extends WeakReference
+ private static final class WeakTableReference extends WeakReference { private final Integer _pageNumber; - private WeakTableReference(Integer pageNumber, Table table, - ReferenceQueue
queue) { + private WeakTableReference(Integer pageNumber, TableImpl table, + ReferenceQueue queue) { super(table, queue); _pageNumber = pageNumber; } @@ -2063,14 +2063,15 @@ public class DatabaseImpl extends Database { private final Map _tables = new HashMap(); - private final ReferenceQueue
_queue = new ReferenceQueue
(); + private final ReferenceQueue _queue = + new ReferenceQueue(); - public Table get(Integer pageNumber) { + public TableImpl get(Integer pageNumber) { WeakTableReference ref = _tables.get(pageNumber); return ((ref != null) ? ref.get() : null); } - public Table put(Table table) { + public TableImpl put(TableImpl table) { purgeOldRefs(); Integer pageNumber = table.getTableDefPageNumber(); diff --git a/src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java b/src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java index 2fbd478..55ea729 100644 --- a/src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java +++ b/src/java/com/healthmarketscience/jackcess/DebugErrorHandler.java @@ -60,21 +60,19 @@ public class DebugErrorHandler extends ReplacementErrorHandler } @Override - public Object handleRowError(Column column, - byte[] columnData, - Table.RowState rowState, - Exception error) + public Object handleRowError(Column column, byte[] columnData, + Location location, Exception error) throws IOException { if(LOG.isDebugEnabled()) { LOG.debug("Failed reading column " + column + ", row " + - rowState + ", bytes " + + location + ", bytes " + ((columnData != null) ? ByteUtil.toHexString(columnData) : "null"), error); } - return super.handleRowError(column, columnData, rowState, error); + return super.handleRowError(column, columnData, location, error); } } diff --git a/src/java/com/healthmarketscience/jackcess/ErrorHandler.java b/src/java/com/healthmarketscience/jackcess/ErrorHandler.java index 25c4d9d..052dac1 100644 --- a/src/java/com/healthmarketscience/jackcess/ErrorHandler.java +++ b/src/java/com/healthmarketscience/jackcess/ErrorHandler.java @@ -51,15 +51,30 @@ public interface ErrorHandler * @param columnData the actual column data for the column being read (which * may be {@code null} depending on when the exception * was thrown during the reading process) - * @param rowState the current row state for the caller + * @param location the current location of the error * @param error the error that was encountered * * @return replacement for this row's column */ public Object handleRowError(Column column, byte[] columnData, - Table.RowState rowState, + Location location, Exception error) throws IOException; + /** + * Provides location information for an error. + */ + public interface Location + { + /** + * @return the table in which the error occurred + */ + public Table getTable(); + + /** + * Contains details about the errored row, useful for debugging. + */ + public String toString(); + } } diff --git a/src/java/com/healthmarketscience/jackcess/FKEnforcer.java b/src/java/com/healthmarketscience/jackcess/FKEnforcer.java index b5ce3ec..8aa9f01 100644 --- a/src/java/com/healthmarketscience/jackcess/FKEnforcer.java +++ b/src/java/com/healthmarketscience/jackcess/FKEnforcer.java @@ -44,7 +44,7 @@ final class FKEnforcer private static final ColumnMatcher MATCHER = CaseInsensitiveColumnMatcher.INSTANCE; - private final Table _table; + private final TableImpl _table; private final List _cols; private List _primaryJoinersChkUp; private List _primaryJoinersChkDel; @@ -52,7 +52,7 @@ final class FKEnforcer private List _primaryJoinersDoDel; private List _secondaryJoiners; - FKEnforcer(Table table) { + FKEnforcer(TableImpl table) { _table = table; // at this point, only init the index columns diff --git a/src/java/com/healthmarketscience/jackcess/Index.java b/src/java/com/healthmarketscience/jackcess/Index.java index 9fc24c3..fec1bd9 100644 --- a/src/java/com/healthmarketscience/jackcess/Index.java +++ b/src/java/com/healthmarketscience/jackcess/Index.java @@ -115,7 +115,7 @@ public class Index implements Comparable { return _data; } - public Table getTable() { + public TableImpl getTable() { return getIndexData().getTable(); } @@ -173,7 +173,7 @@ public class Index implements Comparable { return null; } - Table refTable = getTable().getDatabase().getTable( + TableImpl refTable = getTable().getDatabase().getTable( _reference.getOtherTablePageNumber()); if(refTable == null) { @@ -399,7 +399,7 @@ public class Index implements Comparable { // write logical index information for(IndexBuilder idx : creator.getIndexes()) { TableCreator.IndexState idxState = creator.getIndexState(idx); - buffer.putInt(Table.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def + buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def buffer.putInt(idxState.getIndexNumber()); // index num buffer.putInt(idxState.getIndexDataNumber()); // index data num buffer.put((byte)0); // related table type @@ -413,7 +413,7 @@ public class Index implements Comparable { // write index names for(IndexBuilder idx : creator.getIndexes()) { - Table.writeName(buffer, idx.getName(), creator.getCharset()); + TableImpl.writeName(buffer, idx.getName(), creator.getCharset()); } } diff --git a/src/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/java/com/healthmarketscience/jackcess/IndexCursor.java index aa77b65..016a9c6 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexCursor.java +++ b/src/java/com/healthmarketscience/jackcess/IndexCursor.java @@ -27,7 +27,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; -import com.healthmarketscience.jackcess.Table.RowState; +import com.healthmarketscience.jackcess.TableImpl.RowState; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -53,7 +53,7 @@ public class IndexCursor extends Cursor /** column names for the index entry columns */ private Set _indexEntryPattern; - private IndexCursor(Table table, Index index, + private IndexCursor(TableImpl table, Index index, IndexData.EntryCursor entryCursor) throws IOException { @@ -76,7 +76,7 @@ public class IndexCursor extends Cursor * @param index index for the table which will define traversal order as * well as enhance certain lookups */ - public static IndexCursor createCursor(Table table, Index index) + public static IndexCursor createCursor(TableImpl table, Index index) throws IOException { return createCursor(table, index, null, null); @@ -99,7 +99,7 @@ public class IndexCursor extends Cursor * {@code null} for the last entry */ public static IndexCursor createCursor( - Table table, Index index, Object[] startRow, Object[] endRow) + TableImpl table, Index index, Object[] startRow, Object[] endRow) throws IOException { return createCursor(table, index, startRow, true, endRow, true); @@ -123,7 +123,7 @@ public class IndexCursor extends Cursor * the last entry * @param endInclusive whether or not endRow is inclusive or exclusive */ - public static IndexCursor createCursor(Table table, Index index, + public static IndexCursor createCursor(TableImpl table, Index index, Object[] startRow, boolean startInclusive, Object[] endRow, diff --git a/src/java/com/healthmarketscience/jackcess/IndexData.java b/src/java/com/healthmarketscience/jackcess/IndexData.java index e3d508c..ca22be4 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/IndexData.java @@ -141,7 +141,7 @@ public class IndexData { /** owning table */ - private final Table _table; + private final TableImpl _table; /** 0-based index data number */ private final int _number; /** Page number of the root index data */ @@ -181,7 +181,7 @@ public class IndexData { /** Cache which manages the index pages */ private final IndexPageCache _pageCache; - protected IndexData(Table table, int number, int uniqueEntryCount, + protected IndexData(TableImpl table, int number, int uniqueEntryCount, int uniqueEntryCountOffset) { _table = table; @@ -196,7 +196,7 @@ public class IndexData { * Creates an IndexData appropriate for the given table, using information * from the given table definition buffer. */ - public static IndexData create(Table table, ByteBuffer tableBuffer, + public static IndexData create(TableImpl table, ByteBuffer tableBuffer, int number, JetFormat format) throws IOException { @@ -208,7 +208,7 @@ public class IndexData { return new IndexData(table, number, uniqueEntryCount, uniqueEntryCountOffset); } - public Table getTable() { + public TableImpl getTable() { return _table; } diff --git a/src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java b/src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java index bdb003c..443de4e 100644 --- a/src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java +++ b/src/java/com/healthmarketscience/jackcess/ReplacementErrorHandler.java @@ -56,10 +56,8 @@ public class ReplacementErrorHandler implements ErrorHandler _replacement = replacement; } - public Object handleRowError(Column column, - byte[] columnData, - Table.RowState rowState, - Exception error) + public Object handleRowError(Column column, byte[] columnData, + Location location, Exception error) throws IOException { return _replacement; diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index 18303df..2c0c3fd 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -1,5 +1,5 @@ /* -Copyright (c) 2005 Health Market Science, Inc. +Copyright (c) 2013 James Ahlborn This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -15,72 +15,21 @@ 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; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** - * A single database table - *

- * Is not thread-safe. - * - * @author Tim McCune + * + * @author James Ahlborn * @usage _general_class_ */ -public class Table - implements Iterable> +public abstract class Table implements Iterable> { - - private static final Log LOG = LogFactory.getLog(Table.class); - - private static final short OFFSET_MASK = (short)0x1FFF; - - private static final short DELETED_ROW_MASK = (short)0x8000; - - private static final short OVERFLOW_ROW_MASK = (short)0x4000; - - static final int MAGIC_TABLE_NUMBER = 1625; - - private static final int MAX_BYTE = 256; - - /** - * Table type code for system tables - * @usage _intermediate_class_ - */ - public static final byte TYPE_SYSTEM = 0x53; - /** - * Table type code for user tables - * @usage _intermediate_class_ - */ - public static final byte TYPE_USER = 0x4e; - /** * enum which controls the ordering of the columns in a table. * @usage _intermediate_class_ @@ -94,183 +43,27 @@ public class Table DISPLAY; } - /** comparator which sorts variable length columns based on their index into - the variable length offset table */ - private static final Comparator VAR_LEN_COLUMN_COMPARATOR = - new Comparator() { - public int compare(Column c1, Column c2) { - return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1 : - ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : - 0)); - } - }; - - /** comparator which sorts columns based on their display index */ - private static final Comparator DISPLAY_ORDER_COMPARATOR = - new Comparator() { - public int compare(Column c1, Column c2) { - return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1 : - ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : - 0)); - } - }; - - /** owning database */ - private final DatabaseImpl _database; - /** additional table flags from the catalog entry */ - private int _flags; - /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ - private byte _tableType; - /** Number of actual indexes on the table */ - private int _indexCount; - /** Number of logical indexes for the table */ - private int _logicalIndexCount; - /** Number of rows in the table */ - private int _rowCount; - /** last long auto number for the table */ - private int _lastLongAutoNumber; - /** last complex type auto number for the table */ - private int _lastComplexTypeAutoNumber; - /** page number of the definition of this table */ - private final int _tableDefPageNumber; - /** max Number of columns in the table (includes previous deletions) */ - private short _maxColumnCount; - /** max Number of variable columns in the table */ - private short _maxVarColumnCount; - /** List of columns in this table, ordered by column number */ - private List _columns = new ArrayList(); - /** List of variable length columns in this table, ordered by offset */ - private final List _varColumns = new ArrayList(); - /** List of autonumber columns in this table, ordered by column number */ - private List _autoNumColumns; - /** List of indexes on this table (multiple logical indexes may be backed by - the same index data) */ - private final List _indexes = new ArrayList(); - /** List of index datas on this table (the actual backing data for an - index) */ - private final List _indexDatas = new ArrayList(); - /** List of columns in this table which are in one or more indexes */ - private final Set _indexColumns = new LinkedHashSet(); - /** Table name as stored in Database */ - private final String _name; - /** Usage map of pages that this table owns */ - private UsageMap _ownedPages; - /** Usage map of pages that this table owns with free space on them */ - private UsageMap _freeSpacePages; - /** modification count for the table, keeps row-states up-to-date */ - private int _modCount; - /** page buffer used to update data pages when adding rows */ - private final TempPageHolder _addRowBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** page buffer used to update the table def page */ - private final TempPageHolder _tableDefBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** buffer used to writing single rows of data */ - private final TempBufferHolder _singleRowBufferH = - TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true); - /** "buffer" used to writing multi rows of data (will create new buffer on - every call) */ - private final TempBufferHolder _multiRowBufferH = - TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true); - /** page buffer used to write out-of-line "long value" data */ - private final TempPageHolder _longValueBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** optional error handler to use when row errors are encountered */ - private ErrorHandler _tableErrorHandler; - /** properties for this table */ - private PropertyMap _props; - /** properties group for this table (and columns) */ - private PropertyMaps _propertyMaps; - /** foreign-key enforcer for this table */ - private final FKEnforcer _fkEnforcer; - - /** common cursor for iterating through the table, kept here for historic - reasons */ - private Cursor _cursor; - - /** - * Only used by unit tests - - */ - Table(boolean testing, List columns) throws IOException { - if(!testing) { - throw new IllegalArgumentException(); - } - _database = null; - _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER; - _name = null; - setColumns(columns); - _fkEnforcer = null; - } - - /** - * @param database database which owns this table - * @param tableBuffer Buffer to read the table with - * @param pageNumber Page number of the table definition - * @param name Table name - */ - protected Table(DatabaseImpl database, ByteBuffer tableBuffer, - int pageNumber, String name, int flags) - throws IOException - { - _database = database; - _tableDefPageNumber = pageNumber; - _name = name; - _flags = flags; - readTableDefinition(loadCompleteTableDefinitionBuffer(tableBuffer)); - _fkEnforcer = new FKEnforcer(this); - } - /** * @return The name of the table * @usage _general_method_ */ - public String getName() { - return _name; - } + public abstract String getName(); /** * Whether or not this table has been marked as hidden. * @usage _general_method_ */ - public boolean isHidden() { - return((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0); - } + public abstract boolean isHidden(); - /** - * @usage _advanced_method_ - */ - public int getMaxColumnCount() { - return _maxColumnCount; - } - - /** - * @usage _general_method_ - */ - public int getColumnCount() { - return _columns.size(); - } - /** * @usage _general_method_ */ - public DatabaseImpl getDatabase() { - return _database; - } - - /** - * @usage _advanced_method_ - */ - public JetFormat getFormat() { - return getDatabase().getFormat(); - } + public abstract int getColumnCount(); /** - * @usage _advanced_method_ + * @usage _general_method_ */ - public PageChannel getPageChannel() { - return getDatabase().getPageChannel(); - } + public abstract Database getDatabase(); /** * Gets the currently configured ErrorHandler (always non-{@code null}). @@ -278,154 +71,45 @@ public class Table * level. * @usage _intermediate_method_ */ - public ErrorHandler getErrorHandler() { - return((_tableErrorHandler != null) ? _tableErrorHandler : - getDatabase().getErrorHandler()); - } + public abstract ErrorHandler getErrorHandler(); /** * Sets a new ErrorHandler. If {@code null}, resets to using the * ErrorHandler configured at the Database level. * @usage _intermediate_method_ */ - public void setErrorHandler(ErrorHandler newErrorHandler) { - _tableErrorHandler = newErrorHandler; - } - - public int getTableDefPageNumber() { - return _tableDefPageNumber; - } - - /** - * @usage _advanced_method_ - */ - public RowState createRowState() { - return new RowState(TempBufferHolder.Type.HARD); - } - - protected UsageMap.PageCursor getOwnedPagesCursor() { - return _ownedPages.cursor(); - } - - /** - * Returns the approximate number of database pages owned by this - * table and all related indexes (this number does not take into - * account pages used for large OLE/MEMO fields). - *

- * To calculate the approximate number of bytes owned by a table: - * - * int approxTableBytes = (table.getApproximateOwnedPageCount() * - * table.getFormat().PAGE_SIZE); - * - * @usage _intermediate_method_ - */ - public int getApproximateOwnedPageCount() { - // add a page for the table def (although that might actually be more than - // one page) - int count = _ownedPages.getPageCount() + 1; - // note, we count owned pages from _physical_ indexes, not logical indexes - // (otherwise we could double count pages) - for(IndexData indexData : _indexDatas) { - count += indexData.getOwnedPageCount(); - } - return count; - } - - protected TempPageHolder getLongValueBuffer() { - return _longValueBufferH; - } + public abstract void setErrorHandler(ErrorHandler newErrorHandler); /** * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ */ - public List getColumns() { - return Collections.unmodifiableList(_columns); - } + public abstract List getColumns(); /** * @return the column with the given name * @usage _general_method_ */ - public Column getColumn(String name) { - for(Column column : _columns) { - if(column.getName().equalsIgnoreCase(name)) { - return column; - } - } - throw new IllegalArgumentException("Column with name " + name + - " does not exist in this table"); - } - - /** - * Only called by unit tests - */ - private void setColumns(List columns) { - _columns = columns; - int colIdx = 0; - int varLenIdx = 0; - int fixedOffset = 0; - for(Column col : _columns) { - col.setColumnNumber((short)colIdx); - col.setColumnIndex(colIdx++); - if(col.isVariableLength()) { - col.setVarLenTableIndex(varLenIdx++); - _varColumns.add(col); - } else { - col.setFixedDataOffset(fixedOffset); - fixedOffset += col.getType().getFixedSize(); - } - } - _maxColumnCount = (short)_columns.size(); - _maxVarColumnCount = (short)_varColumns.size(); - _autoNumColumns = getAutoNumberColumns(columns); - } + public abstract Column getColumn(String name); /** * @return the properties for this table * @usage _general_method_ */ - public PropertyMap getProperties() throws IOException { - if(_props == null) { - _props = getPropertyMaps().getDefault(); - } - return _props; - } + public abstract PropertyMap getProperties() throws IOException; - /** - * @return all PropertyMaps for this table (and columns) - * @usage _general_method_ - */ - protected PropertyMaps getPropertyMaps() throws IOException { - if(_propertyMaps == null) { - _propertyMaps = getDatabase().getPropertiesForObject( - _tableDefPageNumber); - } - return _propertyMaps; - } - /** * @return All of the Indexes on this table (unmodifiable List) * @usage _intermediate_method_ */ - public List getIndexes() { - return Collections.unmodifiableList(_indexes); - } + public abstract List getIndexes(); /** * @return the index with the given name * @throws IllegalArgumentException if there is no index with the given name * @usage _intermediate_method_ */ - public Index getIndex(String name) { - for(Index index : _indexes) { - if(index.getName().equalsIgnoreCase(name)) { - return index; - } - } - throw new IllegalArgumentException("Index with name " + name + - " does not exist on this table"); - } + public abstract Index getIndex(String name); /** * @return the primary key index for this table @@ -433,971 +117,30 @@ public class Table * table * @usage _intermediate_method_ */ - public Index getPrimaryKeyIndex() { - for(Index index : _indexes) { - if(index.isPrimaryKey()) { - return index; - } - } - throw new IllegalArgumentException("Table " + getName() + - " does not have a primary key index"); - } - + public abstract Index getPrimaryKeyIndex(); + /** * @return the foreign key index joining this table to the given other table * @throws IllegalArgumentException if there is no relationship between this * table and the given table * @usage _intermediate_method_ */ - public Index getForeignKeyIndex(Table otherTable) { - for(Index index : _indexes) { - if(index.isForeignKey() && (index.getReference() != null) && - (index.getReference().getOtherTablePageNumber() == - otherTable.getTableDefPageNumber())) { - return index; - } - } - throw new IllegalArgumentException( - "Table " + getName() + " does not have a foreign key reference to " + - otherTable.getName()); - } - - /** - * @return All of the IndexData on this table (unmodifiable List) - */ - List getIndexDatas() { - return Collections.unmodifiableList(_indexDatas); - } - - /** - * Only called by unit tests - */ - int getLogicalIndexCount() { - return _logicalIndexCount; - } - - private Cursor getInternalCursor() { - if(_cursor == null) { - _cursor = Cursor.createCursor(this); - } - return _cursor; - } - - /** - * After calling this method, getNextRow will return the first row in the - * table, see {@link Cursor#reset}. - * @usage _general_method_ - */ - public void reset() { - getInternalCursor().reset(); - } - - /** - * Delete the current row (retrieved by a call to {@link #getNextRow()}). - * @usage _general_method_ - */ - public void deleteCurrentRow() throws IOException { - getInternalCursor().deleteCurrentRow(); - } - - /** - * Delete the row on which the given rowState is currently positioned. - *

- * Note, this method is not generally meant to be used directly. You should - * use the {@link #deleteCurrentRow} method or use the Cursor class, which - * allows for more complex table interactions. - * @usage _advanced_method_ - */ - public void deleteRow(RowState rowState, RowId rowId) throws IOException { - requireValidRowId(rowId); - - // ensure that the relevant row state is up-to-date - ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId); - - requireNonDeletedRow(rowState, rowId); - - // delete flag always gets set in the "header" row (even if data is on - // overflow row) - int pageNumber = rowState.getHeaderRowId().getPageNumber(); - int rowNumber = rowState.getHeaderRowId().getRowNumber(); - - // attempt to fill in index column values - Object[] rowValues = null; - if(!_indexDatas.isEmpty()) { - - // move to row data to get index values - rowBuffer = positionAtRowData(rowState, rowId); - - for(Column idxCol : _indexColumns) { - getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null); - } - - // use any read rowValues to help update the indexes - rowValues = rowState.getRowValues(); - - // check foreign keys before proceeding w/ deletion - _fkEnforcer.deleteRow(rowValues); - - // move back to the header - rowBuffer = positionAtRowHeader(rowState, rowId); - } - - // finally, pull the trigger - int rowIndex = getRowStartOffset(rowNumber, getFormat()); - rowBuffer.putShort(rowIndex, (short)(rowBuffer.getShort(rowIndex) - | DELETED_ROW_MASK | OVERFLOW_ROW_MASK)); - writeDataPage(rowBuffer, pageNumber); - - // update the indexes - for(IndexData indexData : _indexDatas) { - indexData.deleteRow(rowValues, rowId); - } - - // make sure table def gets updated - updateTableDefinition(-1); - } - - /** - * @return The next row in this table (Column name -> Column value) - * @usage _general_method_ - */ - public Map getNextRow() throws IOException { - return getNextRow(null); - } - - /** - * @param columnNames Only column names in this collection will be returned - * @return The next row in this table (Column name -> Column value) - * @usage _general_method_ - */ - public Map getNextRow(Collection columnNames) - throws IOException - { - return getInternalCursor().getNextRow(columnNames); - } - - /** - * Reads a single column from the given row. - *

- * Note, this method is not generally meant to be used directly. Instead - * use the Cursor class, which allows for more complex table interactions, - * e.g. {@link Cursor#getCurrentRowValue}. - * @usage _advanced_method_ - */ - public Object getRowValue(RowState rowState, RowId rowId, Column column) - throws IOException - { - if(this != column.getTable()) { - throw new IllegalArgumentException( - "Given column " + column + " is not from this table"); - } - requireValidRowId(rowId); - - // position at correct row - ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); - requireNonDeletedRow(rowState, rowId); - - return getRowColumn(getFormat(), rowBuffer, column, rowState, null); - } - - /** - * Reads some columns from the given row. - * @param columnNames Only column names in this collection will be returned - * @usage _advanced_method_ - */ - public Map getRow( - RowState rowState, RowId rowId, Collection columnNames) - throws IOException - { - requireValidRowId(rowId); - - // position at correct row - ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); - requireNonDeletedRow(rowState, rowId); - - return getRow(getFormat(), rowState, rowBuffer, _columns, columnNames); - } - - /** - * Reads the row data from the given row buffer. Leaves limit unchanged. - * Saves parsed row values to the given rowState. - */ - private static Map getRow( - JetFormat format, - RowState rowState, - ByteBuffer rowBuffer, - Collection columns, - Collection columnNames) - throws IOException - { - Map rtn = new LinkedHashMap( - columns.size()); - for(Column column : columns) { - - if((columnNames == null) || (columnNames.contains(column.getName()))) { - // Add the value to the row data - column.setRowValue( - rtn, getRowColumn(format, rowBuffer, column, rowState, null)); - } - } - return rtn; - } - - /** - * Reads the column data from the given row buffer. Leaves limit unchanged. - * Caches the returned value in the rowState. - */ - private static Object getRowColumn(JetFormat format, - ByteBuffer rowBuffer, - Column column, - RowState rowState, - Map rawVarValues) - throws IOException - { - byte[] columnData = null; - try { - - NullMask nullMask = rowState.getNullMask(rowBuffer); - boolean isNull = nullMask.isNull(column); - if(column.getType() == DataType.BOOLEAN) { - // Boolean values are stored in the null mask. see note about - // caching below - return rowState.setRowValue(column.getColumnIndex(), - Boolean.valueOf(!isNull)); - } else if(isNull) { - // well, that's easy! (no need to update cache w/ null) - return null; - } - - // reset position to row start - rowBuffer.reset(); - - // locate the column data bytes - int rowStart = rowBuffer.position(); - int colDataPos = 0; - int colDataLen = 0; - if(!column.isVariableLength()) { - - // read fixed length value (non-boolean at this point) - int dataStart = rowStart + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET; - colDataPos = dataStart + column.getFixedDataOffset(); - colDataLen = column.getType().getFixedSize(column.getLength()); - - } else { - int varDataStart; - int varDataEnd; - - if(format.SIZE_ROW_VAR_COL_OFFSET == 2) { - - // read simple var length value - int varColumnOffsetPos = - (rowBuffer.limit() - nullMask.byteSize() - 4) - - (column.getVarLenTableIndex() * 2); - - varDataStart = rowBuffer.getShort(varColumnOffsetPos); - varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2); - - } else { - - // read jump-table based var length values - short[] varColumnOffsets = readJumpTableVarColOffsets( - rowState, rowBuffer, rowStart, nullMask); - - varDataStart = varColumnOffsets[column.getVarLenTableIndex()]; - varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1]; - } - - colDataPos = rowStart + varDataStart; - colDataLen = varDataEnd - varDataStart; - } - - // grab the column data - rowBuffer.position(colDataPos); - columnData = ByteUtil.getBytes(rowBuffer, colDataLen); - - if((rawVarValues != null) && column.isVariableLength()) { - // caller wants raw value as well - rawVarValues.put(column, columnData); - } - - // parse the column data. we cache the row values in order to be able - // to update the index on row deletion. note, most of the returned - // values are immutable, except for binary data (returned as byte[]), - // but binary data shouldn't be indexed anyway. - return rowState.setRowValue(column.getColumnIndex(), - column.read(columnData)); - - } catch(Exception e) { - - // cache "raw" row value. see note about caching above - rowState.setRowValue(column.getColumnIndex(), - Column.rawDataWrapper(columnData)); - - return rowState.handleRowError(column, columnData, e); - } - } - - private static short[] readJumpTableVarColOffsets( - RowState rowState, ByteBuffer rowBuffer, int rowStart, - NullMask nullMask) - { - short[] varColOffsets = rowState.getVarColOffsets(); - if(varColOffsets != null) { - return varColOffsets; - } - - // calculate offsets using jump-table info - int nullMaskSize = nullMask.byteSize(); - int rowEnd = rowStart + rowBuffer.remaining() - 1; - int numVarCols = ByteUtil.getUnsignedByte(rowBuffer, - rowEnd - nullMaskSize); - varColOffsets = new short[numVarCols + 1]; - - int rowLen = rowEnd - rowStart + 1; - int numJumps = (rowLen - 1) / MAX_BYTE; - int colOffset = rowEnd - nullMaskSize - numJumps - 1; - - // If last jump is a dummy value, ignore it - if(((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) { - numJumps--; - } - - int jumpsUsed = 0; - for(int i = 0; i < numVarCols + 1; i++) { - - while((jumpsUsed < numJumps) && - (i == ByteUtil.getUnsignedByte( - rowBuffer, rowEnd - nullMaskSize-jumpsUsed - 1))) { - jumpsUsed++; - } - - varColOffsets[i] = (short) - (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i) - + (jumpsUsed * MAX_BYTE)); - } - - rowState.setVarColOffsets(varColOffsets); - return varColOffsets; - } - - /** - * Reads the null mask from the given row buffer. Leaves limit unchanged. - */ - private NullMask getRowNullMask(ByteBuffer rowBuffer) - throws IOException - { - // reset position to row start - rowBuffer.reset(); - - // Number of columns in this row - int columnCount = ByteUtil.getUnsignedVarInt( - rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT); - - // read null mask - NullMask nullMask = new NullMask(columnCount); - rowBuffer.position(rowBuffer.limit() - nullMask.byteSize()); //Null mask at end - nullMask.read(rowBuffer); - - return nullMask; - } - - /** - * Sets a new buffer to the correct row header page using the given rowState - * according to the given rowId. Deleted state is - * determined, but overflow row pointers are not followed. - * - * @return a ByteBuffer of the relevant page, or null if row was invalid - * @usage _advanced_method_ - */ - public static ByteBuffer positionAtRowHeader(RowState rowState, RowId rowId) - throws IOException - { - ByteBuffer rowBuffer = rowState.setHeaderRow(rowId); - - if(rowState.isAtHeaderRow()) { - // this task has already been accomplished - return rowBuffer; - } - - if(!rowState.isValid()) { - // this was an invalid page/row - rowState.setStatus(RowStateStatus.AT_HEADER); - return null; - } - - // note, we don't use findRowStart here cause we need the unmasked value - short rowStart = rowBuffer.getShort( - getRowStartOffset(rowId.getRowNumber(), - rowState.getTable().getFormat())); - - // check the deleted, overflow flags for the row (the "real" flags are - // always set on the header row) - RowStatus rowStatus = RowStatus.NORMAL; - if(isDeletedRow(rowStart)) { - rowStatus = RowStatus.DELETED; - } else if(isOverflowRow(rowStart)) { - rowStatus = RowStatus.OVERFLOW; - } - - rowState.setRowStatus(rowStatus); - rowState.setStatus(RowStateStatus.AT_HEADER); - return rowBuffer; - } - - /** - * Sets the position and limit in a new buffer using the given rowState - * according to the given row number and row end, following overflow row - * pointers as necessary. - * - * @return a ByteBuffer narrowed to the actual row data, or null if row was - * invalid or deleted - * @usage _advanced_method_ - */ - public static ByteBuffer positionAtRowData(RowState rowState, RowId rowId) - throws IOException - { - positionAtRowHeader(rowState, rowId); - if(!rowState.isValid() || rowState.isDeleted()) { - // row is invalid or deleted - rowState.setStatus(RowStateStatus.AT_FINAL); - return null; - } - - ByteBuffer rowBuffer = rowState.getFinalPage(); - int rowNum = rowState.getFinalRowId().getRowNumber(); - JetFormat format = rowState.getTable().getFormat(); - - if(rowState.isAtFinalRow()) { - // we've already found the final row data - return PageChannel.narrowBuffer( - rowBuffer, - findRowStart(rowBuffer, rowNum, format), - findRowEnd(rowBuffer, rowNum, format)); - } - - while(true) { - - // note, we don't use findRowStart here cause we need the unmasked value - short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, format)); - short rowEnd = findRowEnd(rowBuffer, rowNum, format); - - // note, at this point we know the row is not deleted, so ignore any - // subsequent deleted flags (as overflow rows are always marked deleted - // anyway) - boolean overflowRow = isOverflowRow(rowStart); - - // now, strip flags from rowStart offset - rowStart = (short)(rowStart & OFFSET_MASK); - - if (overflowRow) { - - if((rowEnd - rowStart) < 4) { - throw new IOException("invalid overflow row info"); - } - - // Overflow page. the "row" data in the current page points to - // another page/row - int overflowRowNum = ByteUtil.getUnsignedByte(rowBuffer, rowStart); - int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1); - rowBuffer = rowState.setOverflowRow( - new RowId(overflowPageNum, overflowRowNum)); - rowNum = overflowRowNum; - - } else { - - rowState.setStatus(RowStateStatus.AT_FINAL); - return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd); - } - } - } - - - /** - * Calls reset on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table. Use of - * the Iterator follows the same restrictions as a call to - * getNextRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - * @usage _general_method_ - */ - public Iterator> iterator() - { - return iterator(null); - } - - /** - * Calls reset on this table and returns a modifiable - * Iterator which will iterate through all the rows of this table, returning - * only the given columns. Use of the Iterator follows the same - * restrictions as a call to getNextRow. - * @throws IllegalStateException if an IOException is thrown by one of the - * operations, the actual exception will be contained within - * @usage _general_method_ - */ - public Iterator> iterator(Collection columnNames) - { - reset(); - return getInternalCursor().iterator(columnNames); - } - - /** - * Writes a new table defined by the given TableCreator to the database. - * @usage _advanced_method_ - */ - protected static void writeTableDefinition(TableCreator creator) - throws IOException - { - // first, create the usage map page - createUsageMapDefinitionBuffer(creator); - - // next, determine how big the table def will be (in case it will be more - // than one page) - JetFormat format = creator.getFormat(); - int idxDataLen = (creator.getIndexCount() * - (format.SIZE_INDEX_DEFINITION + - format.SIZE_INDEX_COLUMN_BLOCK)) + - (creator.getLogicalIndexCount() * format.SIZE_INDEX_INFO_BLOCK); - int totalTableDefSize = format.SIZE_TDEF_HEADER + - (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + - idxDataLen + format.SIZE_TDEF_TRAILER; - - // total up the amount of space used by the column and index names (2 - // bytes per char + 2 bytes for the length) - for(Column col : creator.getColumns()) { - int nameByteLen = (col.getName().length() * - JetFormat.TEXT_FIELD_UNIT_SIZE); - totalTableDefSize += nameByteLen + 2; - } - - for(IndexBuilder idx : creator.getIndexes()) { - int nameByteLen = (idx.getName().length() * - JetFormat.TEXT_FIELD_UNIT_SIZE); - totalTableDefSize += nameByteLen + 2; - } - - - // now, create the table definition - PageChannel pageChannel = creator.getPageChannel(); - ByteBuffer buffer = pageChannel .createBuffer(Math.max(totalTableDefSize, - format.PAGE_SIZE)); - writeTableDefinitionHeader(creator, buffer, totalTableDefSize); - - if(creator.hasIndexes()) { - // index row counts - IndexData.writeRowCountDefinitions(creator, buffer); - } - - // column definitions - Column.writeDefinitions(creator, buffer); - - if(creator.hasIndexes()) { - // index and index data definitions - IndexData.writeDefinitions(creator, buffer); - Index.writeDefinitions(creator, buffer); - } - - //End of tabledef - buffer.put((byte) 0xff); - buffer.put((byte) 0xff); - - // write table buffer to database - if(totalTableDefSize <= format.PAGE_SIZE) { - - // easy case, fits on one page - buffer.putShort(format.OFFSET_FREE_SPACE, - (short)(buffer.remaining() - 8)); // overwrite page free space - // Write the tdef page to disk. - pageChannel.writePage(buffer, creator.getTdefPageNumber()); - - } else { - - // need to split across multiple pages - ByteBuffer partialTdef = pageChannel.createPageBuffer(); - buffer.rewind(); - int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; - while(buffer.hasRemaining()) { - - // reset for next write - partialTdef.clear(); - - if(nextTdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) { - - // this is the first page. note, the first page already has the - // page header, so no need to write it here - nextTdefPageNumber = creator.getTdefPageNumber(); - - } else { - - // write page header - writeTablePageHeader(partialTdef); - } - - // copy the next page of tdef bytes - int curTdefPageNumber = nextTdefPageNumber; - int writeLen = Math.min(partialTdef.remaining(), buffer.remaining()); - partialTdef.put(buffer.array(), buffer.position(), writeLen); - ByteUtil.forward(buffer, writeLen); - - if(buffer.hasRemaining()) { - // need a next page - nextTdefPageNumber = pageChannel.allocateNewPage(); - partialTdef.putInt(format.OFFSET_NEXT_TABLE_DEF_PAGE, - nextTdefPageNumber); - } - - // update page free space - partialTdef.putShort(format.OFFSET_FREE_SPACE, - (short)(partialTdef.remaining() - 8)); // overwrite page free space - - // write partial page to disk - pageChannel.writePage(partialTdef, curTdefPageNumber); - } - - } - } - - /** - * @param buffer Buffer to write to - * @param columns List of Columns in the table - */ - private static void writeTableDefinitionHeader( - TableCreator creator, ByteBuffer buffer, int totalTableDefSize) - throws IOException - { - List columns = creator.getColumns(); - - //Start writing the tdef - writeTablePageHeader(buffer); - buffer.putInt(totalTableDefSize); //Length of table def - buffer.putInt(MAGIC_TABLE_NUMBER); // seemingly constant magic value - buffer.putInt(0); //Number of rows - buffer.putInt(0); //Last Autonumber - buffer.put((byte) 1); // this makes autonumbering work in access - for (int i = 0; i < 15; i++) { //Unknown - buffer.put((byte) 0); - } - buffer.put(Table.TYPE_USER); //Table type - buffer.putShort((short) columns.size()); //Max columns a row will have - buffer.putShort(Column.countVariableLength(columns)); //Number of variable columns in table - buffer.putShort((short) columns.size()); //Number of columns in table - buffer.putInt(creator.getLogicalIndexCount()); //Number of logical indexes in table - buffer.putInt(creator.getIndexCount()); //Number of indexes in table - buffer.put((byte) 0); //Usage map row number - ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Usage map page number - buffer.put((byte) 1); //Free map row number - ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Free map page number - if (LOG.isDebugEnabled()) { - int position = buffer.position(); - buffer.rewind(); - LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString( - buffer, creator.getFormat().SIZE_TDEF_HEADER)); - buffer.position(position); - } - } + public abstract Index getForeignKeyIndex(Table otherTable); - /** - * Writes the page header for a table definition page - * @param buffer Buffer to write to - */ - private static void writeTablePageHeader(ByteBuffer buffer) - { - buffer.put(PageTypes.TABLE_DEF); //Page type - buffer.put((byte) 0x01); //Unknown - buffer.put((byte) 0); //Unknown - buffer.put((byte) 0); //Unknown - buffer.putInt(0); //Next TDEF page pointer - } - - /** - * Writes the given name into the given buffer in the format as expected by - * {@link #readName}. - */ - static void writeName(ByteBuffer buffer, String name, Charset charset) - { - ByteBuffer encName = Column.encodeUncompressedText(name, charset); - buffer.putShort((short) encName.remaining()); - buffer.put(encName); - } - - /** - * Create the usage map definition page buffer. The "used pages" map is in - * row 0, the "pages with free space" map is in row 1. Index usage maps are - * in subsequent rows. - */ - private static void createUsageMapDefinitionBuffer(TableCreator creator) - throws IOException - { - // 2 table usage maps plus 1 for each index - int umapNum = 2 + creator.getIndexCount(); - - JetFormat format = creator.getFormat(); - int usageMapRowLength = format.OFFSET_USAGE_MAP_START + - format.USAGE_MAP_TABLE_BYTE_LENGTH; - int freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE - - (umapNum * getRowSpaceUsage(usageMapRowLength, format)); - - // for now, don't handle writing that many indexes - if(freeSpace < 0) { - throw new IOException("FIXME attempting to write too many indexes"); - } - - int umapPageNumber = creator.getUmapPageNumber(); - - PageChannel pageChannel = creator.getPageChannel(); - ByteBuffer rtn = pageChannel.createPageBuffer(); - rtn.put(PageTypes.DATA); - rtn.put((byte) 0x1); //Unknown - rtn.putShort((short)freeSpace); //Free space in page - rtn.putInt(0); //Table definition - rtn.putInt(0); //Unknown - rtn.putShort((short) umapNum); //Number of records on this page - - // write two rows of usage map definitions for the table - int rowStart = findRowEnd(rtn, 0, format) - usageMapRowLength; - for(int i = 0; i < 2; ++i) { - rtn.putShort(getRowStartOffset(i, format), (short)rowStart); - if(i == 0) { - // initial "usage pages" map definition - rtn.put(rowStart, UsageMap.MAP_TYPE_REFERENCE); - } else { - // initial "pages with free space" map definition - rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE); - } - rowStart -= usageMapRowLength; - } - - if(creator.hasIndexes()) { - - for(int i = 0; i < creator.getIndexes().size(); ++i) { - IndexBuilder idx = creator.getIndexes().get(i); - - // allocate root page for the index - int rootPageNumber = pageChannel.allocateNewPage(); - int umapRowNum = i + 2; - - // stash info for later use - TableCreator.IndexState idxState = creator.getIndexState(idx); - idxState.setRootPageNumber(rootPageNumber); - idxState.setUmapRowNumber((byte)umapRowNum); - idxState.setUmapPageNumber(umapPageNumber); - - // index map definition, including initial root page - rtn.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart); - rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE); - rtn.putInt(rowStart + 1, rootPageNumber); - rtn.put(rowStart + 5, (byte)1); - - rowStart -= usageMapRowLength; - } - } - - pageChannel.writePage(rtn, umapPageNumber); - } - - /** - * Returns a single ByteBuffer which contains the entire table definition - * (which may span multiple database pages). - */ - private ByteBuffer loadCompleteTableDefinitionBuffer(ByteBuffer tableBuffer) - throws IOException - { - int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); - ByteBuffer nextPageBuffer = null; - while (nextPage != 0) { - if (nextPageBuffer == null) { - nextPageBuffer = getPageChannel().createPageBuffer(); - } - getPageChannel().readPage(nextPageBuffer, nextPage); - nextPage = nextPageBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); - ByteBuffer newBuffer = getPageChannel().createBuffer( - tableBuffer.capacity() + getFormat().PAGE_SIZE - 8); - newBuffer.put(tableBuffer); - newBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8); - tableBuffer = newBuffer; - tableBuffer.flip(); - } - return tableBuffer; - } - - /** - * Read the table definition - */ - private void readTableDefinition(ByteBuffer tableBuffer) throws IOException - { - if (LOG.isDebugEnabled()) { - tableBuffer.rewind(); - LOG.debug("Table def block:\n" + ByteUtil.toHexString(tableBuffer, - getFormat().SIZE_TDEF_HEADER)); - } - _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS); - _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); - if(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) { - _lastComplexTypeAutoNumber = tableBuffer.getInt( - getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER); - } - _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE); - _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS); - _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS); - short columnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_COLS); - _logicalIndexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEX_SLOTS); - _indexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEXES); - - int rowNum = ByteUtil.getUnsignedByte( - tableBuffer, getFormat().OFFSET_OWNED_PAGES); - int pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_OWNED_PAGES + 1); - _ownedPages = UsageMap.read(getDatabase(), pageNum, rowNum, false); - rowNum = ByteUtil.getUnsignedByte( - tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES); - pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES + 1); - _freeSpacePages = UsageMap.read(getDatabase(), pageNum, rowNum, false); - - for (int i = 0; i < _indexCount; i++) { - _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat())); - } - - int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK + - _indexCount * getFormat().SIZE_INDEX_DEFINITION; - int dispIndex = 0; - for (int i = 0; i < columnCount; i++) { - Column column = new Column(this, tableBuffer, - colOffset + (i * getFormat().SIZE_COLUMN_HEADER), dispIndex++); - _columns.add(column); - if(column.isVariableLength()) { - // also shove it in the variable columns list, which is ordered - // differently from the _columns list - _varColumns.add(column); - } - } - tableBuffer.position(colOffset + - (columnCount * getFormat().SIZE_COLUMN_HEADER)); - for (int i = 0; i < columnCount; i++) { - Column column = _columns.get(i); - column.setName(readName(tableBuffer)); - } - Collections.sort(_columns); - _autoNumColumns = getAutoNumberColumns(_columns); - - // setup the data index for the columns - int colIdx = 0; - for(Column col : _columns) { - col.setColumnIndex(colIdx++); - } - - // sort variable length columns based on their index into the variable - // length offset table, because we will write the columns in this order - Collections.sort(_varColumns, VAR_LEN_COLUMN_COMPARATOR); - - // read index column information - for (int i = 0; i < _indexCount; i++) { - IndexData idxData = _indexDatas.get(i); - idxData.read(tableBuffer, _columns); - // keep track of all columns involved in indexes - for(IndexData.ColumnDescriptor iCol : idxData.getColumns()) { - _indexColumns.add(iCol.getColumn()); - } - } - - // read logical index info (may be more logical indexes than index datas) - for (int i = 0; i < _logicalIndexCount; i++) { - _indexes.add(new Index(tableBuffer, _indexDatas, getFormat())); - } - - // read logical index names - for (int i = 0; i < _logicalIndexCount; i++) { - _indexes.get(i).setName(readName(tableBuffer)); - } - - Collections.sort(_indexes); - - // re-sort columns if necessary - if(getDatabase().getColumnOrder() != ColumnOrder.DATA) { - Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR); - } - - for(Column col : _columns) { - // some columns need to do extra work after the table is completely - // loaded - col.postTableLoadInit(); - } - } - - /** - * Writes the given page data to the given page number, clears any other - * relevant buffers. - */ - private void writeDataPage(ByteBuffer pageBuffer, int pageNumber) - throws IOException - { - // write the page data - getPageChannel().writePage(pageBuffer, pageNumber); - - // possibly invalidate the add row buffer if a different data buffer is - // being written (e.g. this happens during deleteRow) - _addRowBufferH.possiblyInvalidate(pageNumber, pageBuffer); - - // update modification count so any active RowStates can keep themselves - // up-to-date - ++_modCount; - } - - /** - * Returns a name read from the buffer at the current position. The - * expected name format is the name length followed by the name - * encoded using the {@link JetFormat#CHARSET} - */ - private String readName(ByteBuffer buffer) { - int nameLength = readNameLength(buffer); - byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); - return Column.decodeUncompressedText(nameBytes, - getDatabase().getCharset()); - } - - /** - * Returns a name length read from the buffer at the current position. - */ - private int readNameLength(ByteBuffer buffer) { - return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH); - } - /** * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #addRow(Object...)}. * @usage _general_method_ */ - public Object[] asRow(Map rowMap) { - return asRow(rowMap, null); - } - + public abstract Object[] asRow(Map rowMap); + /** * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #updateCurrentRow(Object...)}. * @usage _general_method_ */ - public Object[] asUpdateRow(Map rowMap) { - return asRow(rowMap, Column.KEEP_VALUE); - } + public abstract Object[] asUpdateRow(Map rowMap); - /** - * Converts a map of columnName -> columnValue to an array of row values. - */ - private Object[] asRow(Map rowMap, Object defaultValue) - { - Object[] row = new Object[_columns.size()]; - if(defaultValue != null) { - Arrays.fill(row, defaultValue); - } - if(rowMap == null) { - return row; - } - for(Column col : _columns) { - if(rowMap.containsKey(col.getName())) { - col.setRowValue(row, col.getRowValue(rowMap)); - } - } - return row; - } - /** * Adds a single row to this table and writes it to disk. The values are * expected to be given in the order that the Columns are listed by the @@ -1416,10 +159,8 @@ public class Table * otherwise it will not be modified. * @usage _general_method_ */ - public void addRow(Object... row) throws IOException { - addRows(Collections.singletonList(row), _singleRowBufferH); - } - + public abstract void addRow(Object... row) throws IOException; + /** * Add multiple rows to this table, only writing to disk after all * rows have been written, and every time a data page is filled. This @@ -1436,1072 +177,10 @@ public class Table * will not be modified. * @usage _general_method_ */ - public void addRows(List rows) throws IOException { - addRows(rows, _multiRowBufferH); - } - - /** - * Add multiple rows to this table, only writing to disk after all - * rows have been written, and every time a data page is filled. - * @param inRows List of Object[] row values - * @param writeRowBufferH TempBufferHolder used to generate buffers for - * writing the row data - */ - private void addRows(List inRows, - TempBufferHolder writeRowBufferH) - throws IOException - { - if(inRows.isEmpty()) { - return; - } - - // copy the input rows to a modifiable list so we can update the elements - List rows = new ArrayList(inRows); - ByteBuffer[] rowData = new ByteBuffer[rows.size()]; - for (int i = 0; i < rows.size(); i++) { - - // we need to make sure the row is the right length and is an Object[] - // (fill with null if too short). note, if the row is copied the caller - // will not be able to access any generated auto-number value, but if - // they need that info they should use a row array of the right - // size/type! - Object[] row = rows.get(i); - if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { - row = dupeRow(row, _columns.size()); - // we copied the row, so put the copy back into the rows list - rows.set(i, row); - } - - // fill in autonumbers - handleAutoNumbersForAdd(row); - - // write the row of data to a temporary buffer - rowData[i] = createRow(row, writeRowBufferH.getPageBuffer(getPageChannel())); - - if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) { - throw new IOException("Row size " + rowData[i].limit() + - " is too large"); - } - } - - ByteBuffer dataPage = null; - int pageNumber = PageChannel.INVALID_PAGE_NUMBER; - - for (int i = 0; i < rowData.length; i++) { - int rowSize = rowData[i].remaining(); - Object[] row = rows.get(i); - - // handle foreign keys before adding to table - _fkEnforcer.addRow(row); - - // get page with space - dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber); - pageNumber = _addRowBufferH.getPageNumber(); - - // write out the row data - int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), 0); - dataPage.put(rowData[i]); - - // update the indexes - RowId rowId = new RowId(pageNumber, rowNum); - for(IndexData indexData : _indexDatas) { - indexData.addRow(row, rowId); - } - } - - writeDataPage(dataPage, pageNumber); - - // Update tdef page - updateTableDefinition(rows.size()); - } + public abstract void addRows(List rows) throws IOException; /** - * Updates the current row to the new values. - *

- * Note, if this table has an auto-number column(s), the existing value(s) - * will be maintained, unchanged. - * - * @param row new row values for the current row. * @usage _general_method_ */ - public void updateCurrentRow(Object... row) throws IOException { - getInternalCursor().updateCurrentRow(row); - } - - /** - * Update the row on which the given rowState is currently positioned. - *

- * Note, this method is not generally meant to be used directly. You should - * use the {@link #updateCurrentRow} method or use the Cursor class, which - * allows for more complex table interactions, e.g. - * {@link Cursor#setCurrentRowValue} and {@link Cursor#updateCurrentRow}. - * @usage _advanced_method_ - */ - public void updateRow(RowState rowState, RowId rowId, Object... row) - throws IOException - { - requireValidRowId(rowId); - - // ensure that the relevant row state is up-to-date - ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); - int oldRowSize = rowBuffer.remaining(); - - requireNonDeletedRow(rowState, rowId); - - // we need to make sure the row is the right length & type (fill with - // null if too short). - if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { - row = dupeRow(row, _columns.size()); - } - - // hang on to the raw values of var length columns we are "keeping". this - // will allow us to re-use pre-written var length data, which can save - // space for things like long value columns. - Map keepRawVarValues = - (!_varColumns.isEmpty() ? new HashMap() : null); - - for(Column column : _columns) { - if(_autoNumColumns.contains(column)) { - // fill in any auto-numbers (we don't allow autonumber values to be - // modified) - column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, - rowState, null)); - } else if(column.getRowValue(row) == Column.KEEP_VALUE) { - // fill in any "keep value" fields - column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, - rowState, keepRawVarValues)); - } else if(_indexColumns.contains(column)) { - // read row value to help update indexes - getRowColumn(getFormat(), rowBuffer, column, rowState, null); - } - } - - // generate new row bytes - ByteBuffer newRowData = createRow( - row, _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, - keepRawVarValues); - - if (newRowData.limit() > getFormat().MAX_ROW_SIZE) { - throw new IOException("Row size " + newRowData.limit() + - " is too large"); - } - - if(!_indexDatas.isEmpty()) { - - Object[] oldRowValues = rowState.getRowValues(); - - // check foreign keys before actually updating - _fkEnforcer.updateRow(oldRowValues, row); - - // delete old values from indexes - for(IndexData indexData : _indexDatas) { - indexData.deleteRow(oldRowValues, rowId); - } - } - - // see if we can squeeze the new row data into the existing row - rowBuffer.reset(); - int rowSize = newRowData.remaining(); - - ByteBuffer dataPage = null; - int pageNumber = PageChannel.INVALID_PAGE_NUMBER; - - if(oldRowSize >= rowSize) { - - // awesome, slap it in! - rowBuffer.put(newRowData); - - // grab the page we just updated - dataPage = rowState.getFinalPage(); - pageNumber = rowState.getFinalRowId().getPageNumber(); - - } else { - - // bummer, need to find a new page for the data - dataPage = findFreeRowSpace(rowSize, null, - PageChannel.INVALID_PAGE_NUMBER); - pageNumber = _addRowBufferH.getPageNumber(); - - RowId headerRowId = rowState.getHeaderRowId(); - ByteBuffer headerPage = rowState.getHeaderPage(); - if(pageNumber == headerRowId.getPageNumber()) { - // new row is on the same page as header row, share page - dataPage = headerPage; - } - - // write out the new row data (set the deleted flag on the new data row - // so that it is ignored during normal table traversal) - int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), - DELETED_ROW_MASK); - dataPage.put(newRowData); - - // write the overflow info into the header row and clear out the - // remaining header data - rowBuffer = PageChannel.narrowBuffer( - headerPage, - findRowStart(headerPage, headerRowId.getRowNumber(), getFormat()), - findRowEnd(headerPage, headerRowId.getRowNumber(), getFormat())); - rowBuffer.put((byte)rowNum); - ByteUtil.put3ByteInt(rowBuffer, pageNumber); - ByteUtil.clearRemaining(rowBuffer); - - // set the overflow flag on the header row - int headerRowIndex = getRowStartOffset(headerRowId.getRowNumber(), - getFormat()); - headerPage.putShort(headerRowIndex, - (short)(headerPage.getShort(headerRowIndex) - | OVERFLOW_ROW_MASK)); - if(pageNumber != headerRowId.getPageNumber()) { - writeDataPage(headerPage, headerRowId.getPageNumber()); - } - } - - // update the indexes - for(IndexData indexData : _indexDatas) { - indexData.addRow(row, rowId); - } - - writeDataPage(dataPage, pageNumber); - - updateTableDefinition(0); - } - - private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, - int pageNumber) - throws IOException - { - if(dataPage == null) { - - // find last data page (Not bothering to check other pages for free - // space.) - UsageMap.PageCursor revPageCursor = _ownedPages.cursor(); - revPageCursor.afterLast(); - while(true) { - int tmpPageNumber = revPageCursor.getPreviousPage(); - if(tmpPageNumber < 0) { - break; - } - dataPage = _addRowBufferH.setPage(getPageChannel(), tmpPageNumber); - if(dataPage.get() == PageTypes.DATA) { - // found last data page, only use if actually listed in free space - // pages - if(_freeSpacePages.containsPageNumber(tmpPageNumber)) { - pageNumber = tmpPageNumber; - } - break; - } - } - - if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) { - // No data pages exist (with free space). Create a new one. - return newDataPage(); - } - - } - - if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) { - - // Last data page is full. Create a new one. - writeDataPage(dataPage, pageNumber); - _freeSpacePages.removePageNumber(pageNumber); - - dataPage = newDataPage(); - } - - return dataPage; - } - - /** - * Updates the table definition after rows are modified. - */ - private void updateTableDefinition(int rowCountInc) throws IOException - { - // load table definition - ByteBuffer tdefPage = _tableDefBufferH.setPage(getPageChannel(), - _tableDefPageNumber); - - // make sure rowcount and autonumber are up-to-date - _rowCount += rowCountInc; - tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount); - tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber); - int ctypeOff = getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER; - if(ctypeOff >= 0) { - tdefPage.putInt(ctypeOff, _lastComplexTypeAutoNumber); - } - - // write any index changes - for (IndexData indexData : _indexDatas) { - // write the unique entry count for the index to the table definition - // page - tdefPage.putInt(indexData.getUniqueEntryCountOffset(), - indexData.getUniqueEntryCount()); - // write the entry page for the index - indexData.update(); - } - - // write modified table definition - getPageChannel().writePage(tdefPage, _tableDefPageNumber); - } - - /** - * Create a new data page - * @return Page number of the new page - */ - private ByteBuffer newDataPage() throws IOException { - if (LOG.isDebugEnabled()) { - LOG.debug("Creating new data page"); - } - ByteBuffer dataPage = _addRowBufferH.setNewPage(getPageChannel()); - dataPage.put(PageTypes.DATA); //Page type - dataPage.put((byte) 1); //Unknown - dataPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space in this page - dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition - dataPage.putInt(0); //Unknown - dataPage.putShort((short)0); //Number of rows on this page - int pageNumber = _addRowBufferH.getPageNumber(); - getPageChannel().writePage(dataPage, pageNumber); - _ownedPages.addPageNumber(pageNumber); - _freeSpacePages.addPageNumber(pageNumber); - return dataPage; - } - - ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) - throws IOException - { - return createRow(rowArray, buffer, 0, Collections.emptyMap()); - } - - /** - * Serialize a row of Objects into a byte buffer. - * - * @param rowArray row data, expected to be correct length for this table - * @param buffer buffer to which to write the row data - * @param minRowSize min size for result row - * @param rawVarValues optional, pre-written values for var length columns - * (enables re-use of previously written values). - * @return the given buffer, filled with the row data - */ - private ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer, - int minRowSize, Map rawVarValues) - throws IOException - { - buffer.putShort(_maxColumnCount); - NullMask nullMask = new NullMask(_maxColumnCount); - - //Fixed length column data comes first - int fixedDataStart = buffer.position(); - int fixedDataEnd = fixedDataStart; - for (Column col : _columns) { - - if(col.isVariableLength()) { - continue; - } - - Object rowValue = col.getRowValue(rowArray); - - if (col.getType() == DataType.BOOLEAN) { - - if(Column.toBooleanValue(rowValue)) { - //Booleans are stored in the null mask - nullMask.markNotNull(col); - } - rowValue = null; - } - - if(rowValue != null) { - - // we have a value to write - nullMask.markNotNull(col); - - // remainingRowLength is ignored when writing fixed length data - buffer.position(fixedDataStart + col.getFixedDataOffset()); - buffer.put(col.write(rowValue, 0)); - } - - // always insert space for the entire fixed data column length - // (including null values), access expects the row to always be at least - // big enough to hold all fixed values - buffer.position(fixedDataStart + col.getFixedDataOffset() + - col.getLength()); - - // keep track of the end of fixed data - if(buffer.position() > fixedDataEnd) { - fixedDataEnd = buffer.position(); - } - - } - - // reposition at end of fixed data - buffer.position(fixedDataEnd); - - // only need this info if this table contains any var length data - if(_maxVarColumnCount > 0) { - - int maxRowSize = getFormat().MAX_ROW_SIZE; - - // figure out how much space remains for var length data. first, - // account for already written space - maxRowSize -= buffer.position(); - // now, account for trailer space - int trailerSize = (nullMask.byteSize() + 4 + (_maxVarColumnCount * 2)); - maxRowSize -= trailerSize; - - // for each non-null long value column we need to reserve a small - // amount of space so that we don't end up running out of row space - // later by being too greedy - for (Column varCol : _varColumns) { - if((varCol.getType().isLongValue()) && - (varCol.getRowValue(rowArray) != null)) { - maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF; - } - } - - //Now write out variable length column data - short[] varColumnOffsets = new short[_maxVarColumnCount]; - int varColumnOffsetsIndex = 0; - for (Column varCol : _varColumns) { - short offset = (short) buffer.position(); - Object rowValue = varCol.getRowValue(rowArray); - if (rowValue != null) { - // we have a value - nullMask.markNotNull(varCol); - - byte[] rawValue = null; - ByteBuffer varDataBuf = null; - if(((rawValue = rawVarValues.get(varCol)) != null) && - (rawValue.length <= maxRowSize)) { - // save time and potentially db space, re-use raw value - varDataBuf = ByteBuffer.wrap(rawValue); - } else { - // write column value - varDataBuf = varCol.write(rowValue, maxRowSize); - } - - maxRowSize -= varDataBuf.remaining(); - if(varCol.getType().isLongValue()) { - // we already accounted for some amount of the long value data - // above. add that space back so we don't double count - maxRowSize += getFormat().SIZE_LONG_VALUE_DEF; - } - buffer.put(varDataBuf); - } - - // we do a loop here so that we fill in offsets for deleted columns - while(varColumnOffsetsIndex <= varCol.getVarLenTableIndex()) { - varColumnOffsets[varColumnOffsetsIndex++] = offset; - } - } - - // fill in offsets for any remaining deleted columns - while(varColumnOffsetsIndex < varColumnOffsets.length) { - varColumnOffsets[varColumnOffsetsIndex++] = (short) buffer.position(); - } - - // record where we stopped writing - int eod = buffer.position(); - - // insert padding if necessary - padRowBuffer(buffer, minRowSize, trailerSize); - - buffer.putShort((short) eod); //EOD marker - - //Now write out variable length offsets - //Offsets are stored in reverse order - for (int i = _maxVarColumnCount - 1; i >= 0; i--) { - buffer.putShort(varColumnOffsets[i]); - } - buffer.putShort(_maxVarColumnCount); //Number of var length columns - - } else { - - // insert padding for row w/ no var cols - padRowBuffer(buffer, minRowSize, nullMask.byteSize()); - } - - nullMask.write(buffer); //Null mask - buffer.flip(); - if (LOG.isDebugEnabled()) { - LOG.debug("Creating new data block:\n" + ByteUtil.toHexString(buffer, buffer.limit())); - } - return buffer; - } - - /** - * Fill in all autonumber column values. - */ - private void handleAutoNumbersForAdd(Object[] row) - throws IOException - { - if(_autoNumColumns.isEmpty()) { - return; - } - - Object complexAutoNumber = null; - for(Column col : _autoNumColumns) { - // ignore given row value, use next autonumber - Column.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator(); - Object rowValue = null; - if(autoNumGen.getType() != DataType.COMPLEX_TYPE) { - rowValue = autoNumGen.getNext(null); - } else { - // complex type auto numbers are shared across all complex columns - // in the row - complexAutoNumber = autoNumGen.getNext(complexAutoNumber); - rowValue = complexAutoNumber; - } - col.setRowValue(row, rowValue); - } - } - - private static void padRowBuffer(ByteBuffer buffer, int minRowSize, - int trailerSize) - { - int pos = buffer.position(); - if((pos + trailerSize) < minRowSize) { - // pad the row to get to the min byte size - int padSize = minRowSize - (pos + trailerSize); - ByteUtil.clearRange(buffer, pos, pos + padSize); - ByteUtil.forward(buffer, padSize); - } - } - - /** - * @usage _general_method_ - */ - public int getRowCount() { - return _rowCount; - } - - int getNextLongAutoNumber() { - // note, the saved value is the last one handed out, so pre-increment - return ++_lastLongAutoNumber; - } - - int getLastLongAutoNumber() { - // gets the last used auto number (does not modify) - return _lastLongAutoNumber; - } - - int getNextComplexTypeAutoNumber() { - // note, the saved value is the last one handed out, so pre-increment - return ++_lastComplexTypeAutoNumber; - } - - int getLastComplexTypeAutoNumber() { - // gets the last used auto number (does not modify) - return _lastComplexTypeAutoNumber; - } - - @Override - public String toString() { - StringBuilder rtn = new StringBuilder(); - rtn.append("Type: " + _tableType + - ((_tableType == TYPE_USER) ? " (USER)" : " (SYSTEM)")); - rtn.append("\nName: " + _name); - rtn.append("\nRow count: " + _rowCount); - rtn.append("\nColumn count: " + _columns.size()); - rtn.append("\nIndex (data) count: " + _indexCount); - rtn.append("\nLogical Index count: " + _logicalIndexCount); - rtn.append("\nColumns:\n"); - for(Column col : _columns) { - rtn.append(col); - } - rtn.append("\nIndexes:\n"); - for(Index index : _indexes) { - rtn.append(index); - } - rtn.append("\nOwned pages: " + _ownedPages + "\n"); - return rtn.toString(); - } - - /** - * @return A simple String representation of the entire table in - * tab-delimited format - * @usage _general_method_ - */ - public String display() throws IOException { - return display(Long.MAX_VALUE); - } - - /** - * @param limit Maximum number of rows to display - * @return A simple String representation of the entire table in - * tab-delimited format - * @usage _general_method_ - */ - public String display(long limit) throws IOException { - reset(); - StringBuilder rtn = new StringBuilder(); - for(Iterator iter = _columns.iterator(); iter.hasNext(); ) { - Column col = iter.next(); - rtn.append(col.getName()); - if (iter.hasNext()) { - rtn.append("\t"); - } - } - rtn.append("\n"); - Map row; - int rowCount = 0; - while ((rowCount++ < limit) && (row = getNextRow()) != null) { - for(Iterator iter = row.values().iterator(); iter.hasNext(); ) { - Object obj = iter.next(); - if (obj instanceof byte[]) { - byte[] b = (byte[]) obj; - rtn.append(ByteUtil.toHexString(b)); - //This block can be used to easily dump a binary column to a file - /*java.io.File f = java.io.File.createTempFile("ole", ".bin"); - java.io.FileOutputStream out = new java.io.FileOutputStream(f); - out.write(b); - out.flush(); - out.close();*/ - } else { - rtn.append(String.valueOf(obj)); - } - if (iter.hasNext()) { - rtn.append("\t"); - } - } - rtn.append("\n"); - } - return rtn.toString(); - } - - /** - * Updates free space and row info for a new row of the given size in the - * given data page. Positions the page for writing the row data. - * @return the row number of the new row - * @usage _advanced_method_ - */ - public static int addDataPageRow(ByteBuffer dataPage, - int rowSize, - JetFormat format, - int rowFlags) - { - int rowSpaceUsage = getRowSpaceUsage(rowSize, format); - - // Decrease free space record. - short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); - dataPage.putShort(format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage - - rowSpaceUsage)); - - // Increment row count record. - short rowCount = dataPage.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); - dataPage.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, - (short) (rowCount + 1)); - - // determine row position - short rowLocation = findRowEnd(dataPage, rowCount, format); - rowLocation -= rowSize; - - // write row position - dataPage.putShort(getRowStartOffset(rowCount, format), - (short)(rowLocation | rowFlags)); - - // set position for row data - dataPage.position(rowLocation); - - return rowCount; - } - - /** - * Returns the row count for the current page. If the page is invalid - * ({@code null}) or the page is not a DATA page, 0 is returned. - */ - private static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format) - throws IOException - { - int rowsOnPage = 0; - if((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) { - rowsOnPage = rowBuffer.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); - } - return rowsOnPage; - } - - /** - * @throws IllegalStateException if the given rowId is invalid - */ - private static void requireValidRowId(RowId rowId) { - if(!rowId.isValid()) { - throw new IllegalArgumentException("Given rowId is invalid: " + rowId); - } - } - - /** - * @throws IllegalStateException if the given row is invalid or deleted - */ - private static void requireNonDeletedRow(RowState rowState, RowId rowId) { - if(!rowState.isValid()) { - throw new IllegalArgumentException( - "Given rowId is invalid for this table: " + rowId); - } - if(rowState.isDeleted()) { - throw new IllegalStateException("Row is deleted: " + rowId); - } - } - - /** - * @usage _advanced_method_ - */ - public static boolean isDeletedRow(short rowStart) { - return ((rowStart & DELETED_ROW_MASK) != 0); - } - - /** - * @usage _advanced_method_ - */ - public static boolean isOverflowRow(short rowStart) { - return ((rowStart & OVERFLOW_ROW_MASK) != 0); - } - - /** - * @usage _advanced_method_ - */ - public static short cleanRowStart(short rowStart) { - return (short)(rowStart & OFFSET_MASK); - } - - /** - * @usage _advanced_method_ - */ - public static short findRowStart(ByteBuffer buffer, int rowNum, - JetFormat format) - { - return cleanRowStart( - buffer.getShort(getRowStartOffset(rowNum, format))); - } - - /** - * @usage _advanced_method_ - */ - public static int getRowStartOffset(int rowNum, JetFormat format) - { - return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * rowNum); - } - - /** - * @usage _advanced_method_ - */ - public static short findRowEnd(ByteBuffer buffer, int rowNum, - JetFormat format) - { - return (short)((rowNum == 0) ? - format.PAGE_SIZE : - cleanRowStart( - buffer.getShort(getRowEndOffset(rowNum, format)))); - } - - /** - * @usage _advanced_method_ - */ - public static int getRowEndOffset(int rowNum, JetFormat format) - { - return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * (rowNum - 1)); - } - - /** - * @usage _advanced_method_ - */ - public static int getRowSpaceUsage(int rowSize, JetFormat format) - { - return rowSize + format.SIZE_ROW_LOCATION; - } - - /** - * @return the "AutoNumber" columns in the given collection of columns. - * @usage _advanced_method_ - */ - public static List getAutoNumberColumns(Collection columns) { - List autoCols = new ArrayList(1); - for(Column c : columns) { - if(c.isAutoNumber()) { - autoCols.add(c); - } - } - return (!autoCols.isEmpty() ? autoCols : Collections.emptyList()); - } - - /** - * Returns {@code true} if a row of the given size will fit on the given - * data page, {@code false} otherwise. - * @usage _advanced_method_ - */ - public static boolean rowFitsOnDataPage( - int rowLength, ByteBuffer dataPage, JetFormat format) - throws IOException - { - int rowSpaceUsage = getRowSpaceUsage(rowLength, format); - short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); - int rowsOnPage = getRowsOnDataPage(dataPage, format); - return ((rowSpaceUsage <= freeSpaceInPage) && - (rowsOnPage < format.MAX_NUM_ROWS_ON_DATA_PAGE)); - } - - /** - * Duplicates and returns a row of data, optionally with a longer length - * filled with {@code null}. - */ - static Object[] dupeRow(Object[] row, int newRowLength) { - Object[] copy = new Object[newRowLength]; - System.arraycopy(row, 0, copy, 0, Math.min(row.length, newRowLength)); - return copy; - } - - /** various statuses for the row data */ - private enum RowStatus { - INIT, INVALID_PAGE, INVALID_ROW, VALID, DELETED, NORMAL, OVERFLOW; - } - - /** the phases the RowState moves through as the data is parsed */ - private enum RowStateStatus { - INIT, AT_HEADER, AT_FINAL; - } - - /** - * Maintains the state of reading a row of data. - * @usage _advanced_class_ - */ - public final class RowState - { - /** Buffer used for reading the header row data pages */ - private final TempPageHolder _headerRowBufferH; - /** the header rowId */ - private RowId _headerRowId = RowId.FIRST_ROW_ID; - /** the number of rows on the header page */ - private int _rowsOnHeaderPage; - /** the rowState status */ - private RowStateStatus _status = RowStateStatus.INIT; - /** the row status */ - private RowStatus _rowStatus = RowStatus.INIT; - /** buffer used for reading overflow pages */ - private final TempPageHolder _overflowRowBufferH = - TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); - /** the row buffer which contains the final data (after following any - overflow pointers) */ - private ByteBuffer _finalRowBuffer; - /** the rowId which contains the final data (after following any overflow - pointers) */ - private RowId _finalRowId = null; - /** true if the row values array has data */ - private boolean _haveRowValues; - /** values read from the last row */ - private final Object[] _rowValues; - /** null mask for the last row */ - private NullMask _nullMask; - /** last modification count seen on the table we track this so that the - rowState can detect updates to the table and re-read any buffered - data */ - private int _lastModCount; - /** optional error handler to use when row errors are encountered */ - private ErrorHandler _errorHandler; - /** cached variable column offsets for jump-table based rows */ - private short[] _varColOffsets; - - private RowState(TempBufferHolder.Type headerType) { - _headerRowBufferH = TempPageHolder.newHolder(headerType); - _rowValues = new Object[Table.this.getColumnCount()]; - _lastModCount = Table.this._modCount; - } - - public Table getTable() { - return Table.this; - } - - public ErrorHandler getErrorHandler() { - return((_errorHandler != null) ? _errorHandler : - getTable().getErrorHandler()); - } - - public void setErrorHandler(ErrorHandler newErrorHandler) { - _errorHandler = newErrorHandler; - } - - public void reset() { - _finalRowId = null; - _finalRowBuffer = null; - _rowsOnHeaderPage = 0; - _status = RowStateStatus.INIT; - _rowStatus = RowStatus.INIT; - _varColOffsets = null; - _nullMask = null; - if(_haveRowValues) { - Arrays.fill(_rowValues, null); - _haveRowValues = false; - } - } - - public boolean isUpToDate() { - return(Table.this._modCount == _lastModCount); - } - - private void checkForModification() { - if(!isUpToDate()) { - reset(); - _headerRowBufferH.invalidate(); - _overflowRowBufferH.invalidate(); - _lastModCount = Table.this._modCount; - } - } - - private ByteBuffer getFinalPage() - throws IOException - { - if(_finalRowBuffer == null) { - // (re)load current page - _finalRowBuffer = getHeaderPage(); - } - return _finalRowBuffer; - } - - public RowId getFinalRowId() { - if(_finalRowId == null) { - _finalRowId = getHeaderRowId(); - } - return _finalRowId; - } - - private void setRowStatus(RowStatus rowStatus) { - _rowStatus = rowStatus; - } - - public boolean isValid() { - return(_rowStatus.ordinal() >= RowStatus.VALID.ordinal()); - } - - public boolean isDeleted() { - return(_rowStatus == RowStatus.DELETED); - } - - public boolean isOverflow() { - return(_rowStatus == RowStatus.OVERFLOW); - } - - public boolean isHeaderPageNumberValid() { - return(_rowStatus.ordinal() > RowStatus.INVALID_PAGE.ordinal()); - } - - public boolean isHeaderRowNumberValid() { - return(_rowStatus.ordinal() > RowStatus.INVALID_ROW.ordinal()); - } - - private void setStatus(RowStateStatus status) { - _status = status; - } - - public boolean isAtHeaderRow() { - return(_status.ordinal() >= RowStateStatus.AT_HEADER.ordinal()); - } - - public boolean isAtFinalRow() { - return(_status.ordinal() >= RowStateStatus.AT_FINAL.ordinal()); - } - - private Object setRowValue(int idx, Object value) { - _haveRowValues = true; - _rowValues[idx] = value; - return value; - } - - public Object[] getRowValues() { - return dupeRow(_rowValues, _rowValues.length); - } - - public NullMask getNullMask(ByteBuffer rowBuffer) throws IOException { - if(_nullMask == null) { - _nullMask = getRowNullMask(rowBuffer); - } - return _nullMask; - } - - private short[] getVarColOffsets() { - return _varColOffsets; - } - - private void setVarColOffsets(short[] varColOffsets) { - _varColOffsets = varColOffsets; - } - - public RowId getHeaderRowId() { - return _headerRowId; - } - - public int getRowsOnHeaderPage() { - return _rowsOnHeaderPage; - } - - private ByteBuffer getHeaderPage() - throws IOException - { - checkForModification(); - return _headerRowBufferH.getPage(getPageChannel()); - } - - private ByteBuffer setHeaderRow(RowId rowId) - throws IOException - { - checkForModification(); - - // don't do any work if we are already positioned correctly - if(isAtHeaderRow() && (getHeaderRowId().equals(rowId))) { - return(isValid() ? getHeaderPage() : null); - } - - // rejigger everything - reset(); - _headerRowId = rowId; - _finalRowId = rowId; - - int pageNumber = rowId.getPageNumber(); - int rowNumber = rowId.getRowNumber(); - if((pageNumber < 0) || !_ownedPages.containsPageNumber(pageNumber)) { - setRowStatus(RowStatus.INVALID_PAGE); - return null; - } - - _finalRowBuffer = _headerRowBufferH.setPage(getPageChannel(), - pageNumber); - _rowsOnHeaderPage = getRowsOnDataPage(_finalRowBuffer, getFormat()); - - if((rowNumber < 0) || (rowNumber >= _rowsOnHeaderPage)) { - setRowStatus(RowStatus.INVALID_ROW); - return null; - } - - setRowStatus(RowStatus.VALID); - return _finalRowBuffer; - } - - private ByteBuffer setOverflowRow(RowId rowId) - throws IOException - { - // this should never see modifications because it only happens within - // the positionAtRowData method - if(!isUpToDate()) { - throw new IllegalStateException("Table modified while searching?"); - } - if(_rowStatus != RowStatus.OVERFLOW) { - throw new IllegalStateException("Row is not an overflow row?"); - } - _finalRowId = rowId; - _finalRowBuffer = _overflowRowBufferH.setPage(getPageChannel(), - rowId.getPageNumber()); - return _finalRowBuffer; - } - - private Object handleRowError(Column column, - byte[] columnData, - Exception error) - throws IOException - { - return getErrorHandler().handleRowError(column, columnData, - this, error); - } - - @Override - public String toString() - { - return "RowState: headerRowId = " + _headerRowId + ", finalRowId = " + - _finalRowId; - } - } - + public abstract int getRowCount(); } diff --git a/src/java/com/healthmarketscience/jackcess/TableCreator.java b/src/java/com/healthmarketscience/jackcess/TableCreator.java index bc458de..bdf793e 100644 --- a/src/java/com/healthmarketscience/jackcess/TableCreator.java +++ b/src/java/com/healthmarketscience/jackcess/TableCreator.java @@ -129,7 +129,7 @@ class TableCreator _umapPageNumber = reservePageNumber(); //Write the tdef page to disk. - Table.writeTableDefinition(this); + TableImpl.writeTableDefinition(this); // update the database with the new table info _database.addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, null, null); @@ -182,7 +182,7 @@ class TableCreator } } - List autoCols = Table.getAutoNumberColumns(_columns); + List autoCols = TableImpl.getAutoNumberColumns(_columns); if(autoCols.size() > 1) { // for most autonumber types, we can only have one of each type Set autoTypes = EnumSet.noneOf(DataType.class); diff --git a/src/java/com/healthmarketscience/jackcess/TableImpl.java b/src/java/com/healthmarketscience/jackcess/TableImpl.java new file mode 100644 index 0000000..35a5067 --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/TableImpl.java @@ -0,0 +1,2412 @@ +/* +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; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A single database table + *

+ * Is not thread-safe. + * + * @author Tim McCune + * @usage _general_class_ + */ +public class TableImpl extends Table +{ + private static final Log LOG = LogFactory.getLog(TableImpl.class); + + private static final short OFFSET_MASK = (short)0x1FFF; + + private static final short DELETED_ROW_MASK = (short)0x8000; + + private static final short OVERFLOW_ROW_MASK = (short)0x4000; + + static final int MAGIC_TABLE_NUMBER = 1625; + + private static final int MAX_BYTE = 256; + + /** + * Table type code for system tables + * @usage _intermediate_class_ + */ + public static final byte TYPE_SYSTEM = 0x53; + /** + * Table type code for user tables + * @usage _intermediate_class_ + */ + public static final byte TYPE_USER = 0x4e; + + /** comparator which sorts variable length columns based on their index into + the variable length offset table */ + private static final Comparator VAR_LEN_COLUMN_COMPARATOR = + new Comparator() { + public int compare(Column c1, Column c2) { + return ((c1.getVarLenTableIndex() < c2.getVarLenTableIndex()) ? -1 : + ((c1.getVarLenTableIndex() > c2.getVarLenTableIndex()) ? 1 : + 0)); + } + }; + + /** comparator which sorts columns based on their display index */ + private static final Comparator DISPLAY_ORDER_COMPARATOR = + new Comparator() { + public int compare(Column c1, Column c2) { + return ((c1.getDisplayIndex() < c2.getDisplayIndex()) ? -1 : + ((c1.getDisplayIndex() > c2.getDisplayIndex()) ? 1 : + 0)); + } + }; + + /** owning database */ + private final DatabaseImpl _database; + /** additional table flags from the catalog entry */ + private int _flags; + /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ + private byte _tableType; + /** Number of actual indexes on the table */ + private int _indexCount; + /** Number of logical indexes for the table */ + private int _logicalIndexCount; + /** Number of rows in the table */ + private int _rowCount; + /** last long auto number for the table */ + private int _lastLongAutoNumber; + /** last complex type auto number for the table */ + private int _lastComplexTypeAutoNumber; + /** page number of the definition of this table */ + private final int _tableDefPageNumber; + /** max Number of columns in the table (includes previous deletions) */ + private short _maxColumnCount; + /** max Number of variable columns in the table */ + private short _maxVarColumnCount; + /** List of columns in this table, ordered by column number */ + private List _columns = new ArrayList(); + /** List of variable length columns in this table, ordered by offset */ + private final List _varColumns = new ArrayList(); + /** List of autonumber columns in this table, ordered by column number */ + private List _autoNumColumns; + /** List of indexes on this table (multiple logical indexes may be backed by + the same index data) */ + private final List _indexes = new ArrayList(); + /** List of index datas on this table (the actual backing data for an + index) */ + private final List _indexDatas = new ArrayList(); + /** List of columns in this table which are in one or more indexes */ + private final Set _indexColumns = new LinkedHashSet(); + /** Table name as stored in Database */ + private final String _name; + /** Usage map of pages that this table owns */ + private UsageMap _ownedPages; + /** Usage map of pages that this table owns with free space on them */ + private UsageMap _freeSpacePages; + /** modification count for the table, keeps row-states up-to-date */ + private int _modCount; + /** page buffer used to update data pages when adding rows */ + private final TempPageHolder _addRowBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** page buffer used to update the table def page */ + private final TempPageHolder _tableDefBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** buffer used to writing single rows of data */ + private final TempBufferHolder _singleRowBufferH = + TempBufferHolder.newHolder(TempBufferHolder.Type.SOFT, true); + /** "buffer" used to writing multi rows of data (will create new buffer on + every call) */ + private final TempBufferHolder _multiRowBufferH = + TempBufferHolder.newHolder(TempBufferHolder.Type.NONE, true); + /** page buffer used to write out-of-line "long value" data */ + private final TempPageHolder _longValueBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** optional error handler to use when row errors are encountered */ + private ErrorHandler _tableErrorHandler; + /** properties for this table */ + private PropertyMap _props; + /** properties group for this table (and columns) */ + private PropertyMaps _propertyMaps; + /** foreign-key enforcer for this table */ + private final FKEnforcer _fkEnforcer; + + /** common cursor for iterating through the table, kept here for historic + reasons */ + private Cursor _cursor; + + /** + * Only used by unit tests + + */ + TableImpl(boolean testing, List columns) throws IOException { + if(!testing) { + throw new IllegalArgumentException(); + } + _database = null; + _tableDefPageNumber = PageChannel.INVALID_PAGE_NUMBER; + _name = null; + setColumns(columns); + _fkEnforcer = null; + } + + /** + * @param database database which owns this table + * @param tableBuffer Buffer to read the table with + * @param pageNumber Page number of the table definition + * @param name Table name + */ + protected TableImpl(DatabaseImpl database, ByteBuffer tableBuffer, + int pageNumber, String name, int flags) + throws IOException + { + _database = database; + _tableDefPageNumber = pageNumber; + _name = name; + _flags = flags; + readTableDefinition(loadCompleteTableDefinitionBuffer(tableBuffer)); + _fkEnforcer = new FKEnforcer(this); + } + + @Override + public String getName() { + return _name; + } + + @Override + public boolean isHidden() { + return((_flags & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0); + } + + /** + * @usage _advanced_method_ + */ + public int getMaxColumnCount() { + return _maxColumnCount; + } + + @Override + public int getColumnCount() { + return _columns.size(); + } + + @Override + public DatabaseImpl getDatabase() { + return _database; + } + + /** + * @usage _advanced_method_ + */ + public JetFormat getFormat() { + return getDatabase().getFormat(); + } + + /** + * @usage _advanced_method_ + */ + public PageChannel getPageChannel() { + return getDatabase().getPageChannel(); + } + + @Override + public ErrorHandler getErrorHandler() { + return((_tableErrorHandler != null) ? _tableErrorHandler : + getDatabase().getErrorHandler()); + } + + @Override + public void setErrorHandler(ErrorHandler newErrorHandler) { + _tableErrorHandler = newErrorHandler; + } + + public int getTableDefPageNumber() { + return _tableDefPageNumber; + } + + /** + * @usage _advanced_method_ + */ + public RowState createRowState() { + return new RowState(TempBufferHolder.Type.HARD); + } + + protected UsageMap.PageCursor getOwnedPagesCursor() { + return _ownedPages.cursor(); + } + + /** + * Returns the approximate number of database pages owned by this + * table and all related indexes (this number does not take into + * account pages used for large OLE/MEMO fields). + *

+ * To calculate the approximate number of bytes owned by a table: + * + * int approxTableBytes = (table.getApproximateOwnedPageCount() * + * table.getFormat().PAGE_SIZE); + * + * @usage _intermediate_method_ + */ + public int getApproximateOwnedPageCount() { + // add a page for the table def (although that might actually be more than + // one page) + int count = _ownedPages.getPageCount() + 1; + // note, we count owned pages from _physical_ indexes, not logical indexes + // (otherwise we could double count pages) + for(IndexData indexData : _indexDatas) { + count += indexData.getOwnedPageCount(); + } + return count; + } + + protected TempPageHolder getLongValueBuffer() { + return _longValueBufferH; + } + + @Override + public List getColumns() { + return Collections.unmodifiableList(_columns); + } + + @Override + public Column getColumn(String name) { + for(Column column : _columns) { + if(column.getName().equalsIgnoreCase(name)) { + return column; + } + } + throw new IllegalArgumentException("Column with name " + name + + " does not exist in this table"); + } + + /** + * Only called by unit tests + */ + private void setColumns(List columns) { + _columns = columns; + int colIdx = 0; + int varLenIdx = 0; + int fixedOffset = 0; + for(Column col : _columns) { + col.setColumnNumber((short)colIdx); + col.setColumnIndex(colIdx++); + if(col.isVariableLength()) { + col.setVarLenTableIndex(varLenIdx++); + _varColumns.add(col); + } else { + col.setFixedDataOffset(fixedOffset); + fixedOffset += col.getType().getFixedSize(); + } + } + _maxColumnCount = (short)_columns.size(); + _maxVarColumnCount = (short)_varColumns.size(); + _autoNumColumns = getAutoNumberColumns(columns); + } + + @Override + public PropertyMap getProperties() throws IOException { + if(_props == null) { + _props = getPropertyMaps().getDefault(); + } + return _props; + } + + /** + * @return all PropertyMaps for this table (and columns) + * @usage _general_method_ + */ + protected PropertyMaps getPropertyMaps() throws IOException { + if(_propertyMaps == null) { + _propertyMaps = getDatabase().getPropertiesForObject( + _tableDefPageNumber); + } + return _propertyMaps; + } + + @Override + public List getIndexes() { + return Collections.unmodifiableList(_indexes); + } + + @Override + public Index getIndex(String name) { + for(Index index : _indexes) { + if(index.getName().equalsIgnoreCase(name)) { + return index; + } + } + throw new IllegalArgumentException("Index with name " + name + + " does not exist on this table"); + } + + @Override + public Index getPrimaryKeyIndex() { + for(Index index : _indexes) { + if(index.isPrimaryKey()) { + return index; + } + } + throw new IllegalArgumentException("Table " + getName() + + " does not have a primary key index"); + } + + @Override + public Index getForeignKeyIndex(Table otherTable) { + for(Index index : _indexes) { + if(index.isForeignKey() && (index.getReference() != null) && + (index.getReference().getOtherTablePageNumber() == + ((TableImpl)otherTable).getTableDefPageNumber())) { + return index; + } + } + throw new IllegalArgumentException( + "Table " + getName() + " does not have a foreign key reference to " + + otherTable.getName()); + } + + /** + * @return All of the IndexData on this table (unmodifiable List) + */ + List getIndexDatas() { + return Collections.unmodifiableList(_indexDatas); + } + + /** + * Only called by unit tests + */ + int getLogicalIndexCount() { + return _logicalIndexCount; + } + + private Cursor getInternalCursor() { + if(_cursor == null) { + _cursor = Cursor.createCursor(this); + } + return _cursor; + } + + /** + * After calling this method, getNextRow will return the first row in the + * table, see {@link Cursor#reset}. + * @usage _general_method_ + */ + public void reset() { + // FIXME remove internal cursor? + getInternalCursor().reset(); + } + + /** + * Delete the current row (retrieved by a call to {@link #getNextRow()}). + * @usage _general_method_ + */ + public void deleteCurrentRow() throws IOException { + // FIXME remove internal cursor? + getInternalCursor().deleteCurrentRow(); + } + + /** + * Delete the row on which the given rowState is currently positioned. + *

+ * Note, this method is not generally meant to be used directly. You should + * use the {@link #deleteCurrentRow} method or use the Cursor class, which + * allows for more complex table interactions. + * @usage _advanced_method_ + */ + public void deleteRow(RowState rowState, RowId rowId) throws IOException { + requireValidRowId(rowId); + + // ensure that the relevant row state is up-to-date + ByteBuffer rowBuffer = positionAtRowHeader(rowState, rowId); + + requireNonDeletedRow(rowState, rowId); + + // delete flag always gets set in the "header" row (even if data is on + // overflow row) + int pageNumber = rowState.getHeaderRowId().getPageNumber(); + int rowNumber = rowState.getHeaderRowId().getRowNumber(); + + // attempt to fill in index column values + Object[] rowValues = null; + if(!_indexDatas.isEmpty()) { + + // move to row data to get index values + rowBuffer = positionAtRowData(rowState, rowId); + + for(Column idxCol : _indexColumns) { + getRowColumn(getFormat(), rowBuffer, idxCol, rowState, null); + } + + // use any read rowValues to help update the indexes + rowValues = rowState.getRowValues(); + + // check foreign keys before proceeding w/ deletion + _fkEnforcer.deleteRow(rowValues); + + // move back to the header + rowBuffer = positionAtRowHeader(rowState, rowId); + } + + // finally, pull the trigger + int rowIndex = getRowStartOffset(rowNumber, getFormat()); + rowBuffer.putShort(rowIndex, (short)(rowBuffer.getShort(rowIndex) + | DELETED_ROW_MASK | OVERFLOW_ROW_MASK)); + writeDataPage(rowBuffer, pageNumber); + + // update the indexes + for(IndexData indexData : _indexDatas) { + indexData.deleteRow(rowValues, rowId); + } + + // make sure table def gets updated + updateTableDefinition(-1); + } + + /** + * @return The next row in this table (Column name -> Column value) + * @usage _general_method_ + */ + public Map getNextRow() throws IOException { + // FIXME remove internal cursor? + return getNextRow(null); + } + + /** + * @param columnNames Only column names in this collection will be returned + * @return The next row in this table (Column name -> Column value) + * @usage _general_method_ + */ + public Map getNextRow(Collection columnNames) + throws IOException + { + // FIXME remove internal cursor? + return getInternalCursor().getNextRow(columnNames); + } + + /** + * Reads a single column from the given row. + *

+ * Note, this method is not generally meant to be used directly. Instead + * use the Cursor class, which allows for more complex table interactions, + * e.g. {@link Cursor#getCurrentRowValue}. + * @usage _advanced_method_ + */ + public Object getRowValue(RowState rowState, RowId rowId, Column column) + throws IOException + { + if(this != column.getTable()) { + throw new IllegalArgumentException( + "Given column " + column + " is not from this table"); + } + requireValidRowId(rowId); + + // position at correct row + ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); + requireNonDeletedRow(rowState, rowId); + + return getRowColumn(getFormat(), rowBuffer, column, rowState, null); + } + + /** + * Reads some columns from the given row. + * @param columnNames Only column names in this collection will be returned + * @usage _advanced_method_ + */ + public Map getRow( + RowState rowState, RowId rowId, Collection columnNames) + throws IOException + { + requireValidRowId(rowId); + + // position at correct row + ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); + requireNonDeletedRow(rowState, rowId); + + return getRow(getFormat(), rowState, rowBuffer, _columns, columnNames); + } + + /** + * Reads the row data from the given row buffer. Leaves limit unchanged. + * Saves parsed row values to the given rowState. + */ + private static Map getRow( + JetFormat format, + RowState rowState, + ByteBuffer rowBuffer, + Collection columns, + Collection columnNames) + throws IOException + { + Map rtn = new LinkedHashMap( + columns.size()); + for(Column column : columns) { + + if((columnNames == null) || (columnNames.contains(column.getName()))) { + // Add the value to the row data + column.setRowValue( + rtn, getRowColumn(format, rowBuffer, column, rowState, null)); + } + } + return rtn; + } + + /** + * Reads the column data from the given row buffer. Leaves limit unchanged. + * Caches the returned value in the rowState. + */ + private static Object getRowColumn(JetFormat format, + ByteBuffer rowBuffer, + Column column, + RowState rowState, + Map rawVarValues) + throws IOException + { + byte[] columnData = null; + try { + + NullMask nullMask = rowState.getNullMask(rowBuffer); + boolean isNull = nullMask.isNull(column); + if(column.getType() == DataType.BOOLEAN) { + // Boolean values are stored in the null mask. see note about + // caching below + return rowState.setRowValue(column.getColumnIndex(), + Boolean.valueOf(!isNull)); + } else if(isNull) { + // well, that's easy! (no need to update cache w/ null) + return null; + } + + // reset position to row start + rowBuffer.reset(); + + // locate the column data bytes + int rowStart = rowBuffer.position(); + int colDataPos = 0; + int colDataLen = 0; + if(!column.isVariableLength()) { + + // read fixed length value (non-boolean at this point) + int dataStart = rowStart + format.OFFSET_COLUMN_FIXED_DATA_ROW_OFFSET; + colDataPos = dataStart + column.getFixedDataOffset(); + colDataLen = column.getType().getFixedSize(column.getLength()); + + } else { + int varDataStart; + int varDataEnd; + + if(format.SIZE_ROW_VAR_COL_OFFSET == 2) { + + // read simple var length value + int varColumnOffsetPos = + (rowBuffer.limit() - nullMask.byteSize() - 4) - + (column.getVarLenTableIndex() * 2); + + varDataStart = rowBuffer.getShort(varColumnOffsetPos); + varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2); + + } else { + + // read jump-table based var length values + short[] varColumnOffsets = readJumpTableVarColOffsets( + rowState, rowBuffer, rowStart, nullMask); + + varDataStart = varColumnOffsets[column.getVarLenTableIndex()]; + varDataEnd = varColumnOffsets[column.getVarLenTableIndex() + 1]; + } + + colDataPos = rowStart + varDataStart; + colDataLen = varDataEnd - varDataStart; + } + + // grab the column data + rowBuffer.position(colDataPos); + columnData = ByteUtil.getBytes(rowBuffer, colDataLen); + + if((rawVarValues != null) && column.isVariableLength()) { + // caller wants raw value as well + rawVarValues.put(column, columnData); + } + + // parse the column data. we cache the row values in order to be able + // to update the index on row deletion. note, most of the returned + // values are immutable, except for binary data (returned as byte[]), + // but binary data shouldn't be indexed anyway. + return rowState.setRowValue(column.getColumnIndex(), + column.read(columnData)); + + } catch(Exception e) { + + // cache "raw" row value. see note about caching above + rowState.setRowValue(column.getColumnIndex(), + Column.rawDataWrapper(columnData)); + + return rowState.handleRowError(column, columnData, e); + } + } + + private static short[] readJumpTableVarColOffsets( + RowState rowState, ByteBuffer rowBuffer, int rowStart, + NullMask nullMask) + { + short[] varColOffsets = rowState.getVarColOffsets(); + if(varColOffsets != null) { + return varColOffsets; + } + + // calculate offsets using jump-table info + int nullMaskSize = nullMask.byteSize(); + int rowEnd = rowStart + rowBuffer.remaining() - 1; + int numVarCols = ByteUtil.getUnsignedByte(rowBuffer, + rowEnd - nullMaskSize); + varColOffsets = new short[numVarCols + 1]; + + int rowLen = rowEnd - rowStart + 1; + int numJumps = (rowLen - 1) / MAX_BYTE; + int colOffset = rowEnd - nullMaskSize - numJumps - 1; + + // If last jump is a dummy value, ignore it + if(((colOffset - rowStart - numVarCols) / MAX_BYTE) < numJumps) { + numJumps--; + } + + int jumpsUsed = 0; + for(int i = 0; i < numVarCols + 1; i++) { + + while((jumpsUsed < numJumps) && + (i == ByteUtil.getUnsignedByte( + rowBuffer, rowEnd - nullMaskSize-jumpsUsed - 1))) { + jumpsUsed++; + } + + varColOffsets[i] = (short) + (ByteUtil.getUnsignedByte(rowBuffer, colOffset - i) + + (jumpsUsed * MAX_BYTE)); + } + + rowState.setVarColOffsets(varColOffsets); + return varColOffsets; + } + + /** + * Reads the null mask from the given row buffer. Leaves limit unchanged. + */ + private NullMask getRowNullMask(ByteBuffer rowBuffer) + throws IOException + { + // reset position to row start + rowBuffer.reset(); + + // Number of columns in this row + int columnCount = ByteUtil.getUnsignedVarInt( + rowBuffer, getFormat().SIZE_ROW_COLUMN_COUNT); + + // read null mask + NullMask nullMask = new NullMask(columnCount); + rowBuffer.position(rowBuffer.limit() - nullMask.byteSize()); //Null mask at end + nullMask.read(rowBuffer); + + return nullMask; + } + + /** + * Sets a new buffer to the correct row header page using the given rowState + * according to the given rowId. Deleted state is + * determined, but overflow row pointers are not followed. + * + * @return a ByteBuffer of the relevant page, or null if row was invalid + * @usage _advanced_method_ + */ + public static ByteBuffer positionAtRowHeader(RowState rowState, RowId rowId) + throws IOException + { + ByteBuffer rowBuffer = rowState.setHeaderRow(rowId); + + if(rowState.isAtHeaderRow()) { + // this task has already been accomplished + return rowBuffer; + } + + if(!rowState.isValid()) { + // this was an invalid page/row + rowState.setStatus(RowStateStatus.AT_HEADER); + return null; + } + + // note, we don't use findRowStart here cause we need the unmasked value + short rowStart = rowBuffer.getShort( + getRowStartOffset(rowId.getRowNumber(), + rowState.getTable().getFormat())); + + // check the deleted, overflow flags for the row (the "real" flags are + // always set on the header row) + RowStatus rowStatus = RowStatus.NORMAL; + if(isDeletedRow(rowStart)) { + rowStatus = RowStatus.DELETED; + } else if(isOverflowRow(rowStart)) { + rowStatus = RowStatus.OVERFLOW; + } + + rowState.setRowStatus(rowStatus); + rowState.setStatus(RowStateStatus.AT_HEADER); + return rowBuffer; + } + + /** + * Sets the position and limit in a new buffer using the given rowState + * according to the given row number and row end, following overflow row + * pointers as necessary. + * + * @return a ByteBuffer narrowed to the actual row data, or null if row was + * invalid or deleted + * @usage _advanced_method_ + */ + public static ByteBuffer positionAtRowData(RowState rowState, RowId rowId) + throws IOException + { + positionAtRowHeader(rowState, rowId); + if(!rowState.isValid() || rowState.isDeleted()) { + // row is invalid or deleted + rowState.setStatus(RowStateStatus.AT_FINAL); + return null; + } + + ByteBuffer rowBuffer = rowState.getFinalPage(); + int rowNum = rowState.getFinalRowId().getRowNumber(); + JetFormat format = rowState.getTable().getFormat(); + + if(rowState.isAtFinalRow()) { + // we've already found the final row data + return PageChannel.narrowBuffer( + rowBuffer, + findRowStart(rowBuffer, rowNum, format), + findRowEnd(rowBuffer, rowNum, format)); + } + + while(true) { + + // note, we don't use findRowStart here cause we need the unmasked value + short rowStart = rowBuffer.getShort(getRowStartOffset(rowNum, format)); + short rowEnd = findRowEnd(rowBuffer, rowNum, format); + + // note, at this point we know the row is not deleted, so ignore any + // subsequent deleted flags (as overflow rows are always marked deleted + // anyway) + boolean overflowRow = isOverflowRow(rowStart); + + // now, strip flags from rowStart offset + rowStart = (short)(rowStart & OFFSET_MASK); + + if (overflowRow) { + + if((rowEnd - rowStart) < 4) { + throw new IOException("invalid overflow row info"); + } + + // Overflow page. the "row" data in the current page points to + // another page/row + int overflowRowNum = ByteUtil.getUnsignedByte(rowBuffer, rowStart); + int overflowPageNum = ByteUtil.get3ByteInt(rowBuffer, rowStart + 1); + rowBuffer = rowState.setOverflowRow( + new RowId(overflowPageNum, overflowRowNum)); + rowNum = overflowRowNum; + + } else { + + rowState.setStatus(RowStateStatus.AT_FINAL); + return PageChannel.narrowBuffer(rowBuffer, rowStart, rowEnd); + } + } + } + + + /** + * Calls reset on this table and returns a modifiable + * Iterator which will iterate through all the rows of this table. Use of + * the Iterator follows the same restrictions as a call to + * getNextRow. + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + * @usage _general_method_ + */ + public Iterator> iterator() + { + // FIXME remove internal cursor? + return iterator(null); + } + + /** + * Calls reset on this table and returns a modifiable + * Iterator which will iterate through all the rows of this table, returning + * only the given columns. Use of the Iterator follows the same + * restrictions as a call to getNextRow. + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + * @usage _general_method_ + */ + public Iterator> iterator(Collection columnNames) + { + // FIXME remove internal cursor? + reset(); + return getInternalCursor().iterator(columnNames); + } + + /** + * Writes a new table defined by the given TableCreator to the database. + * @usage _advanced_method_ + */ + protected static void writeTableDefinition(TableCreator creator) + throws IOException + { + // first, create the usage map page + createUsageMapDefinitionBuffer(creator); + + // next, determine how big the table def will be (in case it will be more + // than one page) + JetFormat format = creator.getFormat(); + int idxDataLen = (creator.getIndexCount() * + (format.SIZE_INDEX_DEFINITION + + format.SIZE_INDEX_COLUMN_BLOCK)) + + (creator.getLogicalIndexCount() * format.SIZE_INDEX_INFO_BLOCK); + int totalTableDefSize = format.SIZE_TDEF_HEADER + + (format.SIZE_COLUMN_DEF_BLOCK * creator.getColumns().size()) + + idxDataLen + format.SIZE_TDEF_TRAILER; + + // total up the amount of space used by the column and index names (2 + // bytes per char + 2 bytes for the length) + for(Column col : creator.getColumns()) { + int nameByteLen = (col.getName().length() * + JetFormat.TEXT_FIELD_UNIT_SIZE); + totalTableDefSize += nameByteLen + 2; + } + + for(IndexBuilder idx : creator.getIndexes()) { + int nameByteLen = (idx.getName().length() * + JetFormat.TEXT_FIELD_UNIT_SIZE); + totalTableDefSize += nameByteLen + 2; + } + + + // now, create the table definition + PageChannel pageChannel = creator.getPageChannel(); + ByteBuffer buffer = pageChannel .createBuffer(Math.max(totalTableDefSize, + format.PAGE_SIZE)); + writeTableDefinitionHeader(creator, buffer, totalTableDefSize); + + if(creator.hasIndexes()) { + // index row counts + IndexData.writeRowCountDefinitions(creator, buffer); + } + + // column definitions + Column.writeDefinitions(creator, buffer); + + if(creator.hasIndexes()) { + // index and index data definitions + IndexData.writeDefinitions(creator, buffer); + Index.writeDefinitions(creator, buffer); + } + + //End of tabledef + buffer.put((byte) 0xff); + buffer.put((byte) 0xff); + + // write table buffer to database + if(totalTableDefSize <= format.PAGE_SIZE) { + + // easy case, fits on one page + buffer.putShort(format.OFFSET_FREE_SPACE, + (short)(buffer.remaining() - 8)); // overwrite page free space + // Write the tdef page to disk. + pageChannel.writePage(buffer, creator.getTdefPageNumber()); + + } else { + + // need to split across multiple pages + ByteBuffer partialTdef = pageChannel.createPageBuffer(); + buffer.rewind(); + int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; + while(buffer.hasRemaining()) { + + // reset for next write + partialTdef.clear(); + + if(nextTdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) { + + // this is the first page. note, the first page already has the + // page header, so no need to write it here + nextTdefPageNumber = creator.getTdefPageNumber(); + + } else { + + // write page header + writeTablePageHeader(partialTdef); + } + + // copy the next page of tdef bytes + int curTdefPageNumber = nextTdefPageNumber; + int writeLen = Math.min(partialTdef.remaining(), buffer.remaining()); + partialTdef.put(buffer.array(), buffer.position(), writeLen); + ByteUtil.forward(buffer, writeLen); + + if(buffer.hasRemaining()) { + // need a next page + nextTdefPageNumber = pageChannel.allocateNewPage(); + partialTdef.putInt(format.OFFSET_NEXT_TABLE_DEF_PAGE, + nextTdefPageNumber); + } + + // update page free space + partialTdef.putShort(format.OFFSET_FREE_SPACE, + (short)(partialTdef.remaining() - 8)); // overwrite page free space + + // write partial page to disk + pageChannel.writePage(partialTdef, curTdefPageNumber); + } + + } + } + + /** + * @param buffer Buffer to write to + * @param columns List of Columns in the table + */ + private static void writeTableDefinitionHeader( + TableCreator creator, ByteBuffer buffer, int totalTableDefSize) + throws IOException + { + List columns = creator.getColumns(); + + //Start writing the tdef + writeTablePageHeader(buffer); + buffer.putInt(totalTableDefSize); //Length of table def + buffer.putInt(MAGIC_TABLE_NUMBER); // seemingly constant magic value + buffer.putInt(0); //Number of rows + buffer.putInt(0); //Last Autonumber + buffer.put((byte) 1); // this makes autonumbering work in access + for (int i = 0; i < 15; i++) { //Unknown + buffer.put((byte) 0); + } + buffer.put(TYPE_USER); //Table type + buffer.putShort((short) columns.size()); //Max columns a row will have + buffer.putShort(Column.countVariableLength(columns)); //Number of variable columns in table + buffer.putShort((short) columns.size()); //Number of columns in table + buffer.putInt(creator.getLogicalIndexCount()); //Number of logical indexes in table + buffer.putInt(creator.getIndexCount()); //Number of indexes in table + buffer.put((byte) 0); //Usage map row number + ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Usage map page number + buffer.put((byte) 1); //Free map row number + ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); //Free map page number + if (LOG.isDebugEnabled()) { + int position = buffer.position(); + buffer.rewind(); + LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString( + buffer, creator.getFormat().SIZE_TDEF_HEADER)); + buffer.position(position); + } + } + + /** + * Writes the page header for a table definition page + * @param buffer Buffer to write to + */ + private static void writeTablePageHeader(ByteBuffer buffer) + { + buffer.put(PageTypes.TABLE_DEF); //Page type + buffer.put((byte) 0x01); //Unknown + buffer.put((byte) 0); //Unknown + buffer.put((byte) 0); //Unknown + buffer.putInt(0); //Next TDEF page pointer + } + + /** + * Writes the given name into the given buffer in the format as expected by + * {@link #readName}. + */ + static void writeName(ByteBuffer buffer, String name, Charset charset) + { + ByteBuffer encName = Column.encodeUncompressedText(name, charset); + buffer.putShort((short) encName.remaining()); + buffer.put(encName); + } + + /** + * Create the usage map definition page buffer. The "used pages" map is in + * row 0, the "pages with free space" map is in row 1. Index usage maps are + * in subsequent rows. + */ + private static void createUsageMapDefinitionBuffer(TableCreator creator) + throws IOException + { + // 2 table usage maps plus 1 for each index + int umapNum = 2 + creator.getIndexCount(); + + JetFormat format = creator.getFormat(); + int usageMapRowLength = format.OFFSET_USAGE_MAP_START + + format.USAGE_MAP_TABLE_BYTE_LENGTH; + int freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE + - (umapNum * getRowSpaceUsage(usageMapRowLength, format)); + + // for now, don't handle writing that many indexes + if(freeSpace < 0) { + throw new IOException("FIXME attempting to write too many indexes"); + } + + int umapPageNumber = creator.getUmapPageNumber(); + + PageChannel pageChannel = creator.getPageChannel(); + ByteBuffer rtn = pageChannel.createPageBuffer(); + rtn.put(PageTypes.DATA); + rtn.put((byte) 0x1); //Unknown + rtn.putShort((short)freeSpace); //Free space in page + rtn.putInt(0); //Table definition + rtn.putInt(0); //Unknown + rtn.putShort((short) umapNum); //Number of records on this page + + // write two rows of usage map definitions for the table + int rowStart = findRowEnd(rtn, 0, format) - usageMapRowLength; + for(int i = 0; i < 2; ++i) { + rtn.putShort(getRowStartOffset(i, format), (short)rowStart); + if(i == 0) { + // initial "usage pages" map definition + rtn.put(rowStart, UsageMap.MAP_TYPE_REFERENCE); + } else { + // initial "pages with free space" map definition + rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE); + } + rowStart -= usageMapRowLength; + } + + if(creator.hasIndexes()) { + + for(int i = 0; i < creator.getIndexes().size(); ++i) { + IndexBuilder idx = creator.getIndexes().get(i); + + // allocate root page for the index + int rootPageNumber = pageChannel.allocateNewPage(); + int umapRowNum = i + 2; + + // stash info for later use + TableCreator.IndexState idxState = creator.getIndexState(idx); + idxState.setRootPageNumber(rootPageNumber); + idxState.setUmapRowNumber((byte)umapRowNum); + idxState.setUmapPageNumber(umapPageNumber); + + // index map definition, including initial root page + rtn.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart); + rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE); + rtn.putInt(rowStart + 1, rootPageNumber); + rtn.put(rowStart + 5, (byte)1); + + rowStart -= usageMapRowLength; + } + } + + pageChannel.writePage(rtn, umapPageNumber); + } + + /** + * Returns a single ByteBuffer which contains the entire table definition + * (which may span multiple database pages). + */ + private ByteBuffer loadCompleteTableDefinitionBuffer(ByteBuffer tableBuffer) + throws IOException + { + int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); + ByteBuffer nextPageBuffer = null; + while (nextPage != 0) { + if (nextPageBuffer == null) { + nextPageBuffer = getPageChannel().createPageBuffer(); + } + getPageChannel().readPage(nextPageBuffer, nextPage); + nextPage = nextPageBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); + ByteBuffer newBuffer = getPageChannel().createBuffer( + tableBuffer.capacity() + getFormat().PAGE_SIZE - 8); + newBuffer.put(tableBuffer); + newBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8); + tableBuffer = newBuffer; + tableBuffer.flip(); + } + return tableBuffer; + } + + /** + * Read the table definition + */ + private void readTableDefinition(ByteBuffer tableBuffer) throws IOException + { + if (LOG.isDebugEnabled()) { + tableBuffer.rewind(); + LOG.debug("Table def block:\n" + ByteUtil.toHexString(tableBuffer, + getFormat().SIZE_TDEF_HEADER)); + } + _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS); + _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); + if(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) { + _lastComplexTypeAutoNumber = tableBuffer.getInt( + getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER); + } + _tableType = tableBuffer.get(getFormat().OFFSET_TABLE_TYPE); + _maxColumnCount = tableBuffer.getShort(getFormat().OFFSET_MAX_COLS); + _maxVarColumnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_VAR_COLS); + short columnCount = tableBuffer.getShort(getFormat().OFFSET_NUM_COLS); + _logicalIndexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEX_SLOTS); + _indexCount = tableBuffer.getInt(getFormat().OFFSET_NUM_INDEXES); + + int rowNum = ByteUtil.getUnsignedByte( + tableBuffer, getFormat().OFFSET_OWNED_PAGES); + int pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_OWNED_PAGES + 1); + _ownedPages = UsageMap.read(getDatabase(), pageNum, rowNum, false); + rowNum = ByteUtil.getUnsignedByte( + tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES); + pageNum = ByteUtil.get3ByteInt(tableBuffer, getFormat().OFFSET_FREE_SPACE_PAGES + 1); + _freeSpacePages = UsageMap.read(getDatabase(), pageNum, rowNum, false); + + for (int i = 0; i < _indexCount; i++) { + _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat())); + } + + int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK + + _indexCount * getFormat().SIZE_INDEX_DEFINITION; + int dispIndex = 0; + for (int i = 0; i < columnCount; i++) { + Column column = new Column(this, tableBuffer, + colOffset + (i * getFormat().SIZE_COLUMN_HEADER), dispIndex++); + _columns.add(column); + if(column.isVariableLength()) { + // also shove it in the variable columns list, which is ordered + // differently from the _columns list + _varColumns.add(column); + } + } + tableBuffer.position(colOffset + + (columnCount * getFormat().SIZE_COLUMN_HEADER)); + for (int i = 0; i < columnCount; i++) { + Column column = _columns.get(i); + column.setName(readName(tableBuffer)); + } + Collections.sort(_columns); + _autoNumColumns = getAutoNumberColumns(_columns); + + // setup the data index for the columns + int colIdx = 0; + for(Column col : _columns) { + col.setColumnIndex(colIdx++); + } + + // sort variable length columns based on their index into the variable + // length offset table, because we will write the columns in this order + Collections.sort(_varColumns, VAR_LEN_COLUMN_COMPARATOR); + + // read index column information + for (int i = 0; i < _indexCount; i++) { + IndexData idxData = _indexDatas.get(i); + idxData.read(tableBuffer, _columns); + // keep track of all columns involved in indexes + for(IndexData.ColumnDescriptor iCol : idxData.getColumns()) { + _indexColumns.add(iCol.getColumn()); + } + } + + // read logical index info (may be more logical indexes than index datas) + for (int i = 0; i < _logicalIndexCount; i++) { + _indexes.add(new Index(tableBuffer, _indexDatas, getFormat())); + } + + // read logical index names + for (int i = 0; i < _logicalIndexCount; i++) { + _indexes.get(i).setName(readName(tableBuffer)); + } + + Collections.sort(_indexes); + + // re-sort columns if necessary + if(getDatabase().getColumnOrder() != ColumnOrder.DATA) { + Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR); + } + + for(Column col : _columns) { + // some columns need to do extra work after the table is completely + // loaded + col.postTableLoadInit(); + } + } + + /** + * Writes the given page data to the given page number, clears any other + * relevant buffers. + */ + private void writeDataPage(ByteBuffer pageBuffer, int pageNumber) + throws IOException + { + // write the page data + getPageChannel().writePage(pageBuffer, pageNumber); + + // possibly invalidate the add row buffer if a different data buffer is + // being written (e.g. this happens during deleteRow) + _addRowBufferH.possiblyInvalidate(pageNumber, pageBuffer); + + // update modification count so any active RowStates can keep themselves + // up-to-date + ++_modCount; + } + + /** + * Returns a name read from the buffer at the current position. The + * expected name format is the name length followed by the name + * encoded using the {@link JetFormat#CHARSET} + */ + private String readName(ByteBuffer buffer) { + int nameLength = readNameLength(buffer); + byte[] nameBytes = ByteUtil.getBytes(buffer, nameLength); + return Column.decodeUncompressedText(nameBytes, + getDatabase().getCharset()); + } + + /** + * Returns a name length read from the buffer at the current position. + */ + private int readNameLength(ByteBuffer buffer) { + return ByteUtil.getUnsignedVarInt(buffer, getFormat().SIZE_NAME_LENGTH); + } + + @Override + public Object[] asRow(Map rowMap) { + return asRow(rowMap, null); + } + + @Override + public Object[] asUpdateRow(Map rowMap) { + return asRow(rowMap, Column.KEEP_VALUE); + } + + /** + * Converts a map of columnName -> columnValue to an array of row values. + */ + private Object[] asRow(Map rowMap, Object defaultValue) + { + Object[] row = new Object[_columns.size()]; + if(defaultValue != null) { + Arrays.fill(row, defaultValue); + } + if(rowMap == null) { + return row; + } + for(Column col : _columns) { + if(rowMap.containsKey(col.getName())) { + col.setRowValue(row, col.getRowValue(rowMap)); + } + } + return row; + } + + @Override + public void addRow(Object... row) throws IOException { + addRows(Collections.singletonList(row), _singleRowBufferH); + } + + @Override + public void addRows(List rows) throws IOException { + addRows(rows, _multiRowBufferH); + } + + /** + * Add multiple rows to this table, only writing to disk after all + * rows have been written, and every time a data page is filled. + * @param inRows List of Object[] row values + * @param writeRowBufferH TempBufferHolder used to generate buffers for + * writing the row data + */ + private void addRows(List inRows, + TempBufferHolder writeRowBufferH) + throws IOException + { + if(inRows.isEmpty()) { + return; + } + + // copy the input rows to a modifiable list so we can update the elements + List rows = new ArrayList(inRows); + ByteBuffer[] rowData = new ByteBuffer[rows.size()]; + for (int i = 0; i < rows.size(); i++) { + + // we need to make sure the row is the right length and is an Object[] + // (fill with null if too short). note, if the row is copied the caller + // will not be able to access any generated auto-number value, but if + // they need that info they should use a row array of the right + // size/type! + Object[] row = rows.get(i); + if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { + row = dupeRow(row, _columns.size()); + // we copied the row, so put the copy back into the rows list + rows.set(i, row); + } + + // fill in autonumbers + handleAutoNumbersForAdd(row); + + // write the row of data to a temporary buffer + rowData[i] = createRow(row, writeRowBufferH.getPageBuffer(getPageChannel())); + + if (rowData[i].limit() > getFormat().MAX_ROW_SIZE) { + throw new IOException("Row size " + rowData[i].limit() + + " is too large"); + } + } + + ByteBuffer dataPage = null; + int pageNumber = PageChannel.INVALID_PAGE_NUMBER; + + for (int i = 0; i < rowData.length; i++) { + int rowSize = rowData[i].remaining(); + Object[] row = rows.get(i); + + // handle foreign keys before adding to table + _fkEnforcer.addRow(row); + + // get page with space + dataPage = findFreeRowSpace(rowSize, dataPage, pageNumber); + pageNumber = _addRowBufferH.getPageNumber(); + + // write out the row data + int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), 0); + dataPage.put(rowData[i]); + + // update the indexes + RowId rowId = new RowId(pageNumber, rowNum); + for(IndexData indexData : _indexDatas) { + indexData.addRow(row, rowId); + } + } + + writeDataPage(dataPage, pageNumber); + + // Update tdef page + updateTableDefinition(rows.size()); + } + + /** + * Updates the current row to the new values. + *

+ * Note, if this table has an auto-number column(s), the existing value(s) + * will be maintained, unchanged. + * + * @param row new row values for the current row. + * @usage _general_method_ + */ + public void updateCurrentRow(Object... row) throws IOException { + // FIXME remove internal cursor? + getInternalCursor().updateCurrentRow(row); + } + + /** + * Update the row on which the given rowState is currently positioned. + *

+ * Note, this method is not generally meant to be used directly. You should + * use the {@link #updateCurrentRow} method or use the Cursor class, which + * allows for more complex table interactions, e.g. + * {@link Cursor#setCurrentRowValue} and {@link Cursor#updateCurrentRow}. + * @usage _advanced_method_ + */ + public void updateRow(RowState rowState, RowId rowId, Object... row) + throws IOException + { + requireValidRowId(rowId); + + // ensure that the relevant row state is up-to-date + ByteBuffer rowBuffer = positionAtRowData(rowState, rowId); + int oldRowSize = rowBuffer.remaining(); + + requireNonDeletedRow(rowState, rowId); + + // we need to make sure the row is the right length & type (fill with + // null if too short). + if((row.length < _columns.size()) || (row.getClass() != Object[].class)) { + row = dupeRow(row, _columns.size()); + } + + // hang on to the raw values of var length columns we are "keeping". this + // will allow us to re-use pre-written var length data, which can save + // space for things like long value columns. + Map keepRawVarValues = + (!_varColumns.isEmpty() ? new HashMap() : null); + + for(Column column : _columns) { + if(_autoNumColumns.contains(column)) { + // fill in any auto-numbers (we don't allow autonumber values to be + // modified) + column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, + rowState, null)); + } else if(column.getRowValue(row) == Column.KEEP_VALUE) { + // fill in any "keep value" fields + column.setRowValue(row, getRowColumn(getFormat(), rowBuffer, column, + rowState, keepRawVarValues)); + } else if(_indexColumns.contains(column)) { + // read row value to help update indexes + getRowColumn(getFormat(), rowBuffer, column, rowState, null); + } + } + + // generate new row bytes + ByteBuffer newRowData = createRow( + row, _singleRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, + keepRawVarValues); + + if (newRowData.limit() > getFormat().MAX_ROW_SIZE) { + throw new IOException("Row size " + newRowData.limit() + + " is too large"); + } + + if(!_indexDatas.isEmpty()) { + + Object[] oldRowValues = rowState.getRowValues(); + + // check foreign keys before actually updating + _fkEnforcer.updateRow(oldRowValues, row); + + // delete old values from indexes + for(IndexData indexData : _indexDatas) { + indexData.deleteRow(oldRowValues, rowId); + } + } + + // see if we can squeeze the new row data into the existing row + rowBuffer.reset(); + int rowSize = newRowData.remaining(); + + ByteBuffer dataPage = null; + int pageNumber = PageChannel.INVALID_PAGE_NUMBER; + + if(oldRowSize >= rowSize) { + + // awesome, slap it in! + rowBuffer.put(newRowData); + + // grab the page we just updated + dataPage = rowState.getFinalPage(); + pageNumber = rowState.getFinalRowId().getPageNumber(); + + } else { + + // bummer, need to find a new page for the data + dataPage = findFreeRowSpace(rowSize, null, + PageChannel.INVALID_PAGE_NUMBER); + pageNumber = _addRowBufferH.getPageNumber(); + + RowId headerRowId = rowState.getHeaderRowId(); + ByteBuffer headerPage = rowState.getHeaderPage(); + if(pageNumber == headerRowId.getPageNumber()) { + // new row is on the same page as header row, share page + dataPage = headerPage; + } + + // write out the new row data (set the deleted flag on the new data row + // so that it is ignored during normal table traversal) + int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), + DELETED_ROW_MASK); + dataPage.put(newRowData); + + // write the overflow info into the header row and clear out the + // remaining header data + rowBuffer = PageChannel.narrowBuffer( + headerPage, + findRowStart(headerPage, headerRowId.getRowNumber(), getFormat()), + findRowEnd(headerPage, headerRowId.getRowNumber(), getFormat())); + rowBuffer.put((byte)rowNum); + ByteUtil.put3ByteInt(rowBuffer, pageNumber); + ByteUtil.clearRemaining(rowBuffer); + + // set the overflow flag on the header row + int headerRowIndex = getRowStartOffset(headerRowId.getRowNumber(), + getFormat()); + headerPage.putShort(headerRowIndex, + (short)(headerPage.getShort(headerRowIndex) + | OVERFLOW_ROW_MASK)); + if(pageNumber != headerRowId.getPageNumber()) { + writeDataPage(headerPage, headerRowId.getPageNumber()); + } + } + + // update the indexes + for(IndexData indexData : _indexDatas) { + indexData.addRow(row, rowId); + } + + writeDataPage(dataPage, pageNumber); + + updateTableDefinition(0); + } + + private ByteBuffer findFreeRowSpace(int rowSize, ByteBuffer dataPage, + int pageNumber) + throws IOException + { + if(dataPage == null) { + + // find last data page (Not bothering to check other pages for free + // space.) + UsageMap.PageCursor revPageCursor = _ownedPages.cursor(); + revPageCursor.afterLast(); + while(true) { + int tmpPageNumber = revPageCursor.getPreviousPage(); + if(tmpPageNumber < 0) { + break; + } + dataPage = _addRowBufferH.setPage(getPageChannel(), tmpPageNumber); + if(dataPage.get() == PageTypes.DATA) { + // found last data page, only use if actually listed in free space + // pages + if(_freeSpacePages.containsPageNumber(tmpPageNumber)) { + pageNumber = tmpPageNumber; + } + break; + } + } + + if(pageNumber == PageChannel.INVALID_PAGE_NUMBER) { + // No data pages exist (with free space). Create a new one. + return newDataPage(); + } + + } + + if(!rowFitsOnDataPage(rowSize, dataPage, getFormat())) { + + // Last data page is full. Create a new one. + writeDataPage(dataPage, pageNumber); + _freeSpacePages.removePageNumber(pageNumber); + + dataPage = newDataPage(); + } + + return dataPage; + } + + /** + * Updates the table definition after rows are modified. + */ + private void updateTableDefinition(int rowCountInc) throws IOException + { + // load table definition + ByteBuffer tdefPage = _tableDefBufferH.setPage(getPageChannel(), + _tableDefPageNumber); + + // make sure rowcount and autonumber are up-to-date + _rowCount += rowCountInc; + tdefPage.putInt(getFormat().OFFSET_NUM_ROWS, _rowCount); + tdefPage.putInt(getFormat().OFFSET_NEXT_AUTO_NUMBER, _lastLongAutoNumber); + int ctypeOff = getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER; + if(ctypeOff >= 0) { + tdefPage.putInt(ctypeOff, _lastComplexTypeAutoNumber); + } + + // write any index changes + for (IndexData indexData : _indexDatas) { + // write the unique entry count for the index to the table definition + // page + tdefPage.putInt(indexData.getUniqueEntryCountOffset(), + indexData.getUniqueEntryCount()); + // write the entry page for the index + indexData.update(); + } + + // write modified table definition + getPageChannel().writePage(tdefPage, _tableDefPageNumber); + } + + /** + * Create a new data page + * @return Page number of the new page + */ + private ByteBuffer newDataPage() throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("Creating new data page"); + } + ByteBuffer dataPage = _addRowBufferH.setNewPage(getPageChannel()); + dataPage.put(PageTypes.DATA); //Page type + dataPage.put((byte) 1); //Unknown + dataPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space in this page + dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition + dataPage.putInt(0); //Unknown + dataPage.putShort((short)0); //Number of rows on this page + int pageNumber = _addRowBufferH.getPageNumber(); + getPageChannel().writePage(dataPage, pageNumber); + _ownedPages.addPageNumber(pageNumber); + _freeSpacePages.addPageNumber(pageNumber); + return dataPage; + } + + ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer) + throws IOException + { + return createRow(rowArray, buffer, 0, Collections.emptyMap()); + } + + /** + * Serialize a row of Objects into a byte buffer. + * + * @param rowArray row data, expected to be correct length for this table + * @param buffer buffer to which to write the row data + * @param minRowSize min size for result row + * @param rawVarValues optional, pre-written values for var length columns + * (enables re-use of previously written values). + * @return the given buffer, filled with the row data + */ + private ByteBuffer createRow(Object[] rowArray, ByteBuffer buffer, + int minRowSize, Map rawVarValues) + throws IOException + { + buffer.putShort(_maxColumnCount); + NullMask nullMask = new NullMask(_maxColumnCount); + + //Fixed length column data comes first + int fixedDataStart = buffer.position(); + int fixedDataEnd = fixedDataStart; + for (Column col : _columns) { + + if(col.isVariableLength()) { + continue; + } + + Object rowValue = col.getRowValue(rowArray); + + if (col.getType() == DataType.BOOLEAN) { + + if(Column.toBooleanValue(rowValue)) { + //Booleans are stored in the null mask + nullMask.markNotNull(col); + } + rowValue = null; + } + + if(rowValue != null) { + + // we have a value to write + nullMask.markNotNull(col); + + // remainingRowLength is ignored when writing fixed length data + buffer.position(fixedDataStart + col.getFixedDataOffset()); + buffer.put(col.write(rowValue, 0)); + } + + // always insert space for the entire fixed data column length + // (including null values), access expects the row to always be at least + // big enough to hold all fixed values + buffer.position(fixedDataStart + col.getFixedDataOffset() + + col.getLength()); + + // keep track of the end of fixed data + if(buffer.position() > fixedDataEnd) { + fixedDataEnd = buffer.position(); + } + + } + + // reposition at end of fixed data + buffer.position(fixedDataEnd); + + // only need this info if this table contains any var length data + if(_maxVarColumnCount > 0) { + + int maxRowSize = getFormat().MAX_ROW_SIZE; + + // figure out how much space remains for var length data. first, + // account for already written space + maxRowSize -= buffer.position(); + // now, account for trailer space + int trailerSize = (nullMask.byteSize() + 4 + (_maxVarColumnCount * 2)); + maxRowSize -= trailerSize; + + // for each non-null long value column we need to reserve a small + // amount of space so that we don't end up running out of row space + // later by being too greedy + for (Column varCol : _varColumns) { + if((varCol.getType().isLongValue()) && + (varCol.getRowValue(rowArray) != null)) { + maxRowSize -= getFormat().SIZE_LONG_VALUE_DEF; + } + } + + //Now write out variable length column data + short[] varColumnOffsets = new short[_maxVarColumnCount]; + int varColumnOffsetsIndex = 0; + for (Column varCol : _varColumns) { + short offset = (short) buffer.position(); + Object rowValue = varCol.getRowValue(rowArray); + if (rowValue != null) { + // we have a value + nullMask.markNotNull(varCol); + + byte[] rawValue = null; + ByteBuffer varDataBuf = null; + if(((rawValue = rawVarValues.get(varCol)) != null) && + (rawValue.length <= maxRowSize)) { + // save time and potentially db space, re-use raw value + varDataBuf = ByteBuffer.wrap(rawValue); + } else { + // write column value + varDataBuf = varCol.write(rowValue, maxRowSize); + } + + maxRowSize -= varDataBuf.remaining(); + if(varCol.getType().isLongValue()) { + // we already accounted for some amount of the long value data + // above. add that space back so we don't double count + maxRowSize += getFormat().SIZE_LONG_VALUE_DEF; + } + buffer.put(varDataBuf); + } + + // we do a loop here so that we fill in offsets for deleted columns + while(varColumnOffsetsIndex <= varCol.getVarLenTableIndex()) { + varColumnOffsets[varColumnOffsetsIndex++] = offset; + } + } + + // fill in offsets for any remaining deleted columns + while(varColumnOffsetsIndex < varColumnOffsets.length) { + varColumnOffsets[varColumnOffsetsIndex++] = (short) buffer.position(); + } + + // record where we stopped writing + int eod = buffer.position(); + + // insert padding if necessary + padRowBuffer(buffer, minRowSize, trailerSize); + + buffer.putShort((short) eod); //EOD marker + + //Now write out variable length offsets + //Offsets are stored in reverse order + for (int i = _maxVarColumnCount - 1; i >= 0; i--) { + buffer.putShort(varColumnOffsets[i]); + } + buffer.putShort(_maxVarColumnCount); //Number of var length columns + + } else { + + // insert padding for row w/ no var cols + padRowBuffer(buffer, minRowSize, nullMask.byteSize()); + } + + nullMask.write(buffer); //Null mask + buffer.flip(); + if (LOG.isDebugEnabled()) { + LOG.debug("Creating new data block:\n" + ByteUtil.toHexString(buffer, buffer.limit())); + } + return buffer; + } + + /** + * Fill in all autonumber column values. + */ + private void handleAutoNumbersForAdd(Object[] row) + throws IOException + { + if(_autoNumColumns.isEmpty()) { + return; + } + + Object complexAutoNumber = null; + for(Column col : _autoNumColumns) { + // ignore given row value, use next autonumber + Column.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator(); + Object rowValue = null; + if(autoNumGen.getType() != DataType.COMPLEX_TYPE) { + rowValue = autoNumGen.getNext(null); + } else { + // complex type auto numbers are shared across all complex columns + // in the row + complexAutoNumber = autoNumGen.getNext(complexAutoNumber); + rowValue = complexAutoNumber; + } + col.setRowValue(row, rowValue); + } + } + + private static void padRowBuffer(ByteBuffer buffer, int minRowSize, + int trailerSize) + { + int pos = buffer.position(); + if((pos + trailerSize) < minRowSize) { + // pad the row to get to the min byte size + int padSize = minRowSize - (pos + trailerSize); + ByteUtil.clearRange(buffer, pos, pos + padSize); + ByteUtil.forward(buffer, padSize); + } + } + + @Override + public int getRowCount() { + return _rowCount; + } + + int getNextLongAutoNumber() { + // note, the saved value is the last one handed out, so pre-increment + return ++_lastLongAutoNumber; + } + + int getLastLongAutoNumber() { + // gets the last used auto number (does not modify) + return _lastLongAutoNumber; + } + + int getNextComplexTypeAutoNumber() { + // note, the saved value is the last one handed out, so pre-increment + return ++_lastComplexTypeAutoNumber; + } + + int getLastComplexTypeAutoNumber() { + // gets the last used auto number (does not modify) + return _lastComplexTypeAutoNumber; + } + + @Override + public String toString() { + StringBuilder rtn = new StringBuilder(); + rtn.append("Type: " + _tableType + + ((_tableType == TYPE_USER) ? " (USER)" : " (SYSTEM)")); + rtn.append("\nName: " + _name); + rtn.append("\nRow count: " + _rowCount); + rtn.append("\nColumn count: " + _columns.size()); + rtn.append("\nIndex (data) count: " + _indexCount); + rtn.append("\nLogical Index count: " + _logicalIndexCount); + rtn.append("\nColumns:\n"); + for(Column col : _columns) { + rtn.append(col); + } + rtn.append("\nIndexes:\n"); + for(Index index : _indexes) { + rtn.append(index); + } + rtn.append("\nOwned pages: " + _ownedPages + "\n"); + return rtn.toString(); + } + + /** + * @return A simple String representation of the entire table in + * tab-delimited format + * @usage _general_method_ + */ + public String display() throws IOException { + return display(Long.MAX_VALUE); + } + + /** + * @param limit Maximum number of rows to display + * @return A simple String representation of the entire table in + * tab-delimited format + * @usage _general_method_ + */ + public String display(long limit) throws IOException { + reset(); + StringBuilder rtn = new StringBuilder(); + for(Iterator iter = _columns.iterator(); iter.hasNext(); ) { + Column col = iter.next(); + rtn.append(col.getName()); + if (iter.hasNext()) { + rtn.append("\t"); + } + } + rtn.append("\n"); + Map row; + int rowCount = 0; + while ((rowCount++ < limit) && (row = getNextRow()) != null) { + for(Iterator iter = row.values().iterator(); iter.hasNext(); ) { + Object obj = iter.next(); + if (obj instanceof byte[]) { + byte[] b = (byte[]) obj; + rtn.append(ByteUtil.toHexString(b)); + //This block can be used to easily dump a binary column to a file + /*java.io.File f = java.io.File.createTempFile("ole", ".bin"); + java.io.FileOutputStream out = new java.io.FileOutputStream(f); + out.write(b); + out.flush(); + out.close();*/ + } else { + rtn.append(String.valueOf(obj)); + } + if (iter.hasNext()) { + rtn.append("\t"); + } + } + rtn.append("\n"); + } + return rtn.toString(); + } + + /** + * Updates free space and row info for a new row of the given size in the + * given data page. Positions the page for writing the row data. + * @return the row number of the new row + * @usage _advanced_method_ + */ + public static int addDataPageRow(ByteBuffer dataPage, + int rowSize, + JetFormat format, + int rowFlags) + { + int rowSpaceUsage = getRowSpaceUsage(rowSize, format); + + // Decrease free space record. + short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); + dataPage.putShort(format.OFFSET_FREE_SPACE, (short) (freeSpaceInPage - + rowSpaceUsage)); + + // Increment row count record. + short rowCount = dataPage.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); + dataPage.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, + (short) (rowCount + 1)); + + // determine row position + short rowLocation = findRowEnd(dataPage, rowCount, format); + rowLocation -= rowSize; + + // write row position + dataPage.putShort(getRowStartOffset(rowCount, format), + (short)(rowLocation | rowFlags)); + + // set position for row data + dataPage.position(rowLocation); + + return rowCount; + } + + /** + * Returns the row count for the current page. If the page is invalid + * ({@code null}) or the page is not a DATA page, 0 is returned. + */ + private static int getRowsOnDataPage(ByteBuffer rowBuffer, JetFormat format) + throws IOException + { + int rowsOnPage = 0; + if((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) { + rowsOnPage = rowBuffer.getShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE); + } + return rowsOnPage; + } + + /** + * @throws IllegalStateException if the given rowId is invalid + */ + private static void requireValidRowId(RowId rowId) { + if(!rowId.isValid()) { + throw new IllegalArgumentException("Given rowId is invalid: " + rowId); + } + } + + /** + * @throws IllegalStateException if the given row is invalid or deleted + */ + private static void requireNonDeletedRow(RowState rowState, RowId rowId) { + if(!rowState.isValid()) { + throw new IllegalArgumentException( + "Given rowId is invalid for this table: " + rowId); + } + if(rowState.isDeleted()) { + throw new IllegalStateException("Row is deleted: " + rowId); + } + } + + /** + * @usage _advanced_method_ + */ + public static boolean isDeletedRow(short rowStart) { + return ((rowStart & DELETED_ROW_MASK) != 0); + } + + /** + * @usage _advanced_method_ + */ + public static boolean isOverflowRow(short rowStart) { + return ((rowStart & OVERFLOW_ROW_MASK) != 0); + } + + /** + * @usage _advanced_method_ + */ + public static short cleanRowStart(short rowStart) { + return (short)(rowStart & OFFSET_MASK); + } + + /** + * @usage _advanced_method_ + */ + public static short findRowStart(ByteBuffer buffer, int rowNum, + JetFormat format) + { + return cleanRowStart( + buffer.getShort(getRowStartOffset(rowNum, format))); + } + + /** + * @usage _advanced_method_ + */ + public static int getRowStartOffset(int rowNum, JetFormat format) + { + return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * rowNum); + } + + /** + * @usage _advanced_method_ + */ + public static short findRowEnd(ByteBuffer buffer, int rowNum, + JetFormat format) + { + return (short)((rowNum == 0) ? + format.PAGE_SIZE : + cleanRowStart( + buffer.getShort(getRowEndOffset(rowNum, format)))); + } + + /** + * @usage _advanced_method_ + */ + public static int getRowEndOffset(int rowNum, JetFormat format) + { + return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * (rowNum - 1)); + } + + /** + * @usage _advanced_method_ + */ + public static int getRowSpaceUsage(int rowSize, JetFormat format) + { + return rowSize + format.SIZE_ROW_LOCATION; + } + + /** + * @return the "AutoNumber" columns in the given collection of columns. + * @usage _advanced_method_ + */ + public static List getAutoNumberColumns(Collection columns) { + List autoCols = new ArrayList(1); + for(Column c : columns) { + if(c.isAutoNumber()) { + autoCols.add(c); + } + } + return (!autoCols.isEmpty() ? autoCols : Collections.emptyList()); + } + + /** + * Returns {@code true} if a row of the given size will fit on the given + * data page, {@code false} otherwise. + * @usage _advanced_method_ + */ + public static boolean rowFitsOnDataPage( + int rowLength, ByteBuffer dataPage, JetFormat format) + throws IOException + { + int rowSpaceUsage = getRowSpaceUsage(rowLength, format); + short freeSpaceInPage = dataPage.getShort(format.OFFSET_FREE_SPACE); + int rowsOnPage = getRowsOnDataPage(dataPage, format); + return ((rowSpaceUsage <= freeSpaceInPage) && + (rowsOnPage < format.MAX_NUM_ROWS_ON_DATA_PAGE)); + } + + /** + * Duplicates and returns a row of data, optionally with a longer length + * filled with {@code null}. + */ + static Object[] dupeRow(Object[] row, int newRowLength) { + Object[] copy = new Object[newRowLength]; + System.arraycopy(row, 0, copy, 0, Math.min(row.length, newRowLength)); + return copy; + } + + /** various statuses for the row data */ + private enum RowStatus { + INIT, INVALID_PAGE, INVALID_ROW, VALID, DELETED, NORMAL, OVERFLOW; + } + + /** the phases the RowState moves through as the data is parsed */ + private enum RowStateStatus { + INIT, AT_HEADER, AT_FINAL; + } + + /** + * Maintains the state of reading a row of data. + * @usage _advanced_class_ + */ + public final class RowState implements ErrorHandler.Location + { + /** Buffer used for reading the header row data pages */ + private final TempPageHolder _headerRowBufferH; + /** the header rowId */ + private RowId _headerRowId = RowId.FIRST_ROW_ID; + /** the number of rows on the header page */ + private int _rowsOnHeaderPage; + /** the rowState status */ + private RowStateStatus _status = RowStateStatus.INIT; + /** the row status */ + private RowStatus _rowStatus = RowStatus.INIT; + /** buffer used for reading overflow pages */ + private final TempPageHolder _overflowRowBufferH = + TempPageHolder.newHolder(TempBufferHolder.Type.SOFT); + /** the row buffer which contains the final data (after following any + overflow pointers) */ + private ByteBuffer _finalRowBuffer; + /** the rowId which contains the final data (after following any overflow + pointers) */ + private RowId _finalRowId = null; + /** true if the row values array has data */ + private boolean _haveRowValues; + /** values read from the last row */ + private final Object[] _rowValues; + /** null mask for the last row */ + private NullMask _nullMask; + /** last modification count seen on the table we track this so that the + rowState can detect updates to the table and re-read any buffered + data */ + private int _lastModCount; + /** optional error handler to use when row errors are encountered */ + private ErrorHandler _errorHandler; + /** cached variable column offsets for jump-table based rows */ + private short[] _varColOffsets; + + private RowState(TempBufferHolder.Type headerType) { + _headerRowBufferH = TempPageHolder.newHolder(headerType); + _rowValues = new Object[TableImpl.this.getColumnCount()]; + _lastModCount = TableImpl.this._modCount; + } + + public TableImpl getTable() { + return TableImpl.this; + } + + public ErrorHandler getErrorHandler() { + return((_errorHandler != null) ? _errorHandler : + getTable().getErrorHandler()); + } + + public void setErrorHandler(ErrorHandler newErrorHandler) { + _errorHandler = newErrorHandler; + } + + public void reset() { + _finalRowId = null; + _finalRowBuffer = null; + _rowsOnHeaderPage = 0; + _status = RowStateStatus.INIT; + _rowStatus = RowStatus.INIT; + _varColOffsets = null; + _nullMask = null; + if(_haveRowValues) { + Arrays.fill(_rowValues, null); + _haveRowValues = false; + } + } + + public boolean isUpToDate() { + return(TableImpl.this._modCount == _lastModCount); + } + + private void checkForModification() { + if(!isUpToDate()) { + reset(); + _headerRowBufferH.invalidate(); + _overflowRowBufferH.invalidate(); + _lastModCount = TableImpl.this._modCount; + } + } + + private ByteBuffer getFinalPage() + throws IOException + { + if(_finalRowBuffer == null) { + // (re)load current page + _finalRowBuffer = getHeaderPage(); + } + return _finalRowBuffer; + } + + public RowId getFinalRowId() { + if(_finalRowId == null) { + _finalRowId = getHeaderRowId(); + } + return _finalRowId; + } + + private void setRowStatus(RowStatus rowStatus) { + _rowStatus = rowStatus; + } + + public boolean isValid() { + return(_rowStatus.ordinal() >= RowStatus.VALID.ordinal()); + } + + public boolean isDeleted() { + return(_rowStatus == RowStatus.DELETED); + } + + public boolean isOverflow() { + return(_rowStatus == RowStatus.OVERFLOW); + } + + public boolean isHeaderPageNumberValid() { + return(_rowStatus.ordinal() > RowStatus.INVALID_PAGE.ordinal()); + } + + public boolean isHeaderRowNumberValid() { + return(_rowStatus.ordinal() > RowStatus.INVALID_ROW.ordinal()); + } + + private void setStatus(RowStateStatus status) { + _status = status; + } + + public boolean isAtHeaderRow() { + return(_status.ordinal() >= RowStateStatus.AT_HEADER.ordinal()); + } + + public boolean isAtFinalRow() { + return(_status.ordinal() >= RowStateStatus.AT_FINAL.ordinal()); + } + + private Object setRowValue(int idx, Object value) { + _haveRowValues = true; + _rowValues[idx] = value; + return value; + } + + public Object[] getRowValues() { + return dupeRow(_rowValues, _rowValues.length); + } + + public NullMask getNullMask(ByteBuffer rowBuffer) throws IOException { + if(_nullMask == null) { + _nullMask = getRowNullMask(rowBuffer); + } + return _nullMask; + } + + private short[] getVarColOffsets() { + return _varColOffsets; + } + + private void setVarColOffsets(short[] varColOffsets) { + _varColOffsets = varColOffsets; + } + + public RowId getHeaderRowId() { + return _headerRowId; + } + + public int getRowsOnHeaderPage() { + return _rowsOnHeaderPage; + } + + private ByteBuffer getHeaderPage() + throws IOException + { + checkForModification(); + return _headerRowBufferH.getPage(getPageChannel()); + } + + private ByteBuffer setHeaderRow(RowId rowId) + throws IOException + { + checkForModification(); + + // don't do any work if we are already positioned correctly + if(isAtHeaderRow() && (getHeaderRowId().equals(rowId))) { + return(isValid() ? getHeaderPage() : null); + } + + // rejigger everything + reset(); + _headerRowId = rowId; + _finalRowId = rowId; + + int pageNumber = rowId.getPageNumber(); + int rowNumber = rowId.getRowNumber(); + if((pageNumber < 0) || !_ownedPages.containsPageNumber(pageNumber)) { + setRowStatus(RowStatus.INVALID_PAGE); + return null; + } + + _finalRowBuffer = _headerRowBufferH.setPage(getPageChannel(), + pageNumber); + _rowsOnHeaderPage = getRowsOnDataPage(_finalRowBuffer, getFormat()); + + if((rowNumber < 0) || (rowNumber >= _rowsOnHeaderPage)) { + setRowStatus(RowStatus.INVALID_ROW); + return null; + } + + setRowStatus(RowStatus.VALID); + return _finalRowBuffer; + } + + private ByteBuffer setOverflowRow(RowId rowId) + throws IOException + { + // this should never see modifications because it only happens within + // the positionAtRowData method + if(!isUpToDate()) { + throw new IllegalStateException("Table modified while searching?"); + } + if(_rowStatus != RowStatus.OVERFLOW) { + throw new IllegalStateException("Row is not an overflow row?"); + } + _finalRowId = rowId; + _finalRowBuffer = _overflowRowBufferH.setPage(getPageChannel(), + rowId.getPageNumber()); + return _finalRowBuffer; + } + + private Object handleRowError(Column column, + byte[] columnData, + Exception error) + throws IOException + { + return getErrorHandler().handleRowError(column, columnData, + this, error); + } + + @Override + public String toString() + { + return "RowState: headerRowId = " + _headerRowId + ", finalRowId = " + + _finalRowId; + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/UsageMap.java b/src/java/com/healthmarketscience/jackcess/UsageMap.java index 920ce25..292c9bf 100644 --- a/src/java/com/healthmarketscience/jackcess/UsageMap.java +++ b/src/java/com/healthmarketscience/jackcess/UsageMap.java @@ -125,8 +125,8 @@ public class UsageMap PageChannel pageChannel = database.getPageChannel(); ByteBuffer tableBuffer = pageChannel.createPageBuffer(); pageChannel.readPage(tableBuffer, pageNum); - short rowStart = Table.findRowStart(tableBuffer, rowNum, format); - int rowEnd = Table.findRowEnd(tableBuffer, rowNum, format); + short rowStart = TableImpl.findRowStart(tableBuffer, rowNum, format); + int rowEnd = TableImpl.findRowEnd(tableBuffer, rowNum, format); tableBuffer.limit(rowEnd); byte mapType = tableBuffer.get(rowStart); UsageMap rtn = new UsageMap(database, tableBuffer, pageNum, rowStart); diff --git a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java index 5410e41..7335729 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/AttachmentColumnInfo.java @@ -26,7 +26,7 @@ import java.util.Map; import com.healthmarketscience.jackcess.ByteUtil; import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableImpl; /** @@ -47,7 +47,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo private final Column _fileFlagsCol; public AttachmentColumnInfo(Column column, int complexId, - Table typeObjTable, Table flatTable) + TableImpl typeObjTable, TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); @@ -183,7 +183,7 @@ public class AttachmentColumnInfo extends ComplexColumnInfo } - public static boolean isAttachmentColumn(Table typeObjTable) { + public static boolean isAttachmentColumn(TableImpl typeObjTable) { // attachment data has these columns FileURL(MEMO), FileName(TEXT), // FileType(TEXT), FileData(OLE), FileTimeStamp(SHORT_DATE_TIME), // FileFlags(LONG) diff --git a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java index 21b7b5d..d78c4a2 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/ComplexColumnInfo.java @@ -35,7 +35,7 @@ import com.healthmarketscience.jackcess.DatabaseImpl; import com.healthmarketscience.jackcess.IndexCursor; import com.healthmarketscience.jackcess.JetFormat; import com.healthmarketscience.jackcess.PageChannel; -import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableImpl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -58,7 +58,7 @@ public abstract class ComplexColumnInfo private final Column _column; private final int _complexTypeId; - private final Table _flatTable; + private final TableImpl _flatTable; private final List _typeCols; private final Column _pkCol; private final Column _complexValFkCol; @@ -66,7 +66,7 @@ public abstract class ComplexColumnInfo private IndexCursor _complexValIdCursor; protected ComplexColumnInfo(Column column, int complexTypeId, - Table typeObjTable, Table flatTable) + TableImpl typeObjTable, TableImpl flatTable) throws IOException { _column = column; @@ -109,7 +109,7 @@ public abstract class ComplexColumnInfo offset + column.getFormat().OFFSET_COLUMN_COMPLEX_ID); DatabaseImpl db = column.getDatabase(); - Table complexColumns = db.getSystemComplexColumns(); + TableImpl complexColumns = db.getSystemComplexColumns(); IndexCursor cursor = IndexCursor.createCursor( complexColumns, complexColumns.getPrimaryKeyIndex()); if(!cursor.findFirstRowByEntry(complexTypeId)) { @@ -127,8 +127,8 @@ public abstract class ComplexColumnInfo int flatTableId = (Integer)cColRow.get(COL_FLAT_TABLE_ID); int typeObjId = (Integer)cColRow.get(COL_COMPLEX_TYPE_OBJECT_ID); - Table typeObjTable = db.getTable(typeObjId); - Table flatTable = db.getTable(flatTableId); + TableImpl typeObjTable = db.getTable(typeObjId); + TableImpl flatTable = db.getTable(flatTableId); if((typeObjTable == null) || (flatTable == null)) { throw new IOException( @@ -370,7 +370,8 @@ public abstract class ComplexColumnInfo return rtn.toString(); } - protected static void diffFlatColumns(Table typeObjTable, Table flatTable, + protected static void diffFlatColumns(TableImpl typeObjTable, + TableImpl flatTable, List typeCols, List otherCols) { diff --git a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java index b1bec20..2cf504c 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/MultiValueColumnInfo.java @@ -27,7 +27,7 @@ import java.util.Set; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.DataType; -import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableImpl; /** * Complex column info for a column holding multiple values per row. @@ -43,7 +43,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo private final Column _valueCol; public MultiValueColumnInfo(Column column, int complexId, - Table typeObjTable, Table flatTable) + TableImpl typeObjTable, TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); @@ -88,7 +88,7 @@ public class MultiValueColumnInfo extends ComplexColumnInfo return new SingleValueImpl(INVALID_ID, complexValueFk, value); } - public static boolean isMultiValueColumn(Table typeObjTable) { + public static boolean isMultiValueColumn(TableImpl typeObjTable) { // if we found a single value of a "simple" type, then we are dealing with // a multi-value column List typeCols = typeObjTable.getColumns(); diff --git a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java index 03bd8b1..25a28f7 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/UnsupportedColumnInfo.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableImpl; /** * Complex column info for an unsupported complex type. @@ -35,8 +35,8 @@ import com.healthmarketscience.jackcess.Table; public class UnsupportedColumnInfo extends ComplexColumnInfo { - public UnsupportedColumnInfo(Column column, int complexId, Table typeObjTable, - Table flatTable) + public UnsupportedColumnInfo(Column column, int complexId, TableImpl typeObjTable, + TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); diff --git a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java index 8fc5622..dedcb53 100644 --- a/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java +++ b/src/java/com/healthmarketscience/jackcess/complex/VersionHistoryColumnInfo.java @@ -26,7 +26,7 @@ import java.util.List; import java.util.Map; import com.healthmarketscience.jackcess.Column; -import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableImpl; /** * Complex column info for a column which tracking the version history of an @@ -45,7 +45,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo private final Column _modifiedCol; public VersionHistoryColumnInfo(Column column, int complexId, - Table typeObjTable, Table flatTable) + TableImpl typeObjTable, TableImpl flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); @@ -151,7 +151,7 @@ public class VersionHistoryColumnInfo extends ComplexColumnInfo return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate); } - public static boolean isVersionHistoryColumn(Table typeObjTable) { + public static boolean isVersionHistoryColumn(TableImpl typeObjTable) { // version history data has these columns (MEMO), // (SHORT_DATE_TIME) List typeCols = typeObjTable.getColumns(); -- 2.39.5