1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2000,2008 Oracle.  All rights reserved.
5 *
6 * $Id: StoredIterator.java,v 12.9 2008/02/07 17:12:26 mark Exp $
7 */
8
9package com.sleepycat.collections;
10
11import java.util.Iterator;
12import java.util.ListIterator;
13import java.util.NoSuchElementException;
14
15import com.sleepycat.db.DatabaseException;
16import com.sleepycat.db.OperationStatus;
17import com.sleepycat.util.RuntimeExceptionWrapper;
18
19/**
20 * The Iterator returned by all stored collections.
21 *
22 * <p>While in general this class conforms to the {@link Iterator} interface,
23 * it is important to note that all iterators for stored collections must be
24 * explicitly closed with {@link #close()}.  The static method {@link
25 * #close(java.util.Iterator)} allows calling close for all iterators without
26 * harm to iterators that are not from stored collections, and also avoids
27 * casting.  If a stored iterator is not closed, unpredictable behavior
28 * including process death may result.</p>
29 *
30 * <p>This class implements the {@link Iterator} interface for all stored
31 * iterators.  It also implements {@link ListIterator} because some list
32 * iterator methods apply to all stored iterators, for example, {@link
33 * #previous} and {@link #hasPrevious}.  Other list iterator methods are always
34 * supported for lists, but for other types of collections are only supported
35 * under certain conditions.  See {@link #nextIndex}, {@link #previousIndex},
36 * {@link #add} and {@link #set} for details.</p>
37 *
38 * <p>In addition, this class provides the following methods for stored
39 * collection iterators only.  Note that the use of these methods is not
40 * compatible with the standard Java collections interface.</p>
41 * <ul>
42 * <li>{@link #close()}</li>
43 * <li>{@link #close(Iterator)}</li>
44 * <li>{@link #count()}</li>
45 * <li>{@link #getCollection}</li>
46 * <li>{@link #setReadModifyWrite}</li>
47 * <li>{@link #isReadModifyWrite}</li>
48 * </ul>
49 *
50 * @author Mark Hayes
51 */
52public class StoredIterator implements BaseIterator, Cloneable {
53
54    /**
55     * Closes the given iterator using {@link #close()} if it is a {@link
56     * StoredIterator}.  If the given iterator is not a {@link StoredIterator},
57     * this method does nothing.
58     *
59     * @param i is the iterator to close.
60     *
61     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is thrown.
62     */
63    public static void close(Iterator i) {
64
65        if (i instanceof StoredIterator) {
66            ((StoredIterator) i).close();
67        }
68    }
69
70    private static final int MOVE_NEXT = 1;
71    private static final int MOVE_PREV = 2;
72    private static final int MOVE_FIRST = 3;
73
74    private boolean lockForWrite;
75    private StoredCollection coll;
76    private DataCursor cursor;
77    private int toNext;
78    private int toPrevious;
79    private int toCurrent;
80    private boolean writeAllowed;
81    private boolean setAndRemoveAllowed;
82    private Object currentData;
83
84    StoredIterator(StoredCollection coll, boolean writeAllowed,
85                   DataCursor joinCursor) {
86        try {
87            this.coll = coll;
88            this.writeAllowed = writeAllowed;
89            if (joinCursor == null)
90                this.cursor = new DataCursor(coll.view, writeAllowed);
91            else
92                this.cursor = joinCursor;
93            reset();
94        } catch (Exception e) {
95            try {
96                /* Ensure that the cursor is closed.  [#10516] */
97                close();
98            } catch (Exception ignored) {
99		/* Klockwork - ok */
100	    }
101            throw StoredContainer.convertException(e);
102        }
103    }
104
105    /**
106     * Returns whether write-locks will be obtained when reading with this
107     * cursor.
108     * Obtaining write-locks can prevent deadlocks when reading and then
109     * modifying data.
110     *
111     * @return the write-lock setting.
112     */
113    public final boolean isReadModifyWrite() {
114
115        return lockForWrite;
116    }
117
118    /**
119     * Changes whether write-locks will be obtained when reading with this
120     * cursor.
121     * Obtaining write-locks can prevent deadlocks when reading and then
122     * modifying data.
123     *
124     * @param lockForWrite the write-lock setting.
125     */
126    public void setReadModifyWrite(boolean lockForWrite) {
127
128        this.lockForWrite = lockForWrite;
129    }
130
131    // --- begin Iterator/ListIterator methods ---
132
133    /**
134     * Returns true if this iterator has more elements when traversing in the
135     * forward direction.  False is returned if the iterator has been closed.
136     * This method conforms to the {@link Iterator#hasNext} interface.
137     *
138     * @return whether {@link #next()} will succeed.
139     *
140     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is thrown.
141     */
142    public boolean hasNext() {
143
144        if (cursor == null) {
145            return false;
146        }
147        try {
148            if (toNext != 0) {
149                OperationStatus status = move(toNext);
150                if (status == OperationStatus.SUCCESS) {
151                    toNext = 0;
152                    toPrevious = MOVE_PREV;
153                    toCurrent = MOVE_PREV;
154                }
155            }
156            return (toNext == 0);
157        } catch (Exception e) {
158            throw StoredContainer.convertException(e);
159        }
160    }
161
162    /**
163     * Returns true if this iterator has more elements when traversing in the
164     * reverse direction.  It returns false if the iterator has been closed.
165     * This method conforms to the {@link ListIterator#hasPrevious} interface.
166     *
167     * @return whether {@link #previous()} will succeed.
168     *
169     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is thrown.
170     */
171    public boolean hasPrevious() {
172
173        if (cursor == null) {
174            return false;
175        }
176        try {
177            if (toPrevious != 0) {
178                OperationStatus status = move(toPrevious);
179                if (status == OperationStatus.SUCCESS) {
180                    toPrevious = 0;
181                    toNext = MOVE_NEXT;
182                    toCurrent = MOVE_NEXT;
183                }
184            }
185            return (toPrevious == 0);
186        } catch (Exception e) {
187            throw StoredContainer.convertException(e);
188        }
189    }
190
191    /**
192     * Returns the next element in the iteration.
193     * This method conforms to the {@link Iterator#next} interface.
194     *
195     * @return the next element.
196     *
197     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
198     * thrown.
199     */
200    public Object next() {
201
202        try {
203            if (toNext != 0) {
204                OperationStatus status = move(toNext);
205                if (status == OperationStatus.SUCCESS) {
206                    toNext = 0;
207                }
208            }
209            if (toNext == 0) {
210                currentData = coll.makeIteratorData(this, cursor);
211                toNext = MOVE_NEXT;
212                toPrevious = 0;
213                toCurrent = 0;
214                setAndRemoveAllowed = true;
215                return currentData;
216            }
217            // else throw NoSuchElementException below
218        } catch (Exception e) {
219            throw StoredContainer.convertException(e);
220        }
221        throw new NoSuchElementException();
222    }
223
224    /**
225     * Returns the next element in the iteration.
226     * This method conforms to the {@link ListIterator#previous} interface.
227     *
228     * @return the previous element.
229     *
230     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
231     * thrown.
232     */
233    public Object previous() {
234
235        try {
236            if (toPrevious != 0) {
237                OperationStatus status = move(toPrevious);
238                if (status == OperationStatus.SUCCESS) {
239                    toPrevious = 0;
240                }
241            }
242            if (toPrevious == 0) {
243                currentData = coll.makeIteratorData(this, cursor);
244                toPrevious = MOVE_PREV;
245                toNext = 0;
246                toCurrent = 0;
247                setAndRemoveAllowed = true;
248                return currentData;
249            }
250            // else throw NoSuchElementException below
251        } catch (Exception e) {
252            throw StoredContainer.convertException(e);
253        }
254        throw new NoSuchElementException();
255    }
256
257    /**
258     * Returns the index of the element that would be returned by a subsequent
259     * call to next.
260     * This method conforms to the {@link ListIterator#nextIndex} interface
261     * except that it returns Integer.MAX_VALUE for stored lists when
262     * positioned at the end of the list, rather than returning the list size
263     * as specified by the ListIterator interface. This is because the database
264     * size is not available.
265     *
266     * @return the next index.
267     *
268     * @throws UnsupportedOperationException if this iterator's collection does
269     * not use record number keys.
270     *
271     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
272     * thrown.
273     */
274    public int nextIndex() {
275
276        if (!coll.view.recNumAccess) {
277            throw new UnsupportedOperationException(
278                "Record number access not supported");
279        }
280        try {
281            return hasNext() ? (cursor.getCurrentRecordNumber() -
282                                coll.getIndexOffset())
283                             : Integer.MAX_VALUE;
284        } catch (Exception e) {
285            throw StoredContainer.convertException(e);
286        }
287    }
288
289    /**
290     * Returns the index of the element that would be returned by a subsequent
291     * call to previous.
292     * This method conforms to the {@link ListIterator#previousIndex}
293     * interface.
294     *
295     * @return the previous index.
296     *
297     * @throws UnsupportedOperationException if this iterator's collection does
298     * not use record number keys.
299     *
300     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
301     * thrown.
302     */
303    public int previousIndex() {
304
305        if (!coll.view.recNumAccess) {
306            throw new UnsupportedOperationException(
307                "Record number access not supported");
308        }
309        try {
310            return hasPrevious() ? (cursor.getCurrentRecordNumber() -
311                                    coll.getIndexOffset())
312                                 : (-1);
313        } catch (Exception e) {
314            throw StoredContainer.convertException(e);
315        }
316    }
317
318    /**
319     * Replaces the last element returned by next or previous with the
320     * specified element (optional operation).
321     * This method conforms to the {@link ListIterator#set} interface.
322     *
323     * <p>In order to call this method, if the underlying Database is
324     * transactional then a transaction must be active when creating the
325     * iterator.</p>
326     *
327     * @param value the new value.
328     *
329     * @throws UnsupportedOperationException if the collection is a {@link
330     * StoredKeySet} (the set returned by {@link java.util.Map#keySet}), or if
331     * duplicates are sorted since this would change the iterator position, or
332     * if the collection is indexed, or if the collection is read-only.
333     *
334     * @throws IllegalArgumentException if an entity value binding is used and
335     * the primary key of the value given is different than the existing stored
336     * primary key.
337     *
338     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
339     * thrown.
340     */
341    public void set(Object value) {
342
343        if (!coll.hasValues()) throw new UnsupportedOperationException();
344        if (!setAndRemoveAllowed) throw new IllegalStateException();
345        try {
346            moveToCurrent();
347            cursor.putCurrent(value);
348        } catch (Exception e) {
349            throw StoredContainer.convertException(e);
350        }
351    }
352
353    /**
354     * Removes the last element that was returned by next or previous (optional
355     * operation).
356     * This method conforms to the {@link ListIterator#remove} interface except
357     * that when the collection is a list and the RECNO-RENUMBER access method
358     * is not used, list indices will not be renumbered.
359     *
360     * <p>In order to call this method, if the underlying Database is
361     * transactional then a transaction must be active when creating the
362     * iterator.</p>
363     *
364     * <p>Note that for the JE product, RECNO-RENUMBER databases are not
365     * supported, and therefore list indices are never renumbered by this
366     * method.</p>
367     *
368     * @throws UnsupportedOperationException if the collection is a sublist, or
369     * if the collection is read-only.
370     *
371     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
372     * thrown.
373     */
374    public void remove() {
375
376        if (!setAndRemoveAllowed) throw new IllegalStateException();
377        try {
378            moveToCurrent();
379            cursor.delete();
380            setAndRemoveAllowed = false;
381            toNext = MOVE_NEXT;
382            toPrevious = MOVE_PREV;
383        } catch (Exception e) {
384            throw StoredContainer.convertException(e);
385        }
386    }
387
388    /**
389     * Inserts the specified element into the list or inserts a duplicate into
390     * other types of collections (optional operation).
391     * This method conforms to the {@link ListIterator#add} interface when
392     * the collection is a list and the RECNO-RENUMBER access method is used.
393     * Otherwise, this method may only be called when duplicates are allowed.
394     * If duplicates are unsorted, the new value will be inserted in the same
395     * manner as list elements.
396     * If duplicates are sorted, the new value will be inserted in sort order.
397     *
398     * <p>Note that for the JE product, RECNO-RENUMBER databases are not
399     * supported, and therefore this method may only be used to add
400     * duplicates.</p>
401     *
402     * @param value the new value.
403     *
404     * @throws UnsupportedOperationException if the collection is a sublist, or
405     * if the collection is indexed, or if the collection is read-only, or if
406     * the collection is a list and the RECNO-RENUMBER access method was not
407     * used, or if the collection is not a list and duplicates are not allowed.
408     *
409     * @throws IllegalStateException if the collection is empty and is not a
410     * list with RECNO-RENUMBER access.
411     *
412     * @throws IllegalArgumentException if a duplicate value is being added
413     * that already exists and duplicates are sorted.
414     *
415     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
416     * thrown.
417     */
418    public void add(Object value) {
419
420        coll.checkIterAddAllowed();
421        try {
422            OperationStatus status = OperationStatus.SUCCESS;
423            if (toNext != 0 && toPrevious != 0) { // database is empty
424                if (coll.view.keysRenumbered) { // recno-renumber database
425                    /*
426                     * Close cursor during append and then reopen to support
427                     * CDB restriction that append may not be called with a
428                     * cursor open; note the append will still fail if the
429                     * application has another cursor open.
430                     */
431                    close();
432                    status = coll.view.append(value, null, null);
433                    cursor = new DataCursor(coll.view, writeAllowed);
434                    reset();
435                    next(); // move past new record
436                } else { // hash/btree with duplicates
437                    throw new IllegalStateException(
438                        "Collection is empty, cannot add() duplicate");
439                }
440            } else { // database is not empty
441                boolean putBefore = false;
442                if (coll.view.keysRenumbered) { // recno-renumber database
443                    moveToCurrent();
444                    if (hasNext()) {
445                        status = cursor.putBefore(value);
446                        putBefore = true;
447                    } else {
448                        status = cursor.putAfter(value);
449                    }
450                } else { // hash/btree with duplicates
451                    if (coll.areDuplicatesOrdered()) {
452                        status = cursor.putNoDupData(null, value, null, true);
453                    } else if (toNext == 0) {
454                        status = cursor.putBefore(value);
455                        putBefore = true;
456                    } else {
457                        status = cursor.putAfter(value);
458                    }
459                }
460                if (putBefore) {
461                    toPrevious = 0;
462                    toNext = MOVE_NEXT;
463                }
464            }
465            if (status == OperationStatus.KEYEXIST) {
466                throw new IllegalArgumentException("Duplicate value");
467            } else if (status != OperationStatus.SUCCESS) {
468                throw new IllegalArgumentException("Could not insert: " +
469                                                    status);
470            }
471            setAndRemoveAllowed = false;
472        } catch (Exception e) {
473            throw StoredContainer.convertException(e);
474        }
475    }
476
477    // --- end Iterator/ListIterator methods ---
478
479    /**
480     * Resets cursor to an uninitialized state.
481     */
482    private void reset() {
483
484        toNext = MOVE_FIRST;
485        toPrevious = MOVE_PREV;
486        toCurrent = 0;
487        currentData = null;
488        /*
489	 * Initialize cursor at beginning to avoid "initial previous == last"
490	 * behavior when cursor is uninitialized.
491	 *
492	 * FindBugs whines about us ignoring the return value from hasNext().
493	 */
494        hasNext();
495    }
496
497    /**
498     * Returns the number of elements having the same key value as the key
499     * value of the element last returned by next() or previous().  If no
500     * duplicates are allowed, 1 is always returned.
501     * This method does not exist in the standard {@link Iterator} or {@link
502     * ListIterator} interfaces.
503     *
504     * @return the number of duplicates.
505     *
506     * @throws IllegalStateException if next() or previous() has not been
507     * called for this iterator, or if remove() or add() were called after
508     * the last call to next() or previous().
509     */
510    public int count() {
511
512        if (!setAndRemoveAllowed) throw new IllegalStateException();
513        try {
514            moveToCurrent();
515            return cursor.count();
516        } catch (Exception e) {
517            throw StoredContainer.convertException(e);
518        }
519    }
520
521    /**
522     * Closes this iterator.
523     * This method does not exist in the standard {@link Iterator} or {@link
524     * ListIterator} interfaces.
525     *
526     * <p>After being closed, only the {@link #hasNext} and {@link
527     * #hasPrevious} methods may be called and these will return false.  {@link
528     * #close()} may also be called again and will do nothing.  If other
529     * methods are called a <code>NullPointerException</code> will generally be
530     * thrown.</p>
531     *
532     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
533     * thrown.
534     */
535    public void close() {
536
537        if (cursor != null) {
538            coll.closeCursor(cursor);
539            cursor = null;
540        }
541    }
542
543    /**
544     * Returns the collection associated with this iterator.
545     * This method does not exist in the standard {@link Iterator} or {@link
546     * ListIterator} interfaces.
547     *
548     * @return the collection associated with this iterator.
549     */
550    public final StoredCollection getCollection() {
551
552        return coll;
553    }
554
555    // --- begin BaseIterator methods ---
556
557    public final ListIterator dup() {
558
559        try {
560            StoredIterator o = (StoredIterator) super.clone();
561            o.cursor = cursor.cloneCursor();
562            return o;
563        } catch (Exception e) {
564            throw StoredContainer.convertException(e);
565        }
566    }
567
568    public final boolean isCurrentData(Object currentData) {
569
570        return (this.currentData == currentData);
571    }
572
573    public final boolean moveToIndex(int index) {
574
575        try {
576            OperationStatus status =
577                cursor.getSearchKey(new Integer(index), null, lockForWrite);
578            setAndRemoveAllowed = (status == OperationStatus.SUCCESS);
579            return setAndRemoveAllowed;
580        } catch (Exception e) {
581            throw StoredContainer.convertException(e);
582        }
583    }
584
585    // --- end BaseIterator methods ---
586
587    private void moveToCurrent()
588        throws DatabaseException {
589
590        if (toCurrent != 0) {
591            move(toCurrent);
592            toCurrent = 0;
593        }
594    }
595
596    private OperationStatus move(int direction)
597        throws DatabaseException {
598
599        switch (direction) {
600            case MOVE_NEXT:
601                if (coll.iterateDuplicates()) {
602                    return cursor.getNext(lockForWrite);
603                } else {
604                    return cursor.getNextNoDup(lockForWrite);
605                }
606            case MOVE_PREV:
607                if (coll.iterateDuplicates()) {
608                    return cursor.getPrev(lockForWrite);
609                } else {
610                    return cursor.getPrevNoDup(lockForWrite);
611                }
612            case MOVE_FIRST:
613                return cursor.getFirst(lockForWrite);
614            default:
615                throw new IllegalArgumentException(String.valueOf(direction));
616        }
617    }
618}
619