diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2007-11-20 21:03:11 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2007-11-20 21:03:11 +0000 |
commit | e39f2d1d3d4d752a276daff7710a9d91a590930b (patch) | |
tree | 7326649b9b93101db2aed2eb4ee9e1e6d4d8497a /src/java/com/healthmarketscience/jackcess/Cursor.java | |
parent | b77f5f9e97ac1e27368c3b32e8cfa9a9d2f77014 (diff) | |
download | jackcess-e39f2d1d3d4d752a276daff7710a9d91a590930b.tar.gz jackcess-e39f2d1d3d4d752a276daff7710a9d91a590930b.zip |
Move table iteration out of Table and into Cursor. First stage in
offering more complicated table access.
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@178 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src/java/com/healthmarketscience/jackcess/Cursor.java')
-rw-r--r-- | src/java/com/healthmarketscience/jackcess/Cursor.java | 409 |
1 files changed, 393 insertions, 16 deletions
diff --git a/src/java/com/healthmarketscience/jackcess/Cursor.java b/src/java/com/healthmarketscience/jackcess/Cursor.java index 3a161db..e0ae0a7 100644 --- a/src/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/java/com/healthmarketscience/jackcess/Cursor.java @@ -2,31 +2,408 @@ package com.healthmarketscience.jackcess; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +import com.healthmarketscience.jackcess.Table.RowState; +import org.apache.commons.lang.ObjectUtils; + +import static com.healthmarketscience.jackcess.PageChannel.INVALID_PAGE_NUMBER; +import static com.healthmarketscience.jackcess.RowId.INVALID_ROW_NUMBER; + + /** - * Describe class Cursor here. - * + * Manages iteration for a Table. Different cursors provide different methods + * of traversing a table. Cursors should be fairly robust in the face of + * table modification during traversal (although depending on how the table is + * traversed, row updates may or may not be seen). Multiple cursors may + * traverse the same table simultaneously. + * <p> + * Is not thread-safe. * * @author james */ -public class Cursor { +public abstract class Cursor implements Iterable<Map<String, Object>> +{ + private static final int FIRST_PAGE_NUMBER = INVALID_PAGE_NUMBER; + private static final int LAST_PAGE_NUMBER = Integer.MAX_VALUE; + + public static final RowId FIRST_ROW_ID = new RowId( + FIRST_PAGE_NUMBER, INVALID_ROW_NUMBER); + + public static final RowId LAST_ROW_ID = new RowId( + LAST_PAGE_NUMBER, INVALID_ROW_NUMBER); + /** owning table */ - private final Table _table; -// /** Number of the current row in a data page */ -// private int _currentRowInPage = INVALID_ROW_NUMBER; -// /** Number of rows left to be read on the current page */ -// private short _rowsLeftOnPage = 0; -// /** State used for reading the table rows */ -// private RowState _rowState; -// /** Iterator over the pages that this table owns */ -// private UsageMap.PageIterator _ownedPagesIterator; + protected final Table _table; + /** State used for reading the table rows */ + protected final RowState _rowState; + /** the first (exclusive) row id for this iterator */ + protected final RowId _firstRowId; + /** the last (exclusive) row id for this iterator */ + protected final RowId _lastRowId; + /** the current row */ + protected RowId _currentRowId; + + protected Cursor(Table table, RowId firstRowId, RowId lastRowId) { + _table = table; + _rowState = _table.createRowState(); + _firstRowId = firstRowId; + _lastRowId = lastRowId; + _currentRowId = firstRowId; + } + /** - * Creates a new <code>Cursor</code> instance. - * + * Creates a normal, un-indexed cursor for the given table. */ - public Cursor(Table table) { - _table = table; + public static Cursor createCursor(Table table) { + return new TableScanCursor(table); + } + + public Table getTable() { + return _table; + } + + public JetFormat getFormat() { + return getTable().getFormat(); + } + + public PageChannel getPageChannel() { + return getTable().getPageChannel(); + } + + + /** + * Returns the first row id (exclusive) as defined by this cursor. + */ + protected RowId getFirstRowId() { + return _firstRowId; + } + + /** + * Returns the last row id (exclusive) as defined by this cursor. + */ + protected RowId getLastRowId() { + return _lastRowId; + } + + public void reset() { + _currentRowId = getFirstRowId(); + _rowState.reset(); + } + + /** + * Calls <code>reset</code> 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 + * <code>getNextRow</code>. + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterator<Map<String, Object>> iterator() + { + return iterator(null); + } + + /** + * Calls <code>reset</code> 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 <code>getNextRow</code>. + * @throws IllegalStateException if an IOException is thrown by one of the + * operations, the actual exception will be contained within + */ + public Iterator<Map<String, Object>> iterator(Collection<String> columnNames) + { + return new RowIterator(columnNames); + } + + /** + * Delete the current row (retrieved by a call to {@link #getNextRow}). + */ + public void deleteCurrentRow() throws IOException { + _table.deleteRow(_rowState, _currentRowId); + } + + /** + * @return The next row in this table (Column name -> Column value) + */ + public Map<String, Object> 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<String, Object> getNextRow(Collection<String> columnNames) + throws IOException + { + if(moveToNextRow()) { + return getCurrentRow(columnNames); + } + return null; + } + + /** + * Moves to the next row as defined by this cursor. + * @return {@code true} if a valid next row was found, {@code false} + * otherwise + */ + public boolean moveToNextRow() + throws IOException + { + if(_currentRowId.equals(getLastRowId())) { + // already at end + return false; + } + + _rowState.reset(); + _currentRowId = findNextRowId(_currentRowId); + return(!_currentRowId.equals(getLastRowId())); } + /** + * Moves to the first row (as defined by the cursor) where the given column + * has the given value. This may be more efficient on some cursors than + * others. + * + * @return {@code true} if a valid row was found with the given value, + * {@code false} if no row was found (and the cursor is now pointing + * past the end of the table) + */ + public boolean moveToRow(Column column, Object value) + throws IOException + { + while(moveToNextRow()) { + if(ObjectUtils.equals(value, getCurrentRowSingleColumn(column))) { + return true; + } + } + return false; + } + + /** + * Moves to the first row (as defined by the cursor) where the given columns + * have the given values. This may be more efficient on some cursors than + * others. + * + * @return {@code true} if a valid row was found with the given values, + * {@code false} if no row was found (and the cursor is now pointing + * past the end of the table) + */ + public boolean moveToRow(Map<String,Object> row) + throws IOException + { + while(moveToNextRow()) { + if(ObjectUtils.equals(row, getCurrentRow(row.keySet()))) { + return true; + } + } + return false; + } + + /** + * Skips as many rows as possible up to the given number of rows. + * @return the number of rows skipped. + */ + public int skipRows(int numRows) + throws IOException + { + int numSkippedRows = 0; + while((numSkippedRows < numRows) && moveToNextRow()) { + ++numSkippedRows; + } + return numSkippedRows; + } + + /** + * Returns the current row in this cursor (Column name -> Column value). + * @param columnNames Only column names in this collection will be returned + */ + public Map<String, Object> getCurrentRow() + throws IOException + { + return getCurrentRow(null); + } + + /** + * Returns the current row in this cursor (Column name -> Column value). + * @param columnNames Only column names in this collection will be returned + */ + public Map<String, Object> getCurrentRow(Collection<String> columnNames) + throws IOException + { + return _table.getRow(_rowState, columnNames); + } + + /** + * Returns the given column from the current row. + */ + public Object getCurrentRowSingleColumn(Column column) + throws IOException + { + return _table.getRowSingleColumn(_rowState, column); + } + + /** + * Returns {@code true} if the row is marked as deleted, {@code false} + * otherwise. This method will not modify the rowState (it only looks at + * the "main" row, which is where the deleted flag is located). + */ + protected final boolean isCurrentRowDeleted() + throws IOException + { + ByteBuffer rowBuffer = _rowState.getFinalPage(); + int rowNum = _rowState.getFinalRowNumber(); + + // note, we don't use findRowStart here cause we need the unmasked value + return Table.isDeletedRow( + rowBuffer.getShort(Table.getRowStartOffset(rowNum, getFormat()))); + } + + /** + * Returns the row count for the current page. If the page number is + * invalid or the page is not a DATA page, 0 is returned. + */ + protected final int getRowsOnCurrentDataPage(ByteBuffer rowBuffer) + throws IOException + { + int rowsOnPage = 0; + if((rowBuffer != null) && (rowBuffer.get(0) == PageTypes.DATA)) { + rowsOnPage = + rowBuffer.getShort(getFormat().OFFSET_NUM_ROWS_ON_DATA_PAGE); + } + return rowsOnPage; + } + + /** + * Finds the next non-deleted row after the given row as defined by this + * cursor and returns the id of the row. If there are no more rows, the + * returned rowId should equal the value returned by {@link #getLastRowId}. + */ + protected abstract RowId findNextRowId(RowId currentRowId) + throws IOException; + + /** + * Row iterator for this table, supports modification. + */ + private final class RowIterator implements Iterator<Map<String, Object>> + { + private Collection<String> _columnNames; + private boolean _hasNext = false; + + private RowIterator(Collection<String> columnNames) + { + try { + reset(); + _columnNames = columnNames; + _hasNext = moveToNextRow(); + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + + public boolean hasNext() { return _hasNext; } + + public void remove() { + try { + deleteCurrentRow(); + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + + public Map<String, Object> next() { + if(!hasNext()) { + throw new NoSuchElementException(); + } + try { + Map<String, Object> rtn = getCurrentRow(_columnNames); + _hasNext = moveToNextRow(); + return rtn; + } catch(IOException e) { + throw new IllegalStateException(e); + } + } + + } + + /** + * Simple un-indexed cursor. + */ + private static class TableScanCursor extends Cursor + { + /** Iterator over the pages that this table owns */ + private final UsageMap.PageIterator _ownedPagesIterator; + + private TableScanCursor(Table table) { + super(table, FIRST_ROW_ID, LAST_ROW_ID); + _ownedPagesIterator = table.getOwnedPagesIterator(); + } + + @Override + public void reset() { + _ownedPagesIterator.reset(); + super.reset(); + } + + /** + * Position the buffer at the next row in the table + * @return a ByteBuffer narrowed to the next row, or null if none + */ + @Override + protected RowId findNextRowId(RowId currentRowId) + throws IOException + { + + // prepare to read next row + _rowState.reset(); + int currentPageNumber = currentRowId.getPageNumber(); + int currentRowNumber = currentRowId.getRowNumber(); + + int rowsOnPage = getRowsOnCurrentDataPage( + _rowState.setRow(currentPageNumber, currentRowNumber)); + + // loop until we find the next valid row or run out of pages + while(true) { + + currentRowNumber++; + if(currentRowNumber < rowsOnPage) { + _rowState.setRow(currentPageNumber, currentRowNumber); + } else { + + // load next page + currentRowNumber = INVALID_ROW_NUMBER; + currentPageNumber = _ownedPagesIterator.getNextPage(); + + ByteBuffer rowBuffer = _rowState.setRow( + currentPageNumber, currentRowNumber); + if(rowBuffer == null) { + //No more owned pages. No more rows. + return getLastRowId(); + } + + // update row count + rowsOnPage = getRowsOnCurrentDataPage(rowBuffer); + + // start again from the top + continue; + } + + if(!isCurrentRowDeleted()) { + // we found a non-deleted row, return it + return new RowId(currentPageNumber, currentRowNumber); + } + } + } + + } + } |