/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2000-2003
* Sleepycat Software. All rights reserved.
*
* $Id: StoredIterator.java,v 1.18 2003/10/22 14:28:40 mhayes Exp $
*/
package com.sleepycat.bdb.collection;
import com.sleepycat.bdb.DataCursor;
import com.sleepycat.bdb.util.ExceptionWrapper;
import com.sleepycat.db.Db;
import com.sleepycat.db.DbDeadlockException;
import com.sleepycat.db.DbException;
import com.sleepycat.db.DbMemoryException;
import com.sleepycat.db.DbRunRecoveryException;
import java.io.IOException;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
/**
* The Iterator returned by all stored collections.
*
* <p>While in general this class conforms to the {@link Iterator} interface,
* it is important to note that all iterators for stored collections must be
* explicitly closed with {@link #close()}. The static method {@link
* #close(java.util.Iterator)} allows calling close for all iterators without
* harm to iterators that are not from stored collections, and also avoids
* casting. If a stored iterator is not closed, unpredictable behavior
* including process death may result.</p>
*
* <p>This class implements the {@link Iterator} interface for all stored
* iterators. It also implements {@link ListIterator} because some list
* iterator methods apply to all stored iterators, for example, {@link
* #previous} and {@link #hasPrevious}. Other list iterator methods are always
* supports for lists, but for other types of collections are only supported
* under certain conditions. For example, {@link #nextIndex} and {@link
* #previousIndex} are only supported when record number keys are used, while
* {@link #add} and {@link #set} are supported only under certain other
* conditions. See the individual method descriptions for more
* information.</p>
*
* <p>In addition, this class provides the following methods for stored
* collection iterators only. Note that the use of these methods is not
* compatible with the standard Java collections interface.</p>
* <ul>
* <li>{@link #close()}</li>
* <li>{@link #close(Iterator)}</li>
* <li>{@link #getCollection()}</li>
* </ul>
*
* @author Mark Hayes
*/
public class StoredIterator implements ListIterator, Cloneable {
/**
* Closes the given iterator using {@link #close()} if it is a {@link
* StoredIterator}. If the given iterator is not a {@link StoredIterator},
* this method does nothing.
*
* @param i is the iterator to close.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public static void close(Iterator i) {
if (i instanceof StoredIterator) {
((StoredIterator) i).close();
}
}
private boolean lockForWrite;
private StoredCollection coll;
private DataCursor cursor;
private int toNext;
private int toPrevious;
private int toCurrent;
private boolean writeAllowed;
private boolean setAndRemoveAllowed;
private Object currentData;
private final int NEXT_FLAG;
private final int PREV_FLAG;
private final boolean recNumAccess;
StoredIterator(StoredCollection coll, boolean writeAllowed,
DataCursor joinCursor) {
try {
this.coll = coll;
this.writeAllowed = writeAllowed;
if (joinCursor == null)
this.cursor = new DataCursor(coll.view, writeAllowed);
else
this.cursor = joinCursor;
this.recNumAccess = cursor.hasRecNumAccess();
if (coll.iterateDuplicates()) {
this.NEXT_FLAG = Db.DB_NEXT;
this.PREV_FLAG = Db.DB_PREV;
} else {
this.NEXT_FLAG = Db.DB_NEXT_NODUP;
this.PREV_FLAG = Db.DB_PREV_NODUP;
}
reset();
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Clones this iterator preserving its current position.
*
* @return a new {@link StoredIterator} having the same position as this
* iterator.
*/
protected Object clone() {
try {
StoredIterator o = (StoredIterator) super.clone();
o.cursor = new DataCursor(cursor);
return o;
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Returns whether write-locks will be obtained when reading with this
* cursor.
* Obtaining write-locks can prevent deadlocks when reading and then
* modifying data.
*
* @return the write-lock setting.
*/
public final boolean getLockForWrite() {
return lockForWrite;
}
/**
* Changes whether write-locks will be obtained when reading with this
* cursor.
* Obtaining write-locks can prevent deadlocks when reading and then
* modifying data.
*
* @param lockForWrite the write-lock setting.
*/
public void setLockForWrite(boolean lockForWrite) {
this.lockForWrite = lockForWrite;
}
// --- begin Iterator/ListIterator methods ---
/**
* Returns true if this iterator has more elements when traversing in the
* forward direction. False is returned if the iterator has been closed.
* This method conforms to the {@link Iterator#hasNext} interface.
*
* @return whether {@link #next()} will succeed.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public boolean hasNext() {
if (cursor == null) {
return false;
}
try {
if (toNext != 0) {
int err = cursor.get(null, null, toNext, lockForWrite);
if (err == 0) {
toNext = 0;
toPrevious = PREV_FLAG;
toCurrent = PREV_FLAG;
}
}
return (toNext == 0);
}
catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Returns true if this iterator has more elements when traversing in the
* reverse direction. It returns false if the iterator has been closed.
* This method conforms to the {@link ListIterator#hasPrevious} interface.
*
* @return whether {@link #previous()} will succeed.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public boolean hasPrevious() {
if (cursor == null) {
return false;
}
try {
if (toPrevious != 0) {
int err = cursor.get(null, null, toPrevious, lockForWrite);
if (err == 0) {
toPrevious = 0;
toNext = NEXT_FLAG;
toCurrent = NEXT_FLAG;
}
}
return (toPrevious == 0);
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Returns the next element in the interation.
* This method conforms to the {@link Iterator#next} interface.
*
* @return the next element.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public Object next() {
try {
if (toNext != 0) {
int err = cursor.get(null, null, toNext, lockForWrite);
if (err == 0) {
toNext = 0;
}
}
if (toNext == 0) {
currentData = coll.makeIteratorData(this, cursor);
toNext = NEXT_FLAG;
toPrevious = 0;
toCurrent = 0;
setAndRemoveAllowed = true;
return currentData;
}
// else throw NoSuchElementException below
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
throw new NoSuchElementException();
}
/**
* Returns the next element in the interation.
* This method conforms to the {@link ListIterator#previous} interface.
*
* @return the previous element.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public Object previous() {
try {
if (toPrevious != 0) {
int err = cursor.get(null, null, toPrevious, lockForWrite);
if (err == 0) {
toPrevious = 0;
}
}
if (toPrevious == 0) {
currentData = coll.makeIteratorData(this, cursor);
toPrevious = PREV_FLAG;
toNext = 0;
toCurrent = 0;
setAndRemoveAllowed = true;
return currentData;
}
// else throw NoSuchElementException below
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
throw new NoSuchElementException();
}
/**
* Returns the index of the element that would be returned by a subsequent
* call to next.
* This method conforms to the {@link ListIterator#nextIndex} interface
* except that it returns Integer.MAX_VALUE for stored lists when
* positioned at the end of the list, rather than returning the list size
* as specified by the ListIterator interface. This is because the database
* size is not available.
*
* @return the next index.
*
* @throws UnsupportedOperationException if this iterator's collection does
* not use record number keys.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public int nextIndex() {
if (!recNumAccess) {
throw new UnsupportedOperationException(
"Record number access not supported");
}
try {
return hasNext() ? (cursor.getCurrentRecordNumber() -
coll.getIndexOffset())
: Integer.MAX_VALUE;
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Returns the index of the element that would be returned by a subsequent
* call to previous.
* This method conforms to the {@link ListIterator#previousIndex}
* interface.
*
* @return the previous index.
*
* @throws UnsupportedOperationException if this iterator's collection does
* not use record number keys.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public int previousIndex() {
if (!recNumAccess) {
throw new UnsupportedOperationException(
"Record number access not supported");
}
try {
return hasPrevious() ? (cursor.getCurrentRecordNumber() -
coll.getIndexOffset())
: -1;
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Replaces the last element returned by next or previous with the
* specified element (optional operation).
* This method conforms to the {@link ListIterator#set} interface.
*
* @param value the new value.
*
* @throws UnsupportedOperationException if the collection is a {@link
* StoredKeySet} (the set returned by {@link Map#keySet}), or if duplicates
* are sorted since this would change the iterator position, or if
* the collection is indexed, or if the collection is read-only.
*
* @throws IllegalArgumentException if an entity value binding is used and
* the primary key of the value given is different than the existing stored
* primary key.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public void set(Object value) {
if (!coll.hasValues()) throw new UnsupportedOperationException();
if (!setAndRemoveAllowed) throw new IllegalStateException();
try {
moveToCurrent();
cursor.put(null, value, Db.DB_CURRENT, null);
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Removes the last element that was returned by next or previous (optional
* operation).
* This method conforms to the {@link ListIterator#remove} interface except
* that when the collection is a list and the RECNO-RENUMBER access method
* is not used, list indices will not be renumbered.
*
* @throws UnsupportedOperationException if the collection is a sublist, or
* if the collection is read-only.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public void remove() {
if (!setAndRemoveAllowed) throw new IllegalStateException();
try {
moveToCurrent();
cursor.delete();
setAndRemoveAllowed = false;
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Inserts the specified element into the list or inserts a duplicate into
* other types of collections (optional operation).
* This method conforms to the {@link ListIterator#add} interface when
* the collection is a list and the RECNO-RENUMBER access method is used.
* Otherwise, this method may only be called when duplicates are allowed.
* If duplicates are unsorted, the new value will be inserted in the same
* manner as list elements.
* If duplicates are sorted, the new value will be inserted in sort order.
*
* @param value the new value.
*
* @throws UnsupportedOperationException if the collection is a sublist, or
* if the collection is indexed, or if the collection is read-only, or if
* the collection is a list and the RECNO-RENUMBER access method was not
* used, or if the collection is not a list and duplicates are not allowed.
*
* @throws IllegalStateException if the collection is empty and is not a
* list with RECNO-RENUMBER access.
*
* @throws IllegalArgumentException if a duplicate value is being added
* that already exists and duplicates are sorted.
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public void add(Object value) {
coll.checkIterAddAllowed();
try {
int err = 0;
if (toNext != 0 && toPrevious != 0) { // database is empty
if (coll.view.areKeysRenumbered()) { // recno-renumber database
// close cursor during append and then reopen to support
// CDB restriction that append may not be called with a
// cursor open; note the append will still fail if the
// application has another cursor open
close();
err = coll.view.append(value, null, null);
cursor = new DataCursor(coll.view, writeAllowed);
reset();
next(); // move past new record
} else { // hash/btree with duplicates
throw new IllegalStateException(
"Collection is empty, cannot add() duplicate");
}
} else { // database is not empty
int flags;
if (coll.view.areKeysRenumbered()) { // recno-renumber database
moveToCurrent();
flags = hasNext() ? Db.DB_BEFORE : Db.DB_AFTER;
} else { // hash/btree with duplicates
flags = coll.areDuplicatesOrdered()
? Db.DB_NODUPDATA
: (toNext == 0) ? Db.DB_BEFORE : Db.DB_AFTER;
}
err = cursor.put(null, value, flags, null, true);
if (flags == Db.DB_BEFORE) {
toPrevious = 0;
toNext = NEXT_FLAG;
}
}
if (err == Db.DB_KEYEXIST)
throw new IllegalArgumentException("Duplicate value");
else if (err != 0)
throw new IllegalArgumentException("Could not insert: " + err);
setAndRemoveAllowed = false;
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
// --- end Iterator/ListIterator methods ---
// hide for now
private void reset() {
toNext = Db.DB_FIRST;
toPrevious = PREV_FLAG;
toCurrent = 0;
currentData = null;
// initialize cursor at beginning to avoid "initial previous == last"
// behavior when cursor is uninitialized
hasNext();
}
/**
* Returns the number of elements having the same key value as the key
* value of the element last returned by next() or previous(). If no
* duplicates are allowed, 1 is always returned.
*
* @return the number of duplicates.
*
* @throws IllegalStateException if next() or previous() has not been
* called for this iterator, or if remove() or add() were called after
* the last call to next() or previous().
*/
public int count() {
if (!setAndRemoveAllowed) throw new IllegalStateException();
try {
moveToCurrent();
return cursor.count();
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
/**
* Closes this iterator.
* This method does not exist in the standard {@link Iterator} or {@link
* ListIterator} interfaces.
*
* <p>After being closed, only the {@link #hasNext} and {@link
* #hasPrevious} methods may be called and these will return false. {@link
* #close()} may also be called again and will do nothing. If other
* methods are called a <code>NullPointerException</code> will generally be
* thrown.</p>
*
* @throws RuntimeExceptionWrapper if a {@link DbException} is thrown.
*/
public void close() {
if (cursor != null) {
coll.closeCursor(cursor);
cursor = null;
}
}
/**
* Returns the collection associated with this iterator.
* This method does not exist in the standard {@link Iterator} or {@link
* ListIterator} interfaces.
*
* @return the collection associated with this iterator.
*/
public final StoredCollection getCollection() {
return coll;
}
/* hide for now
private final Object current() {
return currentData;
}
*/
/* hide for now
private int currentIndex()
throws IllegalStateException {
if (!recNumAccess) {
throw new UnsupportedOperationException(
"Record number access not supported");
}
try {
// TODO: don't we need to moveToCurrent?
return cursor.getCurrentRecordNumber() - coll.getIndexOffset();
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
*/
/* hide for now
final DataCursor getCursor() {
return cursor;
}
*/
final boolean isCurrentData(Object currentData) {
return (this.currentData == currentData);
}
final boolean moveToIndex(int index) {
try {
int err = cursor.get(new Integer(index), null, Db.DB_SET,
lockForWrite);
setAndRemoveAllowed = (err == 0);
return setAndRemoveAllowed;
} catch (Exception e) {
throw StoredContainer.convertException(e);
}
}
private void moveToCurrent()
throws DbException, IOException {
if (toCurrent != 0) {
cursor.get(null, null, toCurrent, lockForWrite);
toCurrent = 0;
}
}
}