com.healthmarketscience.jackcess.Cursor.java Source code

Java tutorial

Introduction

Here is the source code for com.healthmarketscience.jackcess.Cursor.java

Source

/*
Copyright (c) 2007 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.util.Arrays;
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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 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>
 * The Cursor provides a variety of static utility methods to construct
 * cursors with given characteristics or easily search for specific values.
 * For even friendlier and more flexible construction, see
 * {@link CursorBuilder}.
 * <p>
 * Is not thread-safe.
 *
 * @author James Ahlborn
 */
public abstract class Cursor implements Iterable<Map<String, Object>> {
    private static final Log LOG = LogFactory.getLog(Cursor.class);

    /** boolean value indicating forward movement */
    public static final boolean MOVE_FORWARD = true;
    /** boolean value indicating reverse movement */
    public static final boolean MOVE_REVERSE = false;

    /** first position for the TableScanCursor */
    private static final ScanPosition FIRST_SCAN_POSITION = new ScanPosition(RowId.FIRST_ROW_ID);
    /** last position for the TableScanCursor */
    private static final ScanPosition LAST_SCAN_POSITION = new ScanPosition(RowId.LAST_ROW_ID);

    /** identifier for this cursor */
    private final Id _id;
    /** owning table */
    private final Table _table;
    /** State used for reading the table rows */
    private final RowState _rowState;
    /** the first (exclusive) row id for this cursor */
    private final Position _firstPos;
    /** the last (exclusive) row id for this cursor */
    private final Position _lastPos;
    /** the previous row */
    protected Position _prevPos;
    /** the current row */
    protected Position _curPos;
    /** ColumnMatcher to be used when matching column values */
    protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE;

    protected Cursor(Id id, Table table, Position firstPos, Position lastPos) {
        _id = id;
        _table = table;
        _rowState = _table.createRowState();
        _firstPos = firstPos;
        _lastPos = lastPos;
        _curPos = firstPos;
        _prevPos = firstPos;
    }

    /**
     * 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) {
        return new TableScanCursor(table);
    }

    /**
     * Creates an indexed cursor for the given table.
     * <p>
     * Note, index based table traversal may not include all rows, as certain
     * types of indexes do not include all entries (namely, some indexes ignore
     * null entries, see {@link Index#shouldIgnoreNulls}).
     * 
     * @param table the table over which this cursor will traverse
     * @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) throws IOException {
        return IndexCursor.createCursor(table, index);
    }

    /**
     * Creates an indexed cursor for the given table, narrowed to the given
     * range.
     * <p>
     * Note, index based table traversal may not include all rows, as certain
     * types of indexes do not include all entries (namely, some indexes ignore
     * null entries, see {@link Index#shouldIgnoreNulls}).
     * 
     * @param table the table over which this cursor will traverse
     * @param index index for the table which will define traversal order as
     *              well as enhance certain lookups
     * @param startRow the first row of data for the cursor (inclusive), or
     *                 {@code null} for the first entry
     * @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, Object[] startRow, Object[] endRow)
            throws IOException {
        return IndexCursor.createCursor(table, index, startRow, endRow);
    }

    /**
     * Creates an indexed cursor for the given table, narrowed to the given
     * range.
     * <p>
     * Note, index based table traversal may not include all rows, as certain
     * types of indexes do not include all entries (namely, some indexes ignore
     * null entries, see {@link Index#shouldIgnoreNulls}).
     * 
     * @param table the table over which this cursor will traverse
     * @param index index for the table which will define traversal order as
     *              well as enhance certain lookups
     * @param startRow the first row of data for the cursor, or {@code null} for
     *                 the first entry
     * @param startInclusive whether or not startRow is inclusive or exclusive
     * @param endRow the last row of data for the cursor, or {@code null} for
     *               the last entry
     * @param endInclusive whether or not endRow is inclusive or exclusive
     */
    public static Cursor createIndexCursor(Table table, Index index, Object[] startRow, boolean startInclusive,
            Object[] endRow, boolean endInclusive) throws IOException {
        return IndexCursor.createCursor(table, index, startRow, startInclusive, endRow, endInclusive);
    }

    /**
     * Convenience method for finding a specific row in a table which matches a
     * given row "pattern".  See {@link #findFirstRow(Map)} for details on the
     * rowPattern.
     * <p>
     * Warning, this method <i>always</i> starts searching from the beginning of
     * the Table (you cannot use it to find successive matches).
     * 
     * @param table the table to search
     * @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<String, Object> findRow(Table table, Map<String, ?> rowPattern) throws IOException {
        Cursor cursor = createCursor(table);
        if (cursor.findFirstRow(rowPattern)) {
            return cursor.getCurrentRow();
        }
        return null;
    }

    /**
     * Convenience method for finding a specific row in a table which matches a
     * given row "pattern".  See {@link #findFirstRow(Column,Object)} for
     * details on the pattern.
     * <p>
     * Note, a {@code null} result value is ambiguous in that it could imply no
     * match or a matching row with {@code null} for the desired value.  If
     * distinguishing this situation is important, you will need to use a Cursor
     * directly instead of this convenience method.
     * 
     * @param table the table to search
     * @param column column whose value should be returned
     * @param columnPattern column being matched by the valuePattern
     * @param valuePattern value from the columnPattern which will match the
     *                     desired row
     * @return the matching row or {@code null} if a match could not be found.
     */
    public static Object findValue(Table table, Column column, Column columnPattern, Object valuePattern)
            throws IOException {
        Cursor cursor = createCursor(table);
        if (cursor.findFirstRow(columnPattern, valuePattern)) {
            return cursor.getCurrentRowValue(column);
        }
        return null;
    }

    /**
     * Convenience method for finding a specific row in an indexed table which
     * matches a given row "pattern".  See {@link #findFirstRow(Map)} for
     * details on the rowPattern.
     * <p>
     * Warning, this method <i>always</i> starts searching from the beginning of
     * the Table (you cannot use it to find successive matches).
     * 
     * @param table the table to search
     * @param index index to assist the search
     * @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<String, Object> findRow(Table table, Index index, Map<String, ?> rowPattern)
            throws IOException {
        Cursor cursor = createIndexCursor(table, index);
        if (cursor.findFirstRow(rowPattern)) {
            return cursor.getCurrentRow();
        }
        return null;
    }

    /**
     * Convenience method for finding a specific row in a table which matches a
     * given row "pattern".  See {@link #findFirstRow(Column,Object)} for
     * details on the pattern.
     * <p>
     * Note, a {@code null} result value is ambiguous in that it could imply no
     * match or a matching row with {@code null} for the desired value.  If
     * distinguishing this situation is important, you will need to use a Cursor
     * directly instead of this convenience method.
     * 
     * @param table the table to search
     * @param index index to assist the search
     * @param column column whose value should be returned
     * @param columnPattern column being matched by the valuePattern
     * @param valuePattern value from the columnPattern which will match the
     *                     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, Column columnPattern,
            Object valuePattern) throws IOException {
        Cursor cursor = createIndexCursor(table, index);
        if (cursor.findFirstRow(columnPattern, valuePattern)) {
            return cursor.getCurrentRowValue(column);
        }
        return null;
    }

    public Id getId() {
        return _id;
    }

    public Table getTable() {
        return _table;
    }

    public JetFormat getFormat() {
        return getTable().getFormat();
    }

    public PageChannel getPageChannel() {
        return getTable().getPageChannel();
    }

    /**
     * Gets the currently configured ErrorHandler (always non-{@code null}).
     * This will be used to handle all errors.
     */
    public ErrorHandler getErrorHandler() {
        return _rowState.getErrorHandler();
    }

    /**
     * Sets a new ErrorHandler.  If {@code null}, resets to using the
     * ErrorHandler configured at the Table level.
     */
    public void setErrorHandler(ErrorHandler newErrorHandler) {
        _rowState.setErrorHandler(newErrorHandler);
    }

    /**
     * Returns the currently configured ColumnMatcher, always non-{@code null}.
     */
    public ColumnMatcher getColumnMatcher() {
        return _columnMatcher;
    }

    /**
     * Sets a new ColumnMatcher.  If {@code null}, resets to using the
     * default matcher, {@link SimpleColumnMatcher#INSTANCE}.
     */
    public void setColumnMatcher(ColumnMatcher columnMatcher) {
        if (columnMatcher == null) {
            columnMatcher = getDefaultColumnMatcher();
        }
        _columnMatcher = columnMatcher;
    }

    /**
     * Returns the default ColumnMatcher for this Cursor.
     */
    protected ColumnMatcher getDefaultColumnMatcher() {
        return SimpleColumnMatcher.INSTANCE;
    }

    /**
     * Returns the current state of the cursor which can be restored at a future
     * point in time by a call to {@link #restoreSavepoint}.
     * <p>
     * Savepoints may be used across different cursor instances for the same
     * table, but they must have the same {@link Id}.
     */
    public Savepoint getSavepoint() {
        return new Savepoint(_id, _curPos, _prevPos);
    }

    /**
     * Moves the cursor to a savepoint previously returned from
     * {@link #getSavepoint}.
     * @throws IllegalArgumentException if the given savepoint does not have a
     *         cursorId equal to this cursor's id
     */
    public void restoreSavepoint(Savepoint savepoint) throws IOException {
        if (!_id.equals(savepoint.getCursorId())) {
            throw new IllegalArgumentException(
                    "Savepoint " + savepoint + " is not valid for this cursor with id " + _id);
        }
        restorePosition(savepoint.getCurrentPosition(), savepoint.getPreviousPosition());
    }

    /**
     * Returns the first row id (exclusive) as defined by this cursor.
     */
    protected Position getFirstPosition() {
        return _firstPos;
    }

    /**
     * Returns the last row id (exclusive) as defined by this cursor.
     */
    protected Position getLastPosition() {
        return _lastPos;
    }

    /**
     * Resets this cursor for forward traversal.  Calls {@link #beforeFirst}.
     */
    public void reset() {
        beforeFirst();
    }

    /**
     * Resets this cursor for forward traversal (sets cursor to before the first
     * row).
     */
    public void beforeFirst() {
        reset(MOVE_FORWARD);
    }

    /**
     * Resets this cursor for reverse traversal (sets cursor to after the last
     * row).
     */
    public void afterLast() {
        reset(MOVE_REVERSE);
    }

    /**
     * Returns {@code true} if the cursor is currently positioned before the
     * first row, {@code false} otherwise.
     */
    public boolean isBeforeFirst() throws IOException {
        if (getFirstPosition().equals(_curPos)) {
            return !recheckPosition(MOVE_REVERSE);
        }
        return false;
    }

    /**
     * Returns {@code true} if the cursor is currently positioned after the
     * last row, {@code false} otherwise.
     */
    public boolean isAfterLast() throws IOException {
        if (getLastPosition().equals(_curPos)) {
            return !recheckPosition(MOVE_FORWARD);
        }
        return false;
    }

    /**
     * Returns {@code true} if the row at which the cursor is currently
     * positioned is deleted, {@code false} otherwise (including invalid rows).
     */
    public boolean isCurrentRowDeleted() throws IOException {
        // 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());
        return _rowState.isDeleted();
    }

    /**
     * Resets this cursor for traversing the given direction.
     */
    protected void reset(boolean moveForward) {
        _curPos = getDirHandler(moveForward).getBeginningPosition();
        _prevPos = _curPos;
        _rowState.reset();
    }

    /**
     * Returns an Iterable whose iterator() method calls <code>afterLast</code>
     * on this cursor and returns a modifiable Iterator which will iterate
     * through all the rows of this table in reverse order.  Use of the Iterator
     * follows the same restrictions as a call to <code>getPreviousRow</code>.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> reverseIterable() {
        return reverseIterable(null);
    }

    /**
     * Returns an Iterable whose iterator() method calls <code>afterLast</code>
     * on this table and returns a modifiable Iterator which will iterate
     * through all the rows of this table in reverse order, returning only the
     * given columns.  Use of the Iterator follows the same restrictions as a
     * call to <code>getPreviousRow</code>.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> reverseIterable(final Collection<String> columnNames) {
        return new Iterable<Map<String, Object>>() {
            public Iterator<Map<String, Object>> iterator() {
                return new RowIterator(columnNames, MOVE_REVERSE);
            }
        };
    }

    /**
     * Calls <code>beforeFirst</code> on this cursor 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);
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #iterator(Collection)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> iterable(final Collection<String> columnNames) {
        return new Iterable<Map<String, Object>>() {
            public Iterator<Map<String, Object>> iterator() {
                return Cursor.this.iterator(columnNames);
            }
        };
    }

    /**
     * Calls <code>beforeFirst</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, MOVE_FORWARD);
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #columnMatchIterable(Column,Object)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> columnMatchIterable(Column columnPattern, Object valuePattern) {
        return columnMatchIterable(null, columnPattern, valuePattern);
    }

    /**
     * Calls <code>beforeFirst</code> on this cursor and returns a modifiable
     * Iterator which will iterate through all the rows of this table which
     * match the given column pattern.  Use of the Iterator follows the same
     * restrictions as a call to <code>getNextRow</code>.  See
     * {@link #findFirstRow(Column,Object)} for details on the columnPattern.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterator<Map<String, Object>> columnMatchIterator(Column columnPattern, Object valuePattern) {
        return columnMatchIterator(null, columnPattern, valuePattern);
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #columnMatchIterator(Collection,Column,Object)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> columnMatchIterable(final Collection<String> columnNames,
            final Column columnPattern, final Object valuePattern) {
        return new Iterable<Map<String, Object>>() {
            public Iterator<Map<String, Object>> iterator() {
                return Cursor.this.columnMatchIterator(columnNames, columnPattern, valuePattern);
            }
        };
    }

    /**
     * Calls <code>beforeFirst</code> on this table and returns a modifiable
     * Iterator which will iterate through all the rows of this table which
     * match the given column pattern, returning only the given columns.  Use of
     * the Iterator follows the same restrictions as a call to
     * <code>getNextRow</code>.  See {@link #findFirstRow(Column,Object)} for
     * details on the columnPattern.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterator<Map<String, Object>> columnMatchIterator(Collection<String> columnNames, Column columnPattern,
            Object valuePattern) {
        return new ColumnMatchIterator(columnNames, columnPattern, valuePattern);
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #rowMatchIterator(Map)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> rowMatchIterable(Map<String, ?> rowPattern) {
        return rowMatchIterable(null, rowPattern);
    }

    /**
     * Calls <code>beforeFirst</code> on this cursor and returns a modifiable
     * Iterator which will iterate through all the rows of this table which
     * match the given row pattern.  Use of the Iterator follows the same
     * restrictions as a call to <code>getNextRow</code>.  See
     * {@link #findFirstRow(Map)} for details on the rowPattern.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterator<Map<String, Object>> rowMatchIterator(Map<String, ?> rowPattern) {
        return rowMatchIterator(null, rowPattern);
    }

    /**
     * Returns an Iterable whose iterator() method returns the result of a call
     * to {@link #rowMatchIterator(Collection,Map)}
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterable<Map<String, Object>> rowMatchIterable(final Collection<String> columnNames,
            final Map<String, ?> rowPattern) {
        return new Iterable<Map<String, Object>>() {
            public Iterator<Map<String, Object>> iterator() {
                return Cursor.this.rowMatchIterator(columnNames, rowPattern);
            }
        };
    }

    /**
     * Calls <code>beforeFirst</code> on this table and returns a modifiable
     * Iterator which will iterate through all the rows of this table which
     * match the given row pattern, returning only the given columns.  Use of
     * the Iterator follows the same restrictions as a call to
     * <code>getNextRow</code>.  See {@link #findFirstRow(Map)} for details on
     * the rowPattern.
     * @throws IllegalStateException if an IOException is thrown by one of the
     *         operations, the actual exception will be contained within
     */
    public Iterator<Map<String, Object>> rowMatchIterator(Collection<String> columnNames,
            Map<String, ?> rowPattern) {
        return new RowMatchIterator(columnNames, rowPattern);
    }

    /**
     * Delete the current row.
     * @throws IllegalStateException if the current row is not valid (at
     *         beginning or end of table), or already deleted.
     */
    public void deleteCurrentRow() throws IOException {
        _table.deleteRow(_rowState, _curPos.getRowId());
    }

    /**
     * Update the current row.
     * @throws IllegalStateException if the current row is not valid (at
     *         beginning or end of table), or deleted.
     */
    public void updateCurrentRow(Object... row) throws IOException {
        _table.updateRow(_rowState, _curPos.getRowId(), row);
    }

    /**
     * Moves to the next row in the table and returns it.
     * @return The next row in this table (Column name -> Column value), or
     *         {@code null} if no next row is found
     */
    public Map<String, Object> getNextRow() throws IOException {
        return getNextRow(null);
    }

    /**
     * Moves to the next row in the table and returns it.
     * @param columnNames Only column names in this collection will be returned
     * @return The next row in this table (Column name -> Column value), or
     *         {@code null} if no next row is found
     */
    public Map<String, Object> getNextRow(Collection<String> columnNames) throws IOException {
        return getAnotherRow(columnNames, MOVE_FORWARD);
    }

    /**
     * Moves to the previous row in the table and returns it.
     * @return The previous row in this table (Column name -> Column value), or
     *         {@code null} if no previous row is found
     */
    public Map<String, Object> getPreviousRow() throws IOException {
        return getPreviousRow(null);
    }

    /**
     * Moves to the previous row in the table and returns it.
     * @param columnNames Only column names in this collection will be returned
     * @return The previous row in this table (Column name -> Column value), or
     *         {@code null} if no previous row is found
     */
    public Map<String, Object> getPreviousRow(Collection<String> columnNames) throws IOException {
        return getAnotherRow(columnNames, MOVE_REVERSE);
    }

    /**
     * Moves to another row in the table based on the given direction and
     * returns it.
     * @param columnNames Only column names in this collection will be returned
     * @return another row in this table (Column name -> Column value), where
     *         "next" may be backwards if moveForward is {@code false}, or
     *         {@code null} if there is not another row in the given direction.
     */
    private Map<String, Object> getAnotherRow(Collection<String> columnNames, boolean moveForward)
            throws IOException {
        if (moveToAnotherRow(moveForward)) {
            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 {
        return moveToAnotherRow(MOVE_FORWARD);
    }

    /**
     * Moves to the previous row as defined by this cursor.
     * @return {@code true} if a valid previous row was found, {@code false}
     *         otherwise
     */
    public boolean moveToPreviousRow() throws IOException {
        return moveToAnotherRow(MOVE_REVERSE);
    }

    /**
     * Moves to another row in the given direction as defined by this cursor.
     * @return {@code true} if another valid row was found in the given
     *         direction, {@code false} otherwise
     */
    private boolean moveToAnotherRow(boolean moveForward) throws IOException {
        if (_curPos.equals(getDirHandler(moveForward).getEndPosition())) {
            // already at end, make sure nothing has changed
            return recheckPosition(moveForward);
        }

        return moveToAnotherRowImpl(moveForward);
    }

    /**
     * Restores a current position for the cursor (current position becomes
     * previous position).
     */
    protected void restorePosition(Position curPos) throws IOException {
        restorePosition(curPos, _curPos);
    }

    /**
     * Restores a current and previous position for the cursor if the given
     * positions are different from the current positions.
     */
    protected final void restorePosition(Position curPos, Position prevPos) throws IOException {
        if (!curPos.equals(_curPos) || !prevPos.equals(_prevPos)) {
            restorePositionImpl(curPos, prevPos);
        }
    }

    /**
     * Restores a current and previous position for the cursor.
     */
    protected void restorePositionImpl(Position curPos, Position prevPos) throws IOException {
        // make the current position previous, and the new position current
        _prevPos = _curPos;
        _curPos = curPos;
        _rowState.reset();
    }

    /**
     * Rechecks the current position if the underlying data structures have been
     * modified.
     * @return {@code true} if the cursor ended up in a new position,
     *         {@code false} otherwise.
     */
    private boolean recheckPosition(boolean moveForward) throws IOException {
        if (isUpToDate()) {
            // nothing has changed
            return false;
        }

        // move the cursor back to the previous position
        restorePosition(_prevPos);
        return moveToAnotherRowImpl(moveForward);
    }

    /**
     * Does the grunt work of moving the cursor to another position in the given
     * direction.
     */
    private boolean moveToAnotherRowImpl(boolean moveForward) throws IOException {
        _rowState.reset();
        _prevPos = _curPos;
        _curPos = findAnotherPosition(_rowState, _curPos, moveForward);
        Table.positionAtRowHeader(_rowState, _curPos.getRowId());
        return (!_curPos.equals(getDirHandler(moveForward).getEndPosition()));
    }

    /**
     * @deprecated renamed to {@link #findFirstRow(Column,Object)} to be more clear
     */
    @Deprecated
    public boolean findRow(Column columnPattern, Object valuePattern) throws IOException {
        return findFirstRow(columnPattern, valuePattern);
    }

    /**
     * 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.  If a match is not found (or an exception is thrown), the cursor
     * is restored to its previous state.
     * <p>
     * Warning, this method <i>always</i> starts searching from the beginning of
     * the Table (you cannot use it to find successive matches).
     *
     * @param columnPattern column from the table for this cursor which is being
     *                      matched by the valuePattern
     * @param valuePattern value which is equal to the corresponding value in
     *                     the matched row
     * @return {@code true} if a valid row was found with the given value,
     *         {@code false} if no row was found
     */
    public boolean findFirstRow(Column columnPattern, Object valuePattern) throws IOException {
        Position curPos = _curPos;
        Position prevPos = _prevPos;
        boolean found = false;
        try {
            beforeFirst();
            found = findNextRowImpl(columnPattern, valuePattern);
            return found;
        } finally {
            if (!found) {
                try {
                    restorePosition(curPos, prevPos);
                } catch (IOException e) {
                    LOG.error("Failed restoring position", e);
                }
            }
        }
    }

    /**
     * Moves to the next row (as defined by the cursor) where the given column
     * has the given value.  This may be more efficient on some cursors than
     * others.  If a match is not found (or an exception is thrown), the cursor
     * is restored to its previous state.
     *
     * @param columnPattern column from the table for this cursor which is being
     *                      matched by the valuePattern
     * @param valuePattern value which is equal to the corresponding value in
     *                     the matched row
     * @return {@code true} if a valid row was found with the given value,
     *         {@code false} if no row was found
     */
    public boolean findNextRow(Column columnPattern, Object valuePattern) throws IOException {
        Position curPos = _curPos;
        Position prevPos = _prevPos;
        boolean found = false;
        try {
            found = findNextRowImpl(columnPattern, valuePattern);
            return found;
        } finally {
            if (!found) {
                try {
                    restorePosition(curPos, prevPos);
                } catch (IOException e) {
                    LOG.error("Failed restoring position", e);
                }
            }
        }
    }

    /**
     * @deprecated renamed to {@link #findFirstRow(Map)} to be more clear
     */
    @Deprecated
    public boolean findRow(Map<String, ?> rowPattern) throws IOException {
        return findFirstRow(rowPattern);
    }

    /**
     * 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.  If a match is not found (or an exception is thrown), the cursor
     * is restored to its previous state.
     * <p>
     * Warning, this method <i>always</i> starts searching from the beginning of
     * the Table (you cannot use it to find successive matches).
     *
     * @param rowPattern column names and values which must be equal to the
     *                   corresponding values in the matched row
     * @return {@code true} if a valid row was found with the given values,
     *         {@code false} if no row was found
     */
    public boolean findFirstRow(Map<String, ?> rowPattern) throws IOException {
        Position curPos = _curPos;
        Position prevPos = _prevPos;
        boolean found = false;
        try {
            beforeFirst();
            found = findNextRowImpl(rowPattern);
            return found;
        } finally {
            if (!found) {
                try {
                    restorePosition(curPos, prevPos);
                } catch (IOException e) {
                    LOG.error("Failed restoring position", e);
                }
            }
        }
    }

    /**
     * Moves to the next row (as defined by the cursor) where the given columns
     * have the given values.  This may be more efficient on some cursors than
     * others.  If a match is not found (or an exception is thrown), the cursor
     * is restored to its previous state.
     *
     * @param rowPattern column names and values which must be equal to the
     *                   corresponding values in the matched row
     * @return {@code true} if a valid row was found with the given values,
     *         {@code false} if no row was found
     */
    public boolean findNextRow(Map<String, ?> rowPattern) throws IOException {
        Position curPos = _curPos;
        Position prevPos = _prevPos;
        boolean found = false;
        try {
            found = findNextRowImpl(rowPattern);
            return found;
        } finally {
            if (!found) {
                try {
                    restorePosition(curPos, prevPos);
                } catch (IOException e) {
                    LOG.error("Failed restoring position", e);
                }
            }
        }
    }

    /**
     * Returns {@code true} if the current row matches the given pattern.
     * @param columnPattern column from the table for this cursor which is being
     *                      matched by the valuePattern
     * @param valuePattern value which is tested for equality with the
     *                     corresponding value in the current row
     */
    public boolean currentRowMatches(Column columnPattern, Object valuePattern) throws IOException {
        return _columnMatcher.matches(getTable(), columnPattern.getName(), valuePattern,
                getCurrentRowValue(columnPattern));
    }

    /**
     * Returns {@code true} if the current row matches the given pattern.
     * @param rowPattern column names and values which must be equal to the
     *                   corresponding values in the current row
     */
    public boolean currentRowMatches(Map<String, ?> rowPattern) throws IOException {
        Map<String, Object> row = getCurrentRow(rowPattern.keySet());

        if (rowPattern.size() != row.size()) {
            return false;
        }

        for (Map.Entry<String, Object> e : row.entrySet()) {
            String columnName = e.getKey();
            if (!_columnMatcher.matches(getTable(), columnName, rowPattern.get(columnName), e.getValue())) {
                return false;
            }
        }

        return true;
    }

    /**
     * Moves to the next row (as defined by the cursor) where the given column
     * has the given value.  Caller manages save/restore on failure.
     * <p>
     * Default implementation scans the table from beginning to end.
     *
     * @param columnPattern column from the table for this cursor which is being
     *                      matched by the valuePattern
     * @param valuePattern value which is equal to the corresponding value in
     *                     the matched row
     * @return {@code true} if a valid row was found with the given value,
     *         {@code false} if no row was found
     */
    protected boolean findNextRowImpl(Column columnPattern, Object valuePattern) throws IOException {
        while (moveToNextRow()) {
            if (currentRowMatches(columnPattern, valuePattern)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Moves to the next row (as defined by the cursor) where the given columns
     * have the given values.  Caller manages save/restore on failure.
     * <p>
     * Default implementation scans the table from beginning to end.
     *
     * @param rowPattern column names and values which must be equal to the
     *                   corresponding values in the matched row
     * @return {@code true} if a valid row was found with the given values,
     *         {@code false} if no row was found
     */
    protected boolean findNextRowImpl(Map<String, ?> rowPattern) throws IOException {
        while (moveToNextRow()) {
            if (currentRowMatches(rowPattern)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Moves forward as many rows as possible up to the given number of rows.
     * @return the number of rows moved.
     */
    public int moveNextRows(int numRows) throws IOException {
        return moveSomeRows(numRows, MOVE_FORWARD);
    }

    /**
     * Moves backward as many rows as possible up to the given number of rows.
     * @return the number of rows moved.
     */
    public int movePreviousRows(int numRows) throws IOException {
        return moveSomeRows(numRows, MOVE_REVERSE);
    }

    /**
     * Moves as many rows as possible in the given direction up to the given
     * number of rows.
     * @return the number of rows moved.
     */
    private int moveSomeRows(int numRows, boolean moveForward) throws IOException {
        int numMovedRows = 0;
        while ((numMovedRows < numRows) && moveToAnotherRow(moveForward)) {
            ++numMovedRows;
        }
        return numMovedRows;
    }

    /**
     * Returns the current row in this cursor (Column name -> Column value).
     */
    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, _curPos.getRowId(), columnNames);
    }

    /**
     * Returns the given column from the current row.
     */
    public Object getCurrentRowValue(Column column) throws IOException {
        return _table.getRowValue(_rowState, _curPos.getRowId(), column);
    }

    /**
     * Updates a single value in the current row.
     * @throws IllegalStateException if the current row is not valid (at
     *         beginning or end of table), or deleted.
     */
    public void setCurrentRowValue(Column column, Object value) throws IOException {
        Object[] row = new Object[_table.getColumnCount()];
        Arrays.fill(row, Column.KEEP_VALUE);
        column.setRowValue(row, value);
        _table.updateRow(_rowState, _curPos.getRowId(), row);
    }

    /**
     * Returns {@code true} if this cursor is up-to-date with respect to the
     * relevant table and related table objects, {@code false} otherwise.
     */
    protected boolean isUpToDate() {
        return _rowState.isUpToDate();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " CurPosition " + _curPos + ", PrevPosition " + _prevPos;
    }

    /**
     * Finds the next non-deleted row after the given row (as defined by this
     * cursor) and returns the id of the row, where "next" may be backwards if
     * moveForward is {@code false}.  If there are no more rows, the returned
     * rowId should equal the value returned by {@link #getLastPosition} if
     * moving forward and {@link #getFirstPosition} if moving backward.
     */
    protected abstract Position findAnotherPosition(RowState rowState, Position curPos, boolean moveForward)
            throws IOException;

    /**
     * Returns the DirHandler for the given movement direction.
     */
    protected abstract DirHandler getDirHandler(boolean moveForward);

    /**
     * Base implementation of iterator for this cursor, modifiable.
     */
    protected abstract class BaseIterator implements Iterator<Map<String, Object>> {
        protected final Collection<String> _columnNames;
        protected Boolean _hasNext;
        protected boolean _validRow;

        protected BaseIterator(Collection<String> columnNames) {
            _columnNames = columnNames;
        }

        public boolean hasNext() {
            if (_hasNext == null) {
                try {
                    _hasNext = findNext();
                    _validRow = _hasNext;
                } catch (IOException e) {
                    throw new IllegalStateException(e);
                }
            }
            return _hasNext;
        }

        public Map<String, Object> next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            try {
                Map<String, Object> rtn = getCurrentRow(_columnNames);
                _hasNext = null;
                return rtn;
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }

        public void remove() {
            if (_validRow) {
                try {
                    deleteCurrentRow();
                    _validRow = false;
                } catch (IOException e) {
                    throw new IllegalStateException(e);
                }
            } else {
                throw new IllegalStateException("Not at valid row");
            }
        }

        protected abstract boolean findNext() throws IOException;
    }

    /**
     * Row iterator for this cursor, modifiable.
     */
    private final class RowIterator extends BaseIterator {
        private final boolean _moveForward;

        private RowIterator(Collection<String> columnNames, boolean moveForward) {
            super(columnNames);
            _moveForward = moveForward;
            reset(_moveForward);
        }

        @Override
        protected boolean findNext() throws IOException {
            return moveToAnotherRow(_moveForward);
        }
    }

    /**
     * Row iterator for this cursor, modifiable.
     */
    private final class ColumnMatchIterator extends BaseIterator {
        private final Column _columnPattern;
        private final Object _valuePattern;

        private ColumnMatchIterator(Collection<String> columnNames, Column columnPattern, Object valuePattern) {
            super(columnNames);
            _columnPattern = columnPattern;
            _valuePattern = valuePattern;
            beforeFirst();
        }

        @Override
        protected boolean findNext() throws IOException {
            return findNextRow(_columnPattern, _valuePattern);
        }
    }

    /**
     * Row iterator for this cursor, modifiable.
     */
    private final class RowMatchIterator extends BaseIterator {
        private final Map<String, ?> _rowPattern;

        private RowMatchIterator(Collection<String> columnNames, Map<String, ?> rowPattern) {
            super(columnNames);
            _rowPattern = rowPattern;
            beforeFirst();
        }

        @Override
        protected boolean findNext() throws IOException {
            return findNextRow(_rowPattern);
        }
    }

    /**
     * Handles moving the cursor in a given direction.  Separates cursor
     * logic from value storage.
     */
    protected abstract class DirHandler {
        public abstract Position getBeginningPosition();

        public abstract Position getEndPosition();
    }

    /**
     * Simple un-indexed cursor.
     */
    private static final class TableScanCursor extends Cursor {
        /** ScanDirHandler for forward traversal */
        private final ScanDirHandler _forwardDirHandler = new ForwardScanDirHandler();
        /** ScanDirHandler for backward traversal */
        private final ScanDirHandler _reverseDirHandler = new ReverseScanDirHandler();
        /** Cursor over the pages that this table owns */
        private final UsageMap.PageCursor _ownedPagesCursor;

        private TableScanCursor(Table table) {
            super(new Id(table, null), table, FIRST_SCAN_POSITION, LAST_SCAN_POSITION);
            _ownedPagesCursor = table.getOwnedPagesCursor();
        }

        @Override
        protected ScanDirHandler getDirHandler(boolean moveForward) {
            return (moveForward ? _forwardDirHandler : _reverseDirHandler);
        }

        @Override
        protected boolean isUpToDate() {
            return (super.isUpToDate() && _ownedPagesCursor.isUpToDate());
        }

        @Override
        protected void reset(boolean moveForward) {
            _ownedPagesCursor.reset(moveForward);
            super.reset(moveForward);
        }

        @Override
        protected void restorePositionImpl(Position curPos, Position prevPos) throws IOException {
            if (!(curPos instanceof ScanPosition) || !(prevPos instanceof ScanPosition)) {
                throw new IllegalArgumentException("Restored positions must be scan positions");
            }
            _ownedPagesCursor.restorePosition(curPos.getRowId().getPageNumber(),
                    prevPos.getRowId().getPageNumber());
            super.restorePositionImpl(curPos, prevPos);
        }

        @Override
        protected Position findAnotherPosition(RowState rowState, Position curPos, boolean moveForward)
                throws IOException {
            ScanDirHandler handler = getDirHandler(moveForward);

            // 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);
            int currentRowNumber = curRowId.getRowNumber();

            // loop until we find the next valid row or run out of pages
            while (true) {

                currentRowNumber = handler.getAnotherRowNumber(currentRowNumber);
                curRowId = new RowId(curRowId.getPageNumber(), currentRowNumber);
                Table.positionAtRowHeader(rowState, curRowId);

                if (!rowState.isValid()) {

                    // load next page
                    curRowId = new RowId(handler.getAnotherPageNumber(), RowId.INVALID_ROW_NUMBER);
                    Table.positionAtRowHeader(rowState, curRowId);

                    if (!rowState.isHeaderPageNumberValid()) {
                        //No more owned pages.  No more rows.
                        return handler.getEndPosition();
                    }

                    // update row count and initial row number
                    currentRowNumber = handler.getInitialRowNumber(rowState.getRowsOnHeaderPage());

                } else if (!rowState.isDeleted()) {

                    // we found a valid, non-deleted row, return it
                    return new ScanPosition(curRowId);
                }

            }
        }

        /**
         * Handles moving the table scan cursor in a given direction.  Separates
         * cursor logic from value storage.
         */
        private abstract class ScanDirHandler extends DirHandler {
            public abstract int getAnotherRowNumber(int curRowNumber);

            public abstract int getAnotherPageNumber();

            public abstract int getInitialRowNumber(int rowsOnPage);
        }

        /**
         * Handles moving the table scan cursor forward.
         */
        private final class ForwardScanDirHandler extends ScanDirHandler {
            @Override
            public Position getBeginningPosition() {
                return getFirstPosition();
            }

            @Override
            public Position getEndPosition() {
                return getLastPosition();
            }

            @Override
            public int getAnotherRowNumber(int curRowNumber) {
                return curRowNumber + 1;
            }

            @Override
            public int getAnotherPageNumber() {
                return _ownedPagesCursor.getNextPage();
            }

            @Override
            public int getInitialRowNumber(int rowsOnPage) {
                return -1;
            }
        }

        /**
         * Handles moving the table scan cursor backward.
         */
        private final class ReverseScanDirHandler extends ScanDirHandler {
            @Override
            public Position getBeginningPosition() {
                return getLastPosition();
            }

            @Override
            public Position getEndPosition() {
                return getFirstPosition();
            }

            @Override
            public int getAnotherRowNumber(int curRowNumber) {
                return curRowNumber - 1;
            }

            @Override
            public int getAnotherPageNumber() {
                return _ownedPagesCursor.getPreviousPage();
            }

            @Override
            public int getInitialRowNumber(int rowsOnPage) {
                return rowsOnPage;
            }
        }

    }

    /**
     * Identifier for a cursor.  Will be equal to any other cursor of the same
     * type for the same table.  Primarily used to check the validity of a
     * Savepoint.
     */
    public static final class Id {
        private final String _tableName;
        private final String _indexName;

        protected Id(Table table, Index index) {
            _tableName = table.getName();
            _indexName = ((index != null) ? index.getName() : null);
        }

        @Override
        public int hashCode() {
            return _tableName.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass())
                    && ObjectUtils.equals(_tableName, ((Id) o)._tableName)
                    && ObjectUtils.equals(_indexName, ((Id) o)._indexName)));
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + " " + _tableName + ":" + _indexName;
        }
    }

    /**
     * Value object which represents a complete save state of the cursor.
     */
    public static final class Savepoint {
        private final Id _cursorId;
        private final Position _curPos;
        private final Position _prevPos;

        private Savepoint(Id cursorId, Position curPos, Position prevPos) {
            _cursorId = cursorId;
            _curPos = curPos;
            _prevPos = prevPos;
        }

        public Id getCursorId() {
            return _cursorId;
        }

        public Position getCurrentPosition() {
            return _curPos;
        }

        private Position getPreviousPosition() {
            return _prevPos;
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + " " + _cursorId + " CurPosition " + _curPos + ", PrevPosition "
                    + _prevPos;
        }
    }

    /**
     * Value object which maintains the current position of the cursor.
     */
    public static abstract class Position {
        protected Position() {
        }

        @Override
        public final int hashCode() {
            return getRowId().hashCode();
        }

        @Override
        public final boolean equals(Object o) {
            return ((this == o) || ((o != null) && (getClass() == o.getClass()) && equalsImpl(o)));
        }

        /**
         * Returns the unique RowId of the position of the cursor.
         */
        public abstract RowId getRowId();

        /**
         * Returns {@code true} if the subclass specific info in a Position is
         * equal, {@code false} otherwise.
         * @param o object being tested for equality, guaranteed to be the same
         *          class as this object
         */
        protected abstract boolean equalsImpl(Object o);
    }

    /**
     * Value object which maintains the current position of a TableScanCursor.
     */
    private static final class ScanPosition extends Position {
        private final RowId _rowId;

        private ScanPosition(RowId rowId) {
            _rowId = rowId;
        }

        @Override
        public RowId getRowId() {
            return _rowId;
        }

        @Override
        protected boolean equalsImpl(Object o) {
            return getRowId().equals(((ScanPosition) o).getRowId());
        }

        @Override
        public String toString() {
            return "RowId = " + getRowId();
        }
    }

}