/*
Copyright (c) 2007 Health Market Science, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.healthmarketscience.jackcess.impl;
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 java.util.function.Predicate;
import com.healthmarketscience.jackcess.Column;
import com.healthmarketscience.jackcess.Cursor;
import com.healthmarketscience.jackcess.CursorBuilder;
import com.healthmarketscience.jackcess.Row;
import com.healthmarketscience.jackcess.RowId;
import com.healthmarketscience.jackcess.RuntimeIOException;
import com.healthmarketscience.jackcess.impl.TableImpl.RowState;
import com.healthmarketscience.jackcess.util.ColumnMatcher;
import com.healthmarketscience.jackcess.util.ErrorHandler;
import com.healthmarketscience.jackcess.util.IterableBuilder;
import com.healthmarketscience.jackcess.util.SimpleColumnMatcher;
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.
*
* 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}.
*
* Is not thread-safe.
*
* @author James Ahlborn
*/
public abstract class CursorImpl implements Cursor
{
private static final Log LOG = LogFactory.getLog(CursorImpl.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;
/** identifier for this cursor */
private final IdImpl _id;
/** owning table */
private final TableImpl _table;
/** State used for reading the table rows */
private final RowState _rowState;
/** the first (exclusive) row id for this cursor */
private final PositionImpl _firstPos;
/** the last (exclusive) row id for this cursor */
private final PositionImpl _lastPos;
/** the previous row */
protected PositionImpl _prevPos;
/** the current row */
protected PositionImpl _curPos;
/** ColumnMatcher to be used when matching column values */
protected ColumnMatcher _columnMatcher = SimpleColumnMatcher.INSTANCE;
protected CursorImpl(IdImpl id, TableImpl table, PositionImpl firstPos,
PositionImpl 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 CursorImpl createCursor(TableImpl table) {
return new TableScanCursor(table);
}
public RowState getRowState() {
return _rowState;
}
@Override
public IdImpl getId() {
return _id;
}
@Override
public TableImpl getTable() {
return _table;
}
public JetFormat getFormat() {
return getTable().getFormat();
}
public PageChannel getPageChannel() {
return getTable().getPageChannel();
}
@Override
public ErrorHandler getErrorHandler() {
return _rowState.getErrorHandler();
}
@Override
public void setErrorHandler(ErrorHandler newErrorHandler) {
_rowState.setErrorHandler(newErrorHandler);
}
@Override
public ColumnMatcher getColumnMatcher() {
return _columnMatcher;
}
@Override
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;
}
@Override
public SavepointImpl getSavepoint() {
return new SavepointImpl(_id, _curPos, _prevPos);
}
@Override
public void restoreSavepoint(Savepoint savepoint)
throws IOException
{
restoreSavepoint((SavepointImpl)savepoint);
}
public void restoreSavepoint(SavepointImpl 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 PositionImpl getFirstPosition() {
return _firstPos;
}
/**
* Returns the last row id (exclusive) as defined by this cursor.
*/
protected PositionImpl getLastPosition() {
return _lastPos;
}
@Override
public void reset() {
beforeFirst();
}
@Override
public void beforeFirst() {
reset(MOVE_FORWARD);
}
@Override
public void afterLast() {
reset(MOVE_REVERSE);
}
@Override
public boolean isBeforeFirst() throws IOException {
return isAtBeginning(MOVE_FORWARD);
}
@Override
public boolean isAfterLast() throws IOException {
return isAtBeginning(MOVE_REVERSE);
}
protected boolean isAtBeginning(boolean moveForward) throws IOException {
if(getDirHandler(moveForward).getBeginningPosition().equals(_curPos)) {
return !recheckPosition(!moveForward);
}
return false;
}
@Override
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)
TableImpl.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();
}
@Override
public Iterator iterator() {
return new RowIterator(null, true, MOVE_FORWARD);
}
@Override
public IterableBuilder newIterable() {
return new IterableBuilder(this);
}
public Iterator iterator(IterableBuilder iterBuilder) {
switch(iterBuilder.getType()) {
case SIMPLE:
return new RowIterator(iterBuilder.getColumnNames(),
iterBuilder.isReset(), iterBuilder.isForward());
case COLUMN_MATCH: {
@SuppressWarnings("unchecked")
Map.Entry matchPattern = (Map.Entry)
iterBuilder.getMatchPattern();
return new ColumnMatchIterator(
iterBuilder.getColumnNames(), (ColumnImpl)matchPattern.getKey(),
matchPattern.getValue(), iterBuilder.isReset(),
iterBuilder.isForward(), iterBuilder.getColumnMatcher());
}
case ROW_MATCH: {
@SuppressWarnings("unchecked")
Map matchPattern = (Map)
iterBuilder.getMatchPattern();
return new RowMatchIterator(
iterBuilder.getColumnNames(), matchPattern,iterBuilder.isReset(),
iterBuilder.isForward(), iterBuilder.getColumnMatcher());
}
default:
throw new RuntimeException("unknown match type " + iterBuilder.getType());
}
}
@Override
public void deleteCurrentRow() throws IOException {
_table.deleteRow(_rowState, _curPos.getRowId());
}
@Override
public Object[] updateCurrentRow(Object... row) throws IOException {
return _table.updateRow(_rowState, _curPos.getRowId(), row);
}
@Override
public > M updateCurrentRowFromMap(M row)
throws IOException
{
return _table.updateRowFromMap(_rowState, _curPos.getRowId(), row);
}
@Override
public Row getNextRow() throws IOException {
return getNextRow(null);
}
@Override
public Row getNextRow(Collection columnNames)
throws IOException
{
return getAnotherRow(columnNames, MOVE_FORWARD);
}
@Override
public Row getPreviousRow() throws IOException {
return getPreviousRow(null);
}
@Override
public Row getPreviousRow(Collection 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 Row getAnotherRow(Collection columnNames,
boolean moveForward)
throws IOException
{
if(moveToAnotherRow(moveForward)) {
return getCurrentRow(columnNames);
}
return null;
}
@Override
public boolean moveToNextRow() throws IOException
{
return moveToAnotherRow(MOVE_FORWARD);
}
@Override
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
*/
protected 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(PositionImpl 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(PositionImpl curPos,
PositionImpl 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(PositionImpl curPos, PositionImpl 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);
TableImpl.positionAtRowHeader(_rowState, _curPos.getRowId());
return(!_curPos.equals(getDirHandler(moveForward).getEndPosition()));
}
@Override
public boolean findRow(RowId rowId) throws IOException
{
RowIdImpl rowIdImpl = (RowIdImpl)rowId;
PositionImpl curPos = _curPos;
PositionImpl prevPos = _prevPos;
boolean found = false;
try {
reset(MOVE_FORWARD);
if(TableImpl.positionAtRowHeader(_rowState, rowIdImpl) == null) {
return false;
}
restorePosition(getRowPosition(rowIdImpl));
if(!isCurrentRowValid()) {
return false;
}
found = true;
return true;
} finally {
if(!found) {
try {
restorePosition(curPos, prevPos);
} catch(IOException e) {
LOG.error("Failed restoring position", e);
}
}
}
}
@Override
public boolean findFirstRow(Column columnPattern, Object valuePattern)
throws IOException
{
return findFirstRow((ColumnImpl)columnPattern, valuePattern);
}
public boolean findFirstRow(ColumnImpl columnPattern, Object valuePattern)
throws IOException
{
return findAnotherRow(columnPattern, valuePattern, true, MOVE_FORWARD,
_columnMatcher,
prepareSearchInfo(columnPattern, valuePattern));
}
@Override
public boolean findNextRow(Column columnPattern, Object valuePattern)
throws IOException
{
return findNextRow((ColumnImpl)columnPattern, valuePattern);
}
public boolean findNextRow(ColumnImpl columnPattern, Object valuePattern)
throws IOException
{
return findAnotherRow(columnPattern, valuePattern, false, MOVE_FORWARD,
_columnMatcher,
prepareSearchInfo(columnPattern, valuePattern));
}
protected boolean findAnotherRow(ColumnImpl columnPattern, Object valuePattern,
boolean reset, boolean moveForward,
ColumnMatcher columnMatcher, Object searchInfo)
throws IOException
{
PositionImpl curPos = _curPos;
PositionImpl prevPos = _prevPos;
boolean found = false;
try {
if(reset) {
reset(moveForward);
}
found = findAnotherRowImpl(columnPattern, valuePattern, moveForward,
columnMatcher, searchInfo);
return found;
} finally {
if(!found) {
try {
restorePosition(curPos, prevPos);
} catch(IOException e) {
LOG.error("Failed restoring position", e);
}
}
}
}
@Override
public boolean findFirstRow(Map rowPattern) throws IOException
{
return findAnotherRow(rowPattern, true, MOVE_FORWARD, _columnMatcher,
prepareSearchInfo(rowPattern));
}
@Override
public boolean findNextRow(Map rowPattern)
throws IOException
{
return findAnotherRow(rowPattern, false, MOVE_FORWARD, _columnMatcher,
prepareSearchInfo(rowPattern));
}
protected boolean findAnotherRow(Map rowPattern, boolean reset,
boolean moveForward,
ColumnMatcher columnMatcher, Object searchInfo)
throws IOException
{
PositionImpl curPos = _curPos;
PositionImpl prevPos = _prevPos;
boolean found = false;
try {
if(reset) {
reset(moveForward);
}
found = findAnotherRowImpl(rowPattern, moveForward, columnMatcher,
searchInfo);
return found;
} finally {
if(!found) {
try {
restorePosition(curPos, prevPos);
} catch(IOException e) {
LOG.error("Failed restoring position", e);
}
}
}
}
@Override
public boolean currentRowMatches(Column columnPattern, Object valuePattern)
throws IOException
{
return currentRowMatches((ColumnImpl)columnPattern, valuePattern);
}
public boolean currentRowMatches(ColumnImpl columnPattern, Object valuePattern)
throws IOException
{
return currentRowMatchesImpl(columnPattern, valuePattern, _columnMatcher);
}
protected boolean currentRowMatchesImpl(ColumnImpl columnPattern,
Object valuePattern,
ColumnMatcher columnMatcher)
throws IOException
{
return currentRowMatchesPattern(
columnPattern.getName(), valuePattern, columnMatcher,
getCurrentRowValue(columnPattern));
}
@Override
public boolean currentRowMatches(Map rowPattern)
throws IOException
{
return currentRowMatchesImpl(rowPattern, _columnMatcher);
}
protected boolean currentRowMatchesImpl(Map rowPattern,
ColumnMatcher columnMatcher)
throws IOException
{
Row row = getCurrentRow(rowPattern.keySet());
if(rowPattern.size() != row.size()) {
return false;
}
for(Map.Entry e : row.entrySet()) {
String columnName = e.getKey();
if(!currentRowMatchesPattern(columnName, rowPattern.get(columnName),
columnMatcher, e.getValue())) {
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
protected final boolean currentRowMatchesPattern(
String columnPattern, Object valuePattern,
ColumnMatcher columnMatcher, Object rowValue) {
// if the value pattern is a Predicate use that to test the value
if(valuePattern instanceof Predicate>) {
return ((Predicate