/* 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.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * A single database table *

* Is not thread-safe. * * @author Tim McCune */ public 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; /** Table type code for system tables */ public static final byte TYPE_SYSTEM = 0x53; /** Table type code for user tables */ public static final byte TYPE_USER = 0x4e; /** comparator which sorts variable length columns vased 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)); } }; /** owning database */ private final Database _database; /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ private byte _tableType; /** Number of indexes on the table */ private int _indexCount; /** Number of index slots for the table */ private int _indexSlotCount; /** Number of rows in the table */ private int _rowCount; /** last long auto number for the table */ private int _lastLongAutoNumber; /** 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 List _varColumns = new ArrayList(); /** List of indexes on this table */ private List _indexes = new ArrayList(); /** 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); /** for now, "big index support" is optional */ private final boolean _useBigIndex; /** optional error handler to use when row errors are encountered */ private ErrorHandler _tableErrorHandler; /** 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; _useBigIndex = false; setColumns(columns); } /** * @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 * @param useBigIndex whether or not "big index support" should be enabled * for the table */ protected Table(Database database, ByteBuffer tableBuffer, int pageNumber, String name, boolean useBigIndex) throws IOException { _database = database; _tableDefPageNumber = pageNumber; _name = name; _useBigIndex = useBigIndex; 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(); } readTableDefinition(tableBuffer); tableBuffer = null; // setup common cursor _cursor = Cursor.createCursor(this); } /** * @return The name of the table */ public String getName() { return _name; } public boolean doUseBigIndex() { return _useBigIndex; } public int getMaxColumnCount() { return _maxColumnCount; } public int getColumnCount() { return _columns.size(); } public Database getDatabase() { return _database; } public JetFormat getFormat() { return getDatabase().getFormat(); } public PageChannel getPageChannel() { return getDatabase().getPageChannel(); } /** * Gets the currently configured ErrorHandler (always non-{@code null}). * This will be used to handle all errors unless overridden at the Cursor * level. */ public ErrorHandler getErrorHandler() { return((_tableErrorHandler != null) ? _tableErrorHandler : getDatabase().getErrorHandler()); } /** * Sets a new ErrorHandler. If {@code null}, resets to using the * ErrorHandler configured at the Database level. */ public void setErrorHandler(ErrorHandler newErrorHandler) { _tableErrorHandler = newErrorHandler; } protected int getTableDefPageNumber() { return _tableDefPageNumber; } public RowState createRowState() { return new RowState(TempBufferHolder.Type.HARD); } protected UsageMap.PageCursor getOwnedPagesCursor() { return _ownedPages.cursor(); } protected TempPageHolder getLongValueBuffer() { return _longValueBufferH; } /** * @return All of the columns in this table (unmodifiable List) */ public List getColumns() { return Collections.unmodifiableList(_columns); } /** * @return the column with the given name */ public Column getColumn(String name) { for(Column column : _columns) { if(column.getName().equals(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(); } /** * @return All of the Indexes on this table (unmodifiable List) */ public List getIndexes() { return Collections.unmodifiableList(_indexes); } /** * @return the index with the given name */ public Index getIndex(String name) { for(Index index : _indexes) { if(index.getName().equals(name)) { return index; } } throw new IllegalArgumentException("Index with name " + name + " does not exist on this table"); } /** * Only called by unit tests */ int getIndexSlotCount() { return _indexSlotCount; } /** * After calling this method, getNextRow will return the first row in the * table */ public void reset() { _cursor.reset(); } /** * Delete the current row (retrieved by a call to {@link #getNextRow()}). */ public void deleteCurrentRow() throws IOException { _cursor.deleteCurrentRow(); } /** * Delete the row on which the given rowState is currently positioned. */ 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(); // use any read rowValues to help update the indexes Object[] rowValues = (!_indexes.isEmpty() ? rowState.getRowValues() : null); 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(Index index : _indexes) { index.deleteRow(rowValues, rowId); } // make sure table def gets updated updateTableDefinition(-1); } /** * @return The next row in this table (Column name -> Column value) */ 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) */ public Map getNextRow(Collection columnNames) throws IOException { return _cursor.getNextRow(columnNames); } /** * Reads a single column from the given row. */ 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(rowBuffer, getRowNullMask(rowBuffer), column, rowState); } /** * Reads some columns from the given row. * @param columnNames Only column names in this collection will be returned */ 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(rowState, rowBuffer, getRowNullMask(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( RowState rowState, ByteBuffer rowBuffer, NullMask nullMask, 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 rtn.put(column.getName(), getRowColumn(rowBuffer, nullMask, column, rowState)); } } 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(ByteBuffer rowBuffer, NullMask nullMask, Column column, RowState rowState) throws IOException { byte[] columnData = null; try { 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 + 2; colDataPos = dataStart + column.getFixedDataOffset(); colDataLen = column.getType().getFixedSize(); } else { // read var length value int varColumnOffsetPos = (rowBuffer.limit() - nullMask.byteSize() - 4) - (column.getVarLenTableIndex() * 2); short varDataStart = rowBuffer.getShort(varColumnOffsetPos); short varDataEnd = rowBuffer.getShort(varColumnOffsetPos - 2); colDataPos = rowStart + varDataStart; colDataLen = varDataEnd - varDataStart; } // grab the column data columnData = new byte[colDataLen]; rowBuffer.position(colDataPos); rowBuffer.get(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); } } /** * Reads the null mask from the given row buffer. Leaves limit unchanged. */ private static NullMask getRowNullMask(ByteBuffer rowBuffer) throws IOException { // reset position to row start rowBuffer.reset(); short columnCount = rowBuffer.getShort(); // Number of columns in this row // 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 */ 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 */ 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 an unmodifiable * 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 */ public Iterator> iterator() { return iterator(null); } /** * Calls reset on this table and returns an unmodifiable * 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 */ public Iterator> iterator(Collection columnNames) { reset(); return _cursor.iterator(columnNames); } /** * Writes a new table defined by the given columns to the database. * @return the first page of the new table's definition */ public static int writeTableDefinition( List columns, PageChannel pageChannel, JetFormat format) throws IOException { // first, create the usage map page int usageMapPageNumber = pageChannel.writeNewPage( createUsageMapDefinitionBuffer(pageChannel, format)); // next, determine how big the table def will be (in case it will be more // than one page) int totalTableDefSize = format.SIZE_TDEF_HEADER + (format.SIZE_COLUMN_DEF_BLOCK * columns.size()) + format.SIZE_TDEF_TRAILER; for(Column col : columns) { // we add the number of bytes for the column name and 2 bytes for the // length of the column name int nameByteLen = (col.getName().length() * JetFormat.TEXT_FIELD_UNIT_SIZE); totalTableDefSize += nameByteLen + 2; } // now, create the table definition ByteBuffer buffer = pageChannel.createBuffer(Math.max(totalTableDefSize, format.PAGE_SIZE)); writeTableDefinitionHeader(buffer, columns, usageMapPageNumber, totalTableDefSize, format); writeColumnDefinitions(buffer, columns, format); //End of tabledef buffer.put((byte) 0xff); buffer.put((byte) 0xff); // write table buffer to database int tdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; 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. tdefPageNumber = pageChannel.writeNewPage(buffer); } 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(tdefPageNumber == 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 tdefPageNumber = pageChannel.allocateNewPage(); nextTdefPageNumber = tdefPageNumber; } 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); } } return tdefPageNumber; } /** * @param buffer Buffer to write to * @param columns List of Columns in the table */ private static void writeTableDefinitionHeader( ByteBuffer buffer, List columns, int usageMapPageNumber, int totalTableDefSize, JetFormat format) throws IOException { //Start writing the tdef writeTablePageHeader(buffer); buffer.putInt(totalTableDefSize); //Length of table def buffer.put((byte) 0x59); //Unknown buffer.put((byte) 0x06); //Unknown buffer.putShort((short) 0); //Unknown 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(0); //Number of indexes in table buffer.putInt(0); //Number of indexes in table buffer.put((byte) 0); //Usage map row number ByteUtil.put3ByteInt(buffer, usageMapPageNumber); //Usage map page number buffer.put((byte) 1); //Free map row number ByteUtil.put3ByteInt(buffer, usageMapPageNumber); //Free map page number if (LOG.isDebugEnabled()) { int position = buffer.position(); buffer.rewind(); LOG.debug("Creating new table def block:\n" + ByteUtil.toHexString( buffer, format.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 } /** * @param buffer Buffer to write to * @param columns List of Columns to write definitions for */ private static void writeColumnDefinitions( ByteBuffer buffer, List columns, JetFormat format) throws IOException { short columnNumber = (short) 0; short fixedOffset = (short) 0; short variableOffset = (short) 0; // we specifically put the "long variable" values after the normal // variable length values so that we have a better chance of fitting it // all (because "long variable" values can go in separate pages) short longVariableOffset = Column.countNonLongVariableLength(columns); for (Column col : columns) { int position = buffer.position(); buffer.put(col.getType().getValue()); buffer.put((byte) 0x59); //Unknown buffer.put((byte) 0x06); //Unknown buffer.putShort((short) 0); //Unknown buffer.putShort(columnNumber); //Column Number if (col.isVariableLength()) { if(!col.getType().isLongValue()) { buffer.putShort(variableOffset++); } else { buffer.putShort(longVariableOffset++); } } else { buffer.putShort((short) 0); } buffer.putShort(columnNumber); //Column Number again if(col.getType().getHasScalePrecision()) { buffer.put(col.getPrecision()); // numeric precision buffer.put(col.getScale()); // numeric scale } else { buffer.put((byte) 0x00); //unused buffer.put((byte) 0x00); //unused } buffer.putShort((short) 0); //Unknown buffer.put(getColumnBitFlags(col)); // misc col flags if (col.isCompressedUnicode()) { //Compressed buffer.put((byte) 1); } else { buffer.put((byte) 0); } buffer.putInt(0); //Unknown, but always 0. //Offset for fixed length columns if (col.isVariableLength()) { buffer.putShort((short) 0); } else { buffer.putShort(fixedOffset); fixedOffset += Math.max(col.getType().getFixedSize(), col.getLength()); } if(!col.getType().isLongValue()) { buffer.putShort(col.getLength()); //Column length } else { buffer.putShort((short)0x0000); // unused } columnNumber++; if (LOG.isDebugEnabled()) { LOG.debug("Creating new column def block\n" + ByteUtil.toHexString( buffer, position, format.SIZE_COLUMN_DEF_BLOCK)); } } for (Column col : columns) { writeName(buffer, col.getName(), format); } } /** * Writes the given name into the given buffer in the format as expected by * {@link #readName}. */ private static void writeName(ByteBuffer buffer, String name, JetFormat format) { ByteBuffer encName = Column.encodeUncompressedText( name, format); buffer.putShort((short) encName.remaining()); buffer.put(encName); } /** * Constructs a byte containing the flags for the given column. */ private static byte getColumnBitFlags(Column col) { byte flags = Column.UNKNOWN_FLAG_MASK; if(!col.isVariableLength()) { flags |= Column.FIXED_LEN_FLAG_MASK; } if(col.isAutoNumber()) { flags |= col.getAutoNumberGenerator().getColumnFlags(); } return flags; } /** * 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. */ private static ByteBuffer createUsageMapDefinitionBuffer( PageChannel pageChannel, JetFormat format) throws IOException { int usageMapRowLength = format.OFFSET_USAGE_MAP_START + format.USAGE_MAP_TABLE_BYTE_LENGTH; int freeSpace = format.PAGE_INITIAL_FREE_SPACE - (2 * getRowSpaceUsage(usageMapRowLength, format)); 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) 2); //Number of records on this page // write two rows of usage map definitions 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; } return rtn; } /** * 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); _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); _indexSlotCount = 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++) { int uniqueEntryCountOffset = (getFormat().OFFSET_INDEX_DEF_BLOCK + (i * getFormat().SIZE_INDEX_DEFINITION) + 4); int uniqueEntryCount = tableBuffer.getInt(uniqueEntryCountOffset); _indexes.add(createIndex(uniqueEntryCount, uniqueEntryCountOffset)); } int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK + _indexCount * getFormat().SIZE_INDEX_DEFINITION; for (int i = 0; i < columnCount; i++) { Column column = new Column(this, tableBuffer, colOffset + (i * getFormat().SIZE_COLUMN_HEADER)); _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); // 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); int idxOffset = tableBuffer.position(); tableBuffer.position(idxOffset + (getFormat().OFFSET_INDEX_NUMBER_BLOCK * _indexCount)); // if there are more index slots than indexes, the initial slots are // always empty/invalid, so we skip that data int firstRealIdx = (_indexSlotCount - _indexCount); for (int i = 0; i < _indexSlotCount; i++) { tableBuffer.getInt(); //Forward past Unknown tableBuffer.getInt(); //Forward past alternate index number int indexNumber = tableBuffer.getInt(); ByteUtil.forward(tableBuffer, 11); byte indexType = tableBuffer.get(); ByteUtil.forward(tableBuffer, 4); if(i < firstRealIdx) { // ignore this info continue; } Index index = _indexes.get(i - firstRealIdx); index.setIndexNumber(indexNumber); index.setIndexType(indexType); } // read actual index names for (int i = 0; i < _indexSlotCount; i++) { if(i < firstRealIdx) { // for each empty index slot, there is some weird sort of name, skip // it skipName(tableBuffer); continue; } _indexes.get(i - firstRealIdx) .setName(readName(tableBuffer)); } int idxEndOffset = tableBuffer.position(); Collections.sort(_indexes); // go back to index column info after sorting tableBuffer.position(idxOffset); for (int i = 0; i < _indexCount; i++) { tableBuffer.getInt(); //Forward past Unknown _indexes.get(i).read(tableBuffer, _columns); } // reset to end of index info tableBuffer.position(idxEndOffset); } /** * Creates an index with the given initial info. */ private Index createIndex(int uniqueEntryCount, int uniqueEntryCountOffset) { return(_useBigIndex ? new BigIndex(this, uniqueEntryCount, uniqueEntryCountOffset) : new SimpleIndex(this, uniqueEntryCount, uniqueEntryCountOffset)); } /** * 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 as a short followed by (length * * 2) bytes encoded using the {@link JetFormat#CHARSET} */ private String readName(ByteBuffer buffer) { int nameLength = ByteUtil.getUnsignedShort(buffer); byte[] nameBytes = new byte[nameLength]; buffer.get(nameBytes); return Column.decodeUncompressedText(nameBytes, getFormat()); } /** * Skips past a name int the buffer at the current position. The * expected name format is the same as that for {@link #readName}. */ private void skipName(ByteBuffer buffer) { int nameLength = ByteUtil.getUnsignedShort(buffer); ByteUtil.forward(buffer, nameLength); } /** * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #addRow(Object...)}. */ public Object[] asRow(Map rowMap) { Object[] row = new Object[_columns.size()]; if(rowMap == null) { return row; } for(Column col : _columns) { row[col.getColumnIndex()] = rowMap.get(col.getName()); } return row; } /** * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #updateCurrentRow(Object...)}. */ public Object[] asUpdateRow(Map rowMap) { Object[] row = new Object[_columns.size()]; Arrays.fill(row, Column.KEEP_VALUE); if(rowMap == null) { return row; } for(Column col : _columns) { if(rowMap.containsKey(col.getName())) { row[col.getColumnIndex()] = rowMap.get(col.getName()); } } return row; } /** * Add a single row to this table and write it to disk *

* Note, if this table has an auto-number column, the value written will be * put back into the given row array. * * @param row row values for a single row. the row will be modified if * this table contains an auto-number column, otherwise it * will not be modified. */ public void addRow(Object... row) throws IOException { addRows(Collections.singletonList(row), _singleRowBufferH); } /** * 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 * is much more efficient than calling addRow multiple times. *

* Note, if this table has an auto-number column, the values written will be * put back into the given row arrays. * * @param rows List of Object[] row values. the rows will be modified if * this table contains an auto-number column, otherwise they * will not be modified. */ 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 (fill with null). // 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! Object[] row = rows.get(i); if(row.length < _columns.size()) { row = dupeRow(row, _columns.size()); // we copied the row, so put the copy back into the rows list rows.set(i, row); } // write the row of data to a temporary buffer rowData[i] = createRow(row, getFormat().MAX_ROW_SIZE, writeRowBufferH.getPageBuffer(getPageChannel()), false, 0); 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(); // 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(Index index : _indexes) { index.addRow(rows.get(i), 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. */ public void updateCurrentRow(Object... row) throws IOException { _cursor.updateCurrentRow(row); } /** * Update the row on which the given rowState is currently positioned. */ 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 (fill with null). if(row.length < _columns.size()) { row = dupeRow(row, _columns.size()); } // fill in any auto-numbers (we don't allow autonumber values to be // modified) or "keep value" fields NullMask nullMask = getRowNullMask(rowBuffer); for(Column column : _columns) { if(column.isAutoNumber() || (row[column.getColumnIndex()] == Column.KEEP_VALUE)) { row[column.getColumnIndex()] = getRowColumn(rowBuffer, nullMask, column, rowState); } } // generate new row bytes ByteBuffer newRowData = createRow( row, getFormat().MAX_ROW_SIZE, _singleRowBufferH.getPageBuffer(getPageChannel()), true, oldRowSize); if (newRowData.limit() > getFormat().MAX_ROW_SIZE) { throw new IOException("Row size " + newRowData.limit() + " is too large"); } Object[] oldRowValues = (!_indexes.isEmpty() ? rowState.getRowValues() : null); // delete old values from indexes for(Index index : _indexes) { index.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(); ByteBuffer oldDataPage = rowState.getFinalPage(); int oldPageNumber = rowState.getFinalRowId().getPageNumber(); if(pageNumber == oldPageNumber) { // new row is on the same page as current row, share page dataPage = oldDataPage; } // write out the new row data (set the deleted flag on the new data row) int rowNum = addDataPageRow(dataPage, rowSize, getFormat(), DELETED_ROW_MASK); dataPage.put(newRowData); // write the overflow info into the old row and clear out the remaining // old data rowBuffer.put((byte)rowNum); ByteUtil.put3ByteInt(rowBuffer, pageNumber); ByteUtil.clearRemaining(rowBuffer); // set the overflow flag on the old row int oldRowNumber = rowState.getFinalRowId().getRowNumber(); int oldRowIndex = getRowStartOffset(oldRowNumber, getFormat()); oldDataPage.putShort(oldRowIndex, (short)(oldDataPage.getShort(oldRowIndex) | OVERFLOW_ROW_MASK)); if(pageNumber != oldPageNumber) { writeDataPage(oldDataPage, oldPageNumber); } } // update the indexes for(Index index : _indexes) { index.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); // write any index changes for (Index index : _indexes) { // write the unique entry count for the index to the table definition // page tdefPage.putInt(index.getUniqueEntryCountOffset(), index.getUniqueEntryCount()); // write the entry page for the index index.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().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; } /** * Serialize a row of Objects into a byte buffer. *

* Note, if this table has an auto-number column, the value written will be * put back into the given row array. * * @param rowArray row data, expected to be correct length for this table * @param maxRowSize max size the data can be for this row * @param buffer buffer to which to write the row data * @return the given buffer, filled with the row data */ ByteBuffer createRow(Object[] rowArray, int maxRowSize, ByteBuffer buffer, boolean isUpdate, int minRowSize) 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()) { Object rowValue = rowArray[col.getColumnIndex()]; if (col.getType() == DataType.BOOLEAN) { if(Column.toBooleanValue(rowValue)) { //Booleans are stored in the null mask nullMask.markNotNull(col); } } else { if(col.isAutoNumber() && !isUpdate) { // ignore given row value, use next autonumber rowValue = col.getAutoNumberGenerator().getNext(); // we need to stick this back in the row so that the indexes get // updated correctly (and caller can get the generated value) rowArray[col.getColumnIndex()] = rowValue; } if(rowValue != null) { // we have a value nullMask.markNotNull(col); //remainingRowLength is ignored when writing fixed length data buffer.position(fixedDataStart + col.getFixedDataOffset()); buffer.put(col.write(rowValue, 0)); // 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) { // 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()) && (rowArray[varCol.getColumnIndex()] != 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 = rowArray[varCol.getColumnIndex()]; if (rowValue != null) { // we have a value nullMask.markNotNull(varCol); ByteBuffer 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; } private 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); } } 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; } @Override public String toString() { StringBuilder rtn = new StringBuilder(); rtn.append("Type: " + _tableType); rtn.append("\nName: " + _name); rtn.append("\nRow count: " + _rowCount); rtn.append("\nColumn count: " + _columns.size()); rtn.append("\nIndex count: " + _indexCount); 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 */ 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 */ 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 */ 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); } } public static boolean isDeletedRow(short rowStart) { return ((rowStart & DELETED_ROW_MASK) != 0); } public static boolean isOverflowRow(short rowStart) { return ((rowStart & OVERFLOW_ROW_MASK) != 0); } public static short cleanRowStart(short rowStart) { return (short)(rowStart & OFFSET_MASK); } public static short findRowStart(ByteBuffer buffer, int rowNum, JetFormat format) { return cleanRowStart( buffer.getShort(getRowStartOffset(rowNum, format))); } public static int getRowStartOffset(int rowNum, JetFormat format) { return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * rowNum); } public static short findRowEnd(ByteBuffer buffer, int rowNum, JetFormat format) { return (short)((rowNum == 0) ? format.PAGE_SIZE : cleanRowStart( buffer.getShort(getRowEndOffset(rowNum, format)))); } public static int getRowEndOffset(int rowNum, JetFormat format) { return format.OFFSET_ROW_START + (format.SIZE_ROW_LOCATION * (rowNum - 1)); } public static int getRowSpaceUsage(int rowSize, JetFormat format) { return rowSize + format.SIZE_ROW_LOCATION; } /** * @return the "AutoNumber" columns in the given collection of columns. */ public static List getAutoNumberColumns(Collection columns) { List autoCols = new ArrayList(); for(Column c : columns) { if(c.isAutoNumber()) { autoCols.add(c); } } return autoCols; } /** * Returns {@code true} if a row of the given size will fit on the given * data page, {@code false} otherwise. */ 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, row.length); 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. */ 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; /** 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; 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; 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 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; } } }