1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2000,2008 Oracle.  All rights reserved.
5 *
6 * $Id: StoredList.java,v 12.8 2008/02/07 17:12:26 mark Exp $
7 */
8
9package com.sleepycat.collections;
10
11import java.util.Collection;
12import java.util.Iterator;
13import java.util.List;
14import java.util.ListIterator;
15
16import com.sleepycat.bind.EntityBinding;
17import com.sleepycat.bind.EntryBinding;
18import com.sleepycat.bind.RecordNumberBinding;
19import com.sleepycat.db.Database;
20import com.sleepycat.db.DatabaseEntry;
21import com.sleepycat.db.DatabaseException;
22import com.sleepycat.db.OperationStatus;
23import com.sleepycat.util.keyrange.KeyRangeException;
24
25/**
26 * A List view of a {@link Database}.
27 *
28 * <p>For all stored lists the keys of the underlying Database
29 * must have record number format, and therefore the store or index must be a
30 * RECNO, RECNO-RENUMBER, QUEUE, or BTREE-RECNUM database.  Only RECNO-RENUMBER
31 * allows true list behavior where record numbers are renumbered following the
32 * position of an element that is added or removed.  For the other access
33 * methods (RECNO, QUEUE, and BTREE-RECNUM), stored Lists are most useful as
34 * read-only collections where record numbers are not required to be
35 * sequential.</p>
36 *
37 * <p>In addition to the standard List methods, this class provides the
38 * following methods for stored lists only.  Note that the use of these methods
39 * is not compatible with the standard Java collections interface.</p>
40 * <ul>
41 * <li>{@link #append(Object)}</li>
42 * </ul>
43 * @author Mark Hayes
44 */
45public class StoredList extends StoredCollection implements List {
46
47    private static final EntryBinding DEFAULT_KEY_BINDING =
48        new IndexKeyBinding(1);
49
50    private int baseIndex = 1;
51    private boolean isSubList;
52
53    /**
54     * Creates a list view of a {@link Database}.
55     *
56     * @param database is the Database underlying the new collection.
57     *
58     * @param valueBinding is the binding used to translate between value
59     * buffers and value objects.
60     *
61     * @param writeAllowed is true to create a read-write collection or false
62     * to create a read-only collection.
63     *
64     * @throws IllegalArgumentException if formats are not consistently
65     * defined or a parameter is invalid.
66     *
67     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
68     * thrown.
69     */
70    public StoredList(Database database, EntryBinding valueBinding,
71                      boolean writeAllowed) {
72
73        super(new DataView(database, DEFAULT_KEY_BINDING, valueBinding, null,
74                           writeAllowed, null));
75    }
76
77    /**
78     * Creates a list entity view of a {@link Database}.
79     *
80     * @param database is the Database underlying the new collection.
81     *
82     * @param valueEntityBinding is the binding used to translate between
83     * key/value buffers and entity value objects.
84     *
85     * @param writeAllowed is true to create a read-write collection or false
86     * to create a read-only collection.
87     *
88     * @throws IllegalArgumentException if formats are not consistently
89     * defined or a parameter is invalid.
90     *
91     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
92     * thrown.
93     */
94    public StoredList(Database database, EntityBinding valueEntityBinding,
95                      boolean writeAllowed) {
96
97        super(new DataView(database, DEFAULT_KEY_BINDING, null,
98                           valueEntityBinding, writeAllowed, null));
99    }
100
101    /**
102     * Creates a list view of a {@link Database} with a {@link
103     * PrimaryKeyAssigner}.  Writing is allowed for the created list.
104     *
105     * @param database is the Database underlying the new collection.
106     *
107     * @param valueBinding is the binding used to translate between value
108     * buffers and value objects.
109     *
110     * @param keyAssigner is used by the {@link #add} and {@link #append}
111     * methods to assign primary keys.
112     *
113     * @throws IllegalArgumentException if formats are not consistently
114     * defined or a parameter is invalid.
115     *
116     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
117     * thrown.
118     */
119    public StoredList(Database database, EntryBinding valueBinding,
120                      PrimaryKeyAssigner keyAssigner) {
121
122        super(new DataView(database, DEFAULT_KEY_BINDING, valueBinding,
123                           null, true, keyAssigner));
124    }
125
126    /**
127     * Creates a list entity view of a {@link Database} with a {@link
128     * PrimaryKeyAssigner}.  Writing is allowed for the created list.
129     *
130     * @param database is the Database underlying the new collection.
131     *
132     * @param valueEntityBinding is the binding used to translate between
133     * key/value buffers and entity value objects.
134     *
135     * @param keyAssigner is used by the {@link #add} and {@link #append}
136     * methods to assign primary keys.
137     *
138     * @throws IllegalArgumentException if formats are not consistently
139     * defined or a parameter is invalid.
140     *
141     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
142     * thrown.
143     */
144    public StoredList(Database database, EntityBinding valueEntityBinding,
145                      PrimaryKeyAssigner keyAssigner) {
146
147        super(new DataView(database, DEFAULT_KEY_BINDING, null,
148                           valueEntityBinding, true, keyAssigner));
149    }
150
151    private StoredList(DataView view, int baseIndex) {
152
153        super(view);
154        this.baseIndex = baseIndex;
155        this.isSubList = true;
156    }
157
158    /**
159     * Inserts the specified element at the specified position in this list
160     * (optional operation).
161     * This method conforms to the {@link List#add(int, Object)} interface.
162     *
163     * @throws UnsupportedOperationException if the collection is a sublist, or
164     * if the collection is indexed, or if the collection is read-only, or if
165     * the RECNO-RENUMBER access method was not used.
166     *
167     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
168     * thrown.
169     */
170    public void add(int index, Object value) {
171
172        checkIterAddAllowed();
173        DataCursor cursor = null;
174        boolean doAutoCommit = beginAutoCommit();
175        try {
176            cursor = new DataCursor(view, true);
177            OperationStatus status =
178                cursor.getSearchKey(new Long(index), null, false);
179            if (status == OperationStatus.SUCCESS) {
180                cursor.putBefore(value);
181                closeCursor(cursor);
182            } else {
183                closeCursor(cursor);
184                cursor = null;
185                view.append(value, null, null);
186            }
187            commitAutoCommit(doAutoCommit);
188        } catch (Exception e) {
189            closeCursor(cursor);
190            throw handleException(e, doAutoCommit);
191        }
192    }
193
194    /**
195     * Appends the specified element to the end of this list (optional
196     * operation).
197     * This method conforms to the {@link List#add(Object)} interface.
198     *
199     * @throws UnsupportedOperationException if the collection is a sublist, or
200     * if the collection is indexed, or if the collection is read-only, or if
201     * the RECNO-RENUMBER access method was not used.
202     *
203     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
204     * thrown.
205     */
206    public boolean add(Object value) {
207
208        checkIterAddAllowed();
209        boolean doAutoCommit = beginAutoCommit();
210        try {
211            view.append(value, null, null);
212            commitAutoCommit(doAutoCommit);
213            return true;
214        } catch (Exception e) {
215            throw handleException(e, doAutoCommit);
216        }
217    }
218
219    /**
220     * Appends a given value returning the newly assigned index.
221     * If a {@link com.sleepycat.collections.PrimaryKeyAssigner} is associated
222     * with Store for this list, it will be used to assigned the returned
223     * index.  Otherwise the Store must be a QUEUE or RECNO database and the
224     * next available record number is assigned as the index.  This method does
225     * not exist in the standard {@link List} interface.
226     *
227     * @param value the value to be appended.
228     *
229     * @return the assigned index.
230     *
231     * @throws UnsupportedOperationException if the collection is indexed, or
232     * if the collection is read-only, or if the Store has no {@link
233     * com.sleepycat.collections.PrimaryKeyAssigner} and is not a QUEUE or
234     * RECNO database.
235     *
236     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
237     * thrown.
238     */
239    public int append(Object value) {
240
241        boolean doAutoCommit = beginAutoCommit();
242        try {
243            Object[] key = new Object[1];
244            view.append(value, key, null);
245            commitAutoCommit(doAutoCommit);
246            return ((Number) key[0]).intValue();
247        } catch (Exception e) {
248            throw handleException(e, doAutoCommit);
249        }
250    }
251
252    void checkIterAddAllowed()
253        throws UnsupportedOperationException {
254
255        if (isSubList) {
256            throw new UnsupportedOperationException("cannot add to subList");
257        }
258        if (!view.keysRenumbered) { // RECNO-RENUM
259            throw new UnsupportedOperationException(
260                "requires renumbered keys");
261        }
262    }
263
264    /**
265     * Inserts all of the elements in the specified collection into this list
266     * at the specified position (optional operation).
267     * This method conforms to the {@link List#addAll(int, Collection)}
268     * interface.
269     *
270     * @throws UnsupportedOperationException if the collection is a sublist, or
271     * if the collection is indexed, or if the collection is read-only, or if
272     * the RECNO-RENUMBER access method was not used.
273     *
274     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
275     * thrown.
276     */
277    public boolean addAll(int index, Collection coll) {
278
279        checkIterAddAllowed();
280        DataCursor cursor = null;
281	Iterator i = null;
282        boolean doAutoCommit = beginAutoCommit();
283        try {
284            i = storedOrExternalIterator(coll);
285            if (!i.hasNext()) {
286                return false;
287            }
288            cursor = new DataCursor(view, true);
289            OperationStatus status =
290                cursor.getSearchKey(new Long(index), null, false);
291            if (status == OperationStatus.SUCCESS) {
292                while (i.hasNext()) {
293                    cursor.putBefore(i.next());
294                }
295                closeCursor(cursor);
296            } else {
297                closeCursor(cursor);
298                cursor = null;
299                while (i.hasNext()) {
300                    view.append(i.next(), null, null);
301                }
302            }
303            StoredIterator.close(i);
304            commitAutoCommit(doAutoCommit);
305            return true;
306        } catch (Exception e) {
307            closeCursor(cursor);
308            StoredIterator.close(i);
309            throw handleException(e, doAutoCommit);
310        }
311    }
312
313    /**
314     * Returns true if this list contains the specified element.
315     * This method conforms to the {@link List#contains} interface.
316     *
317     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
318     * thrown.
319     */
320    public boolean contains(Object value) {
321
322        return containsValue(value);
323    }
324
325    /**
326     * Returns the element at the specified position in this list.
327     * This method conforms to the {@link List#get} interface.
328     *
329     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
330     * thrown.
331     */
332    public Object get(int index) {
333
334        return super.get(new Long(index));
335    }
336
337    /**
338     * Returns the index in this list of the first occurrence of the specified
339     * element, or -1 if this list does not contain this element.
340     * This method conforms to the {@link List#indexOf} interface.
341     *
342     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
343     * thrown.
344     */
345    public int indexOf(Object value) {
346
347        return indexOf(value, true);
348    }
349
350    /**
351     * Returns the index in this list of the last occurrence of the specified
352     * element, or -1 if this list does not contain this element.
353     * This method conforms to the {@link List#lastIndexOf} interface.
354     *
355     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
356     * thrown.
357     */
358    public int lastIndexOf(Object value) {
359
360        return indexOf(value, false);
361    }
362
363    private int indexOf(Object value, boolean findFirst) {
364
365        DataCursor cursor = null;
366        try {
367            cursor = new DataCursor(view, false);
368            OperationStatus status = cursor.findValue(value, findFirst);
369            return (status == OperationStatus.SUCCESS)
370                    ? (cursor.getCurrentRecordNumber() - baseIndex)
371                    : (-1);
372        } catch (Exception e) {
373            throw StoredContainer.convertException(e);
374        } finally {
375            closeCursor(cursor);
376        }
377    }
378
379    int getIndexOffset() {
380
381        return baseIndex;
382    }
383
384    /**
385     * Returns a list iterator of the elements in this list (in proper
386     * sequence).
387     * The iterator will be read-only if the collection is read-only.
388     * This method conforms to the {@link List#listIterator()} interface.
389     *
390     * <p>For information on cursor stability and iterator block size, see
391     * {@link #iterator()}.</p>
392     *
393     * @return a {@link ListIterator} for this collection.
394     *
395     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
396     * thrown.
397     *
398     * @see #isWriteAllowed
399     */
400    public ListIterator listIterator() {
401
402        return blockIterator();
403    }
404
405    /**
406     * Returns a list iterator of the elements in this list (in proper
407     * sequence), starting at the specified position in this list.
408     * The iterator will be read-only if the collection is read-only.
409     * This method conforms to the {@link List#listIterator(int)} interface.
410     *
411     * <p>For information on cursor stability and iterator block size, see
412     * {@link #iterator()}.</p>
413     *
414     * @return a {@link ListIterator} for this collection.
415     *
416     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
417     * thrown.
418     *
419     * @see #isWriteAllowed
420     */
421    public ListIterator listIterator(int index) {
422
423        BlockIterator i = blockIterator();
424        if (i.moveToIndex(index)) {
425            return i;
426        } else {
427            throw new IndexOutOfBoundsException(String.valueOf(index));
428        }
429    }
430
431    /**
432     * Removes the element at the specified position in this list (optional
433     * operation).
434     * This method conforms to the {@link List#remove(int)} interface.
435     *
436     * @throws UnsupportedOperationException if the collection is a sublist, or
437     * if the collection is read-only.
438     *
439     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
440     * thrown.
441     */
442    public Object remove(int index) {
443
444        try {
445            Object[] oldVal = new Object[1];
446            removeKey(new Long(index), oldVal);
447            return oldVal[0];
448        } catch (IllegalArgumentException e) {
449            throw new IndexOutOfBoundsException(e.getMessage());
450        }
451    }
452
453    /**
454     * Removes the first occurrence in this list of the specified element
455     * (optional operation).
456     * This method conforms to the {@link List#remove(Object)} interface.
457     *
458     * @throws UnsupportedOperationException if the collection is a sublist, or
459     * if the collection is read-only.
460     *
461     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
462     * thrown.
463     */
464    public boolean remove(Object value) {
465
466        return removeValue(value);
467    }
468
469    /**
470     * Replaces the element at the specified position in this list with the
471     * specified element (optional operation).
472     * This method conforms to the {@link List#set} interface.
473     *
474     * @throws UnsupportedOperationException if the collection is indexed, or
475     * if the collection is read-only.
476     *
477     * @throws IllegalArgumentException if an entity value binding is used and
478     * the primary key of the value given is different than the existing stored
479     * primary key.
480     *
481     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
482     * thrown.
483     */
484    public Object set(int index, Object value) {
485
486        try {
487            return put(new Long(index), value);
488        } catch (IllegalArgumentException e) {
489            throw new IndexOutOfBoundsException(e.getMessage());
490        }
491    }
492
493    /**
494     * Returns a view of the portion of this list between the specified
495     * fromIndex, inclusive, and toIndex, exclusive.
496     * Note that add() and remove() may not be called for the returned sublist.
497     * This method conforms to the {@link List#subList} interface.
498     *
499     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
500     * thrown.
501     */
502    public List subList(int fromIndex, int toIndex) {
503
504        if (fromIndex < 0 || fromIndex > toIndex) {
505            throw new IndexOutOfBoundsException(String.valueOf(fromIndex));
506        }
507        try {
508            int newBaseIndex = baseIndex + fromIndex;
509            return new StoredList(
510                view.subView(new Long(fromIndex), true,
511                             new Long(toIndex), false,
512                             new IndexKeyBinding(newBaseIndex)),
513                newBaseIndex);
514        } catch (KeyRangeException e) {
515            throw new IndexOutOfBoundsException(e.getMessage());
516        } catch (Exception e) {
517            throw StoredContainer.convertException(e);
518        }
519    }
520
521    /**
522     * Compares the specified object with this list for equality.
523     * A value comparison is performed by this method and the stored values
524     * are compared rather than calling the equals() method of each element.
525     * This method conforms to the {@link List#equals} interface.
526     *
527     * @throws RuntimeExceptionWrapper if a {@link DatabaseException} is
528     * thrown.
529     */
530    public boolean equals(Object other) {
531
532        if (!(other instanceof List)) return false;
533        List otherList = (List) other;
534        StoredIterator i1 = null;
535        ListIterator i2 = null;
536        try {
537            i1 = storedIterator();
538            i2 = storedOrExternalListIterator(otherList);
539            while (i1.hasNext()) {
540                if (!i2.hasNext()) return false;
541                if (i1.nextIndex() != i2.nextIndex()) return false;
542                Object o1 = i1.next();
543                Object o2 = i2.next();
544                if (o1 == null) {
545                    if (o2 != null) return false;
546                } else {
547                    if (!o1.equals(o2)) return false;
548                }
549            }
550            if (i2.hasNext()) return false;
551            return true;
552        } finally {
553	    if (i1 != null) {
554		i1.close();
555	    }
556            StoredIterator.close(i2);
557        }
558    }
559
560    /**
561     * Returns a StoredIterator if the given collection is a StoredCollection,
562     * else returns a regular/external ListIterator.  The iterator returned
563     * should be closed with the static method StoredIterator.close(Iterator).
564     */
565    final ListIterator storedOrExternalListIterator(List list) {
566
567        if (list instanceof StoredCollection) {
568            return ((StoredCollection) list).storedIterator();
569        } else {
570            return list.listIterator();
571        }
572    }
573
574    /*
575     * Add this in to keep FindBugs from whining at us about implementing
576     * equals(), but not hashCode().
577     */
578    public int hashCode() {
579	return super.hashCode();
580    }
581
582    Object makeIteratorData(BaseIterator iterator,
583                            DatabaseEntry keyEntry,
584                            DatabaseEntry priKeyEntry,
585                            DatabaseEntry valueEntry) {
586
587        return view.makeValue(priKeyEntry, valueEntry);
588    }
589
590    boolean hasValues() {
591
592        return true;
593    }
594
595    private static class IndexKeyBinding extends RecordNumberBinding {
596
597        private int baseIndex;
598
599        private IndexKeyBinding(int baseIndex) {
600
601            this.baseIndex = baseIndex;
602        }
603
604        public Object entryToObject(DatabaseEntry data) {
605
606            return new Long(entryToRecordNumber(data) - baseIndex);
607        }
608
609        public void objectToEntry(Object object, DatabaseEntry data) {
610
611            recordNumberToEntry(((Number) object).intValue() + baseIndex,
612                                data);
613        }
614    }
615}
616