1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2000,2008 Oracle.  All rights reserved.
5 *
6 * $Id: StoredCollection.java,v 12.10 2008/02/07 17:12:26 mark Exp $
7 */
8
9package com.sleepycat.collections;
10
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Iterator;
15import java.util.List;
16
17import com.sleepycat.compat.DbCompat;
18import com.sleepycat.db.CursorConfig;
19import com.sleepycat.db.DatabaseEntry;
20import com.sleepycat.db.DatabaseException;
21import com.sleepycat.db.JoinConfig;
22import com.sleepycat.db.OperationStatus;
23
24/**
25 * A abstract base class for all stored collections.  This class, and its
26 * base class {@link StoredContainer}, provide implementations of most methods
27 * in the {@link Collection} interface.  Other methods, such as {@link #add}
28 * and {@link #remove}, are provided by concrete classes that extend this
29 * class.
30 *
31 * <p>In addition, this class provides the following methods for stored
32 * collections only.  Note that the use of these methods is not compatible with
33 * the standard Java collections interface.</p>
34 * <ul>
35 * <li>{@link #getIteratorBlockSize}</li>
36 * <li>{@link #setIteratorBlockSize}</li>
37 * <li>{@link #storedIterator()}</li>
38 * <li>{@link #storedIterator(boolean)}</li>
39 * <li>{@link #join}</li>
40 * <li>{@link #toList()}</li>
41 * </ul>
42 *
43 * @author Mark Hayes
44 */
45public abstract class StoredCollection extends StoredContainer
46    implements Collection {
47
48    /**
49     * The default number of records read at one time by iterators.
50     * @see #setIteratorBlockSize
51     */
52    public static final int DEFAULT_ITERATOR_BLOCK_SIZE = 10;
53
54    private int iteratorBlockSize = DEFAULT_ITERATOR_BLOCK_SIZE;
55
56    StoredCollection(DataView view) {
57
58        super(view);
59    }
60
61    /**
62     * Returns the number of records read at one time by iterators returned by
63     * the {@link #iterator} method.  By default this value is {@link
64     * #DEFAULT_ITERATOR_BLOCK_SIZE}.
65     */
66    public int getIteratorBlockSize() {
67
68        return iteratorBlockSize;
69    }
70
71    /**
72     * Changes the number of records read at one time by iterators returned by
73     * the {@link #iterator} method.  By default this value is {@link
74     * #DEFAULT_ITERATOR_BLOCK_SIZE}.
75     *
76     * @throws IllegalArgumentException if the blockSize is less than two.
77     */
78    public void setIteratorBlockSize(int blockSize) {
79
80        if (blockSize < 2) {
81            throw new IllegalArgumentException
82                ("blockSize is less than two: " + blockSize);
83        }
84
85        iteratorBlockSize = blockSize;
86    }
87
88    final boolean add(Object key, Object value) {
89
90        DataCursor cursor = null;
91        boolean doAutoCommit = beginAutoCommit();
92        try {
93            cursor = new DataCursor(view, true);
94            OperationStatus status =
95                cursor.putNoDupData(key, value, null, false);
96            closeCursor(cursor);
97            commitAutoCommit(doAutoCommit);
98            return (status == OperationStatus.SUCCESS);
99        } catch (Exception e) {
100            closeCursor(cursor);
101            throw handleException(e, doAutoCommit);
102        }
103    }
104
105    BlockIterator blockIterator() {
106        return new BlockIterator(this, isWriteAllowed(), iteratorBlockSize);
107    }
108
109    /**
110     * Returns an iterator over the elements in this collection.
111     * The iterator will be read-only if the collection is read-only.
112     * This method conforms to the {@link Collection#iterator} interface.
113     *
114     * <p>The iterator returned by this method does not keep a database cursor
115     * open and therefore it does not need to be closed.  It reads blocks of
116     * records as needed, opening and closing a cursor to read each block of
117     * records.  The number of records per block is 10 by default and can be
118     * changed with {@link #setIteratorBlockSize}.</p>
119     *
120     * <p>Because this iterator does not keep a cursor open, if it is used
121     * without transactions, the iterator does not have <em>cursor
122     * stability</em> characteristics.  In other words, the record at the
123     * current iterator position can be changed or deleted by another thread.
124     * To prevent this from happening, call this method within a transaction or
125     * use the {@link #storedIterator()} method instead.</p>
126     *
127     * @return a standard {@link Iterator} for this collection.
128     *
129     * @see #isWriteAllowed
130     */
131    public Iterator iterator() {
132        return blockIterator();
133    }
134
135    /**
136     * Returns an iterator over the elements in this collection.
137     * The iterator will be read-only if the collection is read-only.
138     * This method does not exist in the standard {@link Collection} interface.
139     *
140     * <p>If {@code Iterator.set} or {@code Iterator.remove} will be called
141     * and the underlying Database is transactional, then a transaction must be
142     * active when calling this method and must remain active while using the
143     * iterator.</p>
144     *
145     * <p><strong>Warning:</strong> The iterator returned must be explicitly
146     * closed using {@link StoredIterator#close()} or {@link
147     * StoredIterator#close(java.util.Iterator)} to release the underlying
148     * database cursor resources.</p>
149     *
150     * @return a {@link StoredIterator} for this collection.
151     *
152     * @see #isWriteAllowed
153     */
154    public StoredIterator storedIterator() {
155
156        return storedIterator(isWriteAllowed());
157    }
158
159    /**
160     * Returns a read or read-write iterator over the elements in this
161     * collection.
162     * This method does not exist in the standard {@link Collection} interface.
163     *
164     * <p>If {@code Iterator.set} or {@code Iterator.remove} will be called
165     * and the underlying Database is transactional, then a transaction must be
166     * active when calling this method and must remain active while using the
167     * iterator.</p>
168     *
169     * <p><strong>Warning:</strong> The iterator returned must be explicitly
170     * closed using {@link StoredIterator#close()} or {@link
171     * StoredIterator#close(java.util.Iterator)} to release the underlying
172     * database cursor resources.</p>
173     *
174     * @param writeAllowed is true to open a read-write iterator or false to
175     * open a read-only iterator.  If the collection is read-only the iterator
176     * will always be read-only.
177     *
178     * @return a {@link StoredIterator} for this collection.
179     *
180     * @throws IllegalStateException if writeAllowed is true but the collection
181     * is read-only.
182     *
183     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
184     * thrown.
185     *
186     * @see #isWriteAllowed
187     */
188    public StoredIterator storedIterator(boolean writeAllowed) {
189
190        try {
191            return new StoredIterator(this, writeAllowed && isWriteAllowed(),
192                                      null);
193        } catch (Exception e) {
194            throw StoredContainer.convertException(e);
195        }
196    }
197
198    /**
199     * @deprecated Please use {@link #storedIterator()} or {@link
200     * #storedIterator(boolean)} instead.  Because the iterator returned must
201     * be closed, the method name {@code iterator} is confusing since standard
202     * Java iterators do not need to be closed.
203     */
204    public StoredIterator iterator(boolean writeAllowed) {
205
206        return storedIterator(writeAllowed);
207    }
208
209    /**
210     * Returns an array of all the elements in this collection.
211     * This method conforms to the {@link Collection#toArray()} interface.
212     *
213     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
214     * thrown.
215     */
216    public Object[] toArray() {
217
218        ArrayList list = new ArrayList();
219        StoredIterator i = storedIterator();
220        try {
221            while (i.hasNext()) {
222                list.add(i.next());
223            }
224        } finally {
225            i.close();
226        }
227        return list.toArray();
228    }
229
230    /**
231     * Returns an array of all the elements in this collection whose runtime
232     * type is that of the specified array.
233     * This method conforms to the {@link Collection#toArray(Object[])}
234     * interface.
235     *
236     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
237     * thrown.
238     */
239    public Object[] toArray(Object[] a) {
240
241        int j = 0;
242        StoredIterator i = storedIterator();
243        try {
244            while (j < a.length && i.hasNext()) {
245                a[j++] = i.next();
246            }
247            if (j < a.length) {
248                a[j] = null;
249            } else if (i.hasNext()) {
250                ArrayList list = new ArrayList(Arrays.asList(a));
251                while (i.hasNext()) {
252                    list.add(i.next());
253                }
254                a = list.toArray(a);
255            }
256        } finally {
257            i.close();
258        }
259        return a;
260    }
261
262    /**
263     * Returns true if this collection contains all of the elements in the
264     * specified collection.
265     * This method conforms to the {@link Collection#containsAll} interface.
266     *
267     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
268     * thrown.
269     */
270    public boolean containsAll(Collection coll) {
271	Iterator i = storedOrExternalIterator(coll);
272        try {
273            while (i.hasNext()) {
274                if (!contains(i.next())) {
275                    return false;
276                }
277            }
278        } finally {
279            StoredIterator.close(i);
280        }
281	return true;
282    }
283
284    /**
285     * Adds all of the elements in the specified collection to this collection
286     * (optional operation).
287     * This method calls the {@link #add(Object)} method of the concrete
288     * collection class, which may or may not be supported.
289     * This method conforms to the {@link Collection#addAll} interface.
290     *
291     * @throws UnsupportedOperationException if the collection is read-only, or
292     * if the collection is indexed, or if the add method is not supported by
293     * the concrete collection.
294     *
295     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
296     * thrown.
297     */
298    public boolean addAll(Collection coll) {
299	Iterator i = null;
300        boolean doAutoCommit = beginAutoCommit();
301        try {
302            i = storedOrExternalIterator(coll);
303            boolean changed = false;
304            while (i.hasNext()) {
305                if (add(i.next())) {
306                    changed = true;
307                }
308            }
309            StoredIterator.close(i);
310            commitAutoCommit(doAutoCommit);
311            return changed;
312        } catch (Exception e) {
313            StoredIterator.close(i);
314            throw handleException(e, doAutoCommit);
315        }
316    }
317
318    /**
319     * Removes all this collection's elements that are also contained in the
320     * specified collection (optional operation).
321     * This method conforms to the {@link Collection#removeAll} interface.
322     *
323     * @throws UnsupportedOperationException if the collection is read-only.
324     *
325     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
326     * thrown.
327     */
328    public boolean removeAll(Collection coll) {
329
330        return removeAll(coll, true);
331    }
332
333    /**
334     * Retains only the elements in this collection that are contained in the
335     * specified collection (optional operation).
336     * This method conforms to the {@link Collection#removeAll} interface.
337     *
338     * @throws UnsupportedOperationException if the collection is read-only.
339     *
340     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
341     * thrown.
342     */
343    public boolean retainAll(Collection coll) {
344
345        return removeAll(coll, false);
346    }
347
348    private boolean removeAll(Collection coll, boolean ifExistsInColl) {
349	StoredIterator i = null;
350        boolean doAutoCommit = beginAutoCommit();
351        try {
352            boolean changed = false;
353            i = storedIterator();
354            while (i.hasNext()) {
355                if (ifExistsInColl == coll.contains(i.next())) {
356                    i.remove();
357                    changed = true;
358                }
359            }
360            i.close();
361            commitAutoCommit(doAutoCommit);
362            return changed;
363        } catch (Exception e) {
364            if (i != null) {
365                i.close();
366            }
367            throw handleException(e, doAutoCommit);
368        }
369    }
370
371    /**
372     * Compares the specified object with this collection for equality.
373     * A value comparison is performed by this method and the stored values
374     * are compared rather than calling the equals() method of each element.
375     * This method conforms to the {@link Collection#equals} interface.
376     *
377     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
378     * thrown.
379     */
380    public boolean equals(Object other) {
381
382        if (other instanceof Collection) {
383            Collection otherColl = StoredCollection.copyCollection(other);
384            StoredIterator i = storedIterator();
385            try {
386                while (i.hasNext()) {
387                    if (!otherColl.remove(i.next())) {
388                        return false;
389                    }
390                }
391                return otherColl.isEmpty();
392            } finally {
393                i.close();
394            }
395        } else {
396            return false;
397        }
398    }
399
400    /*
401     * Add this in to keep FindBugs from whining at us about implementing
402     * equals(), but not hashCode().
403     */
404    public int hashCode() {
405	return super.hashCode();
406    }
407
408    /**
409     * Returns a copy of this collection as an ArrayList.  This is the same as
410     * {@link #toArray()} but returns a collection instead of an array.
411     *
412     * @return an {@link ArrayList} containing a copy of all elements in this
413     * collection.
414     *
415     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
416     * thrown.
417     */
418    public List toList() {
419
420        ArrayList list = new ArrayList();
421        StoredIterator i = storedIterator();
422        try {
423            while (i.hasNext()) list.add(i.next());
424            return list;
425        } finally {
426            i.close();
427        }
428    }
429
430    /**
431     * Converts the collection to a string representation for debugging.
432     * WARNING: The returned string may be very large.
433     *
434     * @return the string representation.
435     *
436     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
437     * thrown.
438     */
439    public String toString() {
440	StringBuffer buf = new StringBuffer();
441	buf.append("[");
442	StoredIterator i = storedIterator();
443        try {
444            while (i.hasNext()) {
445                if (buf.length() > 1) buf.append(',');
446                buf.append(i.next().toString());
447            }
448            buf.append(']');
449            return buf.toString();
450        } finally {
451            i.close();
452        }
453    }
454
455    // Inherit javadoc
456    public int size() {
457
458        boolean countDups = iterateDuplicates();
459        if (DbCompat.DATABASE_COUNT && countDups && !view.range.hasBound()) {
460            try {
461                return (int) DbCompat.getDatabaseCount(view.db);
462            } catch (Exception e) {
463                throw StoredContainer.convertException(e);
464            }
465        } else {
466            int count = 0;
467            CursorConfig cursorConfig = view.currentTxn.isLockingMode() ?
468                CursorConfig.READ_UNCOMMITTED : null;
469            DataCursor cursor = null;
470            try {
471                cursor = new DataCursor(view, false, cursorConfig);
472                OperationStatus status = cursor.getFirst(false);
473                while (status == OperationStatus.SUCCESS) {
474                    if (countDups) {
475                        count += cursor.count();
476                    } else {
477                        count += 1;
478                    }
479                    status = cursor.getNextNoDup(false);
480                }
481            } catch (Exception e) {
482                throw StoredContainer.convertException(e);
483            } finally {
484                closeCursor(cursor);
485            }
486            return count;
487        }
488    }
489
490    /**
491     * Returns an iterator representing an equality join of the indices and
492     * index key values specified.
493     * This method does not exist in the standard {@link Collection} interface.
494     *
495     * <p><strong>Warning:</strong> The iterator returned must be explicitly
496     * closed using {@link StoredIterator#close()} or {@link
497     * StoredIterator#close(java.util.Iterator)} to release the underlying
498     * database cursor resources.</p>
499     *
500     * <p>The returned iterator supports only the two methods: hasNext() and
501     * next().  All other methods will throw UnsupportedOperationException.</p>
502     *
503     * @param indices is an array of indices with elements corresponding to
504     * those in the indexKeys array.
505     *
506     * @param indexKeys is an array of index key values identifying the
507     * elements to be selected.
508     *
509     * @param joinConfig is the join configuration, or null to use the
510     * default configuration.
511     *
512     * @return an iterator over the elements in this collection that match
513     * all specified index key values.
514     *
515     * @throws IllegalArgumentException if this collection is indexed or if a
516     * given index does not have the same store as this collection.
517     *
518     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
519     * thrown.
520     */
521    public StoredIterator join(StoredContainer[] indices, Object[] indexKeys,
522                               JoinConfig joinConfig) {
523
524        try {
525            DataView[] indexViews = new DataView[indices.length];
526            for (int i = 0; i < indices.length; i += 1) {
527                indexViews[i] = indices[i].view;
528            }
529            DataCursor cursor = view.join(indexViews, indexKeys, joinConfig);
530            return new StoredIterator(this, false, cursor);
531        } catch (Exception e) {
532            throw StoredContainer.convertException(e);
533        }
534    }
535
536    final Object getFirstOrLast(boolean doGetFirst) {
537
538        DataCursor cursor = null;
539        try {
540            cursor = new DataCursor(view, false);
541            OperationStatus status;
542            if (doGetFirst) {
543                status = cursor.getFirst(false);
544            } else {
545                status = cursor.getLast(false);
546            }
547            return (status == OperationStatus.SUCCESS) ?
548                    makeIteratorData(null, cursor) : null;
549        } catch (Exception e) {
550            throw StoredContainer.convertException(e);
551        } finally {
552            closeCursor(cursor);
553        }
554    }
555
556    Object makeIteratorData(BaseIterator iterator, DataCursor cursor) {
557
558        return makeIteratorData(iterator,
559                                cursor.getKeyThang(),
560                                cursor.getPrimaryKeyThang(),
561                                cursor.getValueThang());
562    }
563
564    abstract Object makeIteratorData(BaseIterator iterator,
565                                     DatabaseEntry keyEntry,
566                                     DatabaseEntry priKeyEntry,
567                                     DatabaseEntry valueEntry);
568
569    abstract boolean hasValues();
570
571    boolean iterateDuplicates() {
572
573        return true;
574    }
575
576    void checkIterAddAllowed()
577        throws UnsupportedOperationException {
578
579        if (!areDuplicatesAllowed()) {
580            throw new UnsupportedOperationException("duplicates required");
581        }
582    }
583
584    int getIndexOffset() {
585
586        return 0;
587    }
588
589    private static Collection copyCollection(Object other) {
590
591        if (other instanceof StoredCollection) {
592            return ((StoredCollection) other).toList();
593        } else {
594            return new ArrayList((Collection) other);
595        }
596    }
597}
598