/* * * * Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights * Reserved. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. * * Copyright 2000 Motorola, Inc. All Rights Reserved. * This notice does not imply publication. */ package javax.microedition.rms; import com.sun.midp.log.Logging; import com.sun.midp.log.LogChannels; /** * This class implements the RecordEnumeration interface. */ class RecordEnumerationImpl implements RecordEnumeration, RecordListener { /** The associated record store for this enumeration */ private RecordStore recordStore; /** The record filter this enumeration should use, or null if none */ private RecordFilter filter; /** The record comparator this enumeration should use, or null if none */ private RecordComparator comparator; /** True if this should listen to <code>recordStore</code> for changes */ private boolean keepEnumUpdated; // false by default /** Current pos within the enumeration */ private int index; // NO_SUCH_RECORD by default /** Array of recordId's of records included in the enumeration */ private int[] records; /** * A constant recordId indicating the splice point between the * last and first records in the enumeration. Returned by * <code>nextElement()</code> and <code>prevElement()</code> * when the next or prev element does not exist. */ private static final int NO_SUCH_RECORD = -1; /** * Builds an enumeration to traverse a set of records in the * given record store in an optionally specified order.<p> * * The filter, if non-null, will be used to determine what * subset of the record store records will be used.<p> * * The comparator, if non-null, will be used to determine the * order in which the records are returned.<p> * * If both the filter and comparator are null, the enumeration * will traverse all records in the record store in an undefined * order. This is the most efficient way to traverse all of the * records in a record store. * * @param inp_recordStore the RecordStore to enumerate. * @param inp_filter if non-null, will be used to determine what * subset of the record store records will be used. * @param inp_comparator if non-null, will be used to determine the * order in which the records are returned. * @param keepUpdated if true, the enumerator will keep its enumeration * current with any changes in the records of the record store. * Use with caution as there are performance consequences. * * @see #rebuild */ RecordEnumerationImpl(RecordStore inp_recordStore, RecordFilter inp_filter, RecordComparator inp_comparator, boolean keepUpdated) { recordStore = inp_recordStore; filter = inp_filter; comparator = inp_comparator; keepEnumUpdated = keepUpdated; if (keepUpdated) { inp_recordStore.addRecordListener(this); } rebuild(); } /** * Returns the number of records available in this enumeration's * set. That is, the number of records that have matched the * filter criterion. Note that this forces the RecordEnumeration * to fully build the enumeration by applying the filter to all * records, which may take a non-trivial amount * of time if there are a lot of records in the record store. * * @return the number of records available in this enumeration's * set. That is, the number of records that have matched * the filter criterion. */ public synchronized int numRecords() { checkDestroyed(); return records.length; } /** * Returns a copy of the <i>next</i> record in this enumeration, * where <i>next</i> is defined by the comparator and/or filter * supplied in the constructor of this enumerator. The byte array * returned is a copy of the record. Any changes made to this array * will NOT be reflected in the record store. After calling * this method, the enumeration is advanced to the next available * record. * * @exception InvalidRecordIDException no more records are available * * @return the next record in this enumeration. */ public synchronized byte[] nextRecord() throws InvalidRecordIDException, RecordStoreNotOpenException, RecordStoreException { checkDestroyed(); return recordStore.getRecord(nextRecordId()); } /** * Returns the recordId of the <i>next</i> record in this enumeration, * where <i>next</i> is defined by the comparator and/or filter * supplied in the constructor of this enumerator. After calling * this method, the enumeration is advanced to the next available * record. * * @exception InvalidRecordIDException no more records are available. * * @return the recordId of the next record in this enumeration. */ public synchronized int nextRecordId() throws InvalidRecordIDException { checkDestroyed(); if (index == records.length - 1) { throw new InvalidRecordIDException(); } if (index == NO_SUCH_RECORD) { index = 0; } else { index++; } return records[index]; } /** * Returns a copy of the <i>previous</i> record in this enumeration, * where <i>previous</i> is defined by the comparator and/or filter * supplied in the constructor of this enumerator. The byte array * returned is a copy of the record. Any changes made to this array * will NOT be reflected in the record store. After calling * this method, the enumeration is advanced to the next (previous) * available record. * * @exception InvalidRecordIDException no more records are available. * * @return the previous record in this enumeration. */ public synchronized byte[] previousRecord() throws InvalidRecordIDException, RecordStoreNotOpenException, RecordStoreException { checkDestroyed(); return recordStore.getRecord(previousRecordId()); } /** * Returns the recordId of the <i>previous</i> record in this enumeration, * where <i>previous</i> is defined by the comparator and/or filter * supplied in the constructor of this enumerator. After this method * is called, the enumeration is advanced to the next (previous) * available record. * * @exception InvalidRecordIDException when no more records are available. * * @return the recordId of the previous record in this enumeration. */ public synchronized int previousRecordId() throws InvalidRecordIDException { checkDestroyed(); if (index == 0 || records.length == 0) { throw new InvalidRecordIDException(); } if (index == NO_SUCH_RECORD) { index = records.length - 1; } else { index--; } return records[index]; } /** * Returns true if more elements exist in the <i>next</i> direction. * * @return true if more elements exist in the <i>next</i> direction. */ public boolean hasNextElement() { checkDestroyed(); if (records.length == 0 || !recordStore.isOpen()) { return false; } return (index != records.length - 1); } /** * Returns true if more elements exist in the <i>previous</i> direction. * * @return true if more elements exist in the <i>previous</i> direction. */ public boolean hasPreviousElement() { checkDestroyed(); if (records.length == 0 || !recordStore.isOpen()) { return false; // no records in the enumeration } return (index != 0); } /** * Returns the index point of the enumeration to the beginning. */ public void reset() { checkDestroyed(); index = NO_SUCH_RECORD; } /** * Request that the enumeration be updated to reflect the current * record set. Useful for when an application makes a number of * changes to the record store, and then wants an existing * RecordEnumeration to enumerate the new changes. * * @see #keepUpdated */ public void rebuild() { checkDestroyed(); int[] tmp = recordStore.getRecordIDs(); reFilterSort(tmp); } /** * Used to set whether the enumeration should be registered * as a listener of the record store, and rebuild its internal * index with every record addition/deletion in the record store. * Note that this should be used carefully due to the potential * performance cost associated with maintaining the * enumeration with every change. * * @param keepUpdated if true, the enumerator will keep its enumeration * current with any changes in the records of the record store. * Use with caution as there are possible performance consequences. * If false, the enumeration will not be kept current and may * return recordIds for records that have been deleted or miss * records that are added later. It may also return records out * of order that have been modified after the enumeration was * built. * * @see #rebuild */ public void keepUpdated(boolean keepUpdated) { checkDestroyed(); if (keepUpdated != keepEnumUpdated) { keepEnumUpdated = keepUpdated; if (keepUpdated) { recordStore.addRecordListener(this); rebuild(); } else { recordStore.removeRecordListener(this); } } } /** * Returns true if the enumeration keeps its enumeration * current with any changes in the records. * * @return true if the enumeration keeps its enumeration * current with any changes in the records */ public boolean isKeptUpdated() { checkDestroyed(); return keepEnumUpdated; } /** * From the RecordListener interface. This method is called if * a record is added to <code>recordStore</code>. * * @param inp_recordStore the record store to which a record was added * @param recordId the record ID of the new record */ public synchronized void recordAdded(RecordStore inp_recordStore, int recordId) { checkDestroyed(); filterAdd(recordId); } /** * From the RecordListener interface. This method is called if * a record in <code>recordStore</code> is modified. * * @param inp_recordStore the record store in which a record was modified * @param recordId the record ID of the modified record. */ public synchronized void recordChanged(RecordStore inp_recordStore, int recordId) { checkDestroyed(); int recIndex = findIndexOfRecord(recordId); if (recIndex >= 0) { removeRecordAtIndex(recIndex); } // else record not previously in the enumeration filterAdd(recordId); } /** * From the RecordListener interface. This method is called when a * record in <code>recordStore</code> is deleted. * * @param inp_recordStore the record store from which a record was deleted * @param recordId the record id of the deleted record */ public synchronized void recordDeleted(RecordStore inp_recordStore, int recordId) { checkDestroyed(); /* * Remove the deleted element from the records array. * No resorting is required. */ int recIndex = findIndexOfRecord(recordId); if (recIndex < 0) { return; // not in the enumeration } // remove this record from the enumeration removeRecordAtIndex(recIndex); } /** * Implements RecordEnumeration.destroy() interface. Called * to signal that this enumeration will no longer be used, and that * its resources may be collected. */ public synchronized void destroy() { checkDestroyed(); if (keepEnumUpdated) { recordStore.removeRecordListener(this); } filter = null; comparator = null; records = null; recordStore = null; // a signal that this is destroyed! } /** * Helper method that checks if this enumeration can be used. * If this enumeration has been destroyed, an exception is thrown. * * @exception IllegalStateException if RecordEnumeration has been * destroyed. */ private void checkDestroyed() { if (recordStore == null) { throw new IllegalStateException(); } } /** * Used to add a record to an already filtered and sorted * <code>records</code> array. More efficient than * <code>reFilterSort</code> because it relies on * <code>records</code> being in sorted order. * * First ensures that record <code>recordId</code> * meets this enumeration's filter criteria. * If it does it is added to records as array element * 0. If a comparator is defined for this enumeration, * the helper method <code>sortAdd</code> is called to * properly position <code>recordId</code> within the ordered * <code>records</code> array. * * Should be called from within a * synchronized (recordStore.rsLock) block. * * @param recordId the record to add to this enumeration */ private void filterAdd(int recordId) { int insertPoint = -1; if (filter != null) { try { if (!filter.matches(recordStore.getRecord(recordId))) { if (Logging.REPORT_LEVEL <= Logging.WARNING) { Logging.report(Logging.WARNING, LogChannels.LC_RMS, "Unexpected case in filterAdd: " + "recordId filtered out"); } return; // recordId filtered out } } catch (RecordStoreException rse) { return; // recordId does not exist } } // the new record has been accepted by the filter int[] newrecs = new int[records.length + 1]; newrecs[0] = recordId; // insert new record at front of list System.arraycopy(records, 0, newrecs, 1, records.length); records = newrecs; if (comparator != null) { // move the new record into place try { insertPoint = sortInsert(); } catch (RecordStoreException rse) { // NOTE: - should never be here // throw a RSE? destroy record enumeration? if (Logging.TRACE_ENABLED) { Logging.trace(rse, "Unexpected case in filterAdd: " + "caught RSE"); } } } // keep index up to date as well if (index != NO_SUCH_RECORD && insertPoint <= index) { index++; } } /** * Helper method called by <code>filterAdd</code>. * Moves the possibly unsorted element zero in the * <code>records</code> array to its sorted position * within the array. * * @return index of inserted element. * @exception RecordStoreException if an error occurs * in the comparator function. */ private int sortInsert() throws RecordStoreException { // bubble sort the first record in records into place int tmp; int i; int j; for (i = 0, j = 1; i < records.length - 1; i++, j++) { if (comparator.compare(recordStore.getRecord(records[i]), recordStore.getRecord(records[j])) == RecordComparator.FOLLOWS) { // if i follows j swap them in records tmp = records[i]; records[i] = records[j]; records[j] = tmp; } else { break; // done sorting if compare returns EQUALS or PRECEDES } } return i; // final index of new record in records } /** * Find the index in records of record <code>recordId</code> * and return it. * * @param recordId the record index to find * @return the index of the record, or -1. */ private int findIndexOfRecord(int recordId) { int idx; int recIndex = -1; for (idx = records.length - 1; idx >= 0; idx--) { if (records[idx] == recordId) { recIndex = idx; break; } } return recIndex; } /** * Internal helper method which * removes the array element at index <code>recIndex</code> * from the internal <code>records</code> array. * * <code>recIndex</code> should be non negative. * * @param recIndex the array element to remove. */ private void removeRecordAtIndex(int recIndex) { int[] tmp = new int[records.length - 1]; System.arraycopy(records, 0, tmp, 0, recIndex); System.arraycopy(records, recIndex + 1, tmp, recIndex, (records.length - recIndex) - 1); records = tmp; /* * If a record prior to current index was deleted * update index so nothing is skipped */ if (index != NO_SUCH_RECORD && recIndex <= index) { index --; } else if (index == records.length) { // last element in records removed index --; } } /** * Internal helper method for filtering and sorting records if * necessary. Called from rebuild(). * * Should be called from within a synchronized(recordStore.rsLock) block * * @param filtered array of record stores to filter and sort. */ private void reFilterSort(int[] filtered) { int filteredIndex = 0; if (filter == null) { /* * If this enumeration doesn't have any filters, the * recordId's returned by getRecordIDs should be * used as they are. */ records = filtered; } else { /* * If a filter has been specified, filter the recordStore * records to determine the subset to be used for this * enumeration. */ for (int i = 0; i < filtered.length; i++) { // if this record matches the filter keep it try { if (filter.matches(recordStore.getRecord(filtered[i]))) { // need revisit : if element overlap is allowed if (filteredIndex != i) { filtered[filteredIndex++] = filtered[i]; } else { filteredIndex++; } } } catch (RecordStoreException rse) { // if a record can't be found it doesn't match } } records = new int[filteredIndex]; System.arraycopy(filtered, 0, records, 0, filteredIndex); } /* * If a comparator has been specified, sort the remaining * records by comparing records against each other using * the comparator the application provides. */ if (comparator != null) { try { QuickSort(records, 0, records.length - 1, comparator); } catch (RecordStoreException rse) { // NOTE: - should never be here // throw a RSE? destroy record enumeration? if (Logging.TRACE_ENABLED) { Logging.trace(rse, "Unexpected case in reFilterSort:" + " caught RSE"); } } } reset(); // reset the current index of this enumeration } /** * Quicksort helper function for sorting the records. * * @param a the array of recordId's to sort using comparator. * @param lowIndex the low bound of the range to sort. * @param highIndex the hight bound of the range to sort. * @param inp_comparator the RecordComparator to use to compare records. */ private void QuickSort(int a[], int lowIndex, int highIndex, RecordComparator inp_comparator) throws RecordStoreException { /* * A different sorting algorithm may be preferred, because a * large recursive quicksort can consume lots of * stack. Quicksort is very fast for most random sequences * however... */ int left = lowIndex; // the "left" index int right = highIndex; // the "right" index /* * First partition the data into two regions, where every * element on the left side of the partition is less than * every element on the right side of the element. */ if (highIndex > lowIndex) { /* * Arbitrarily choose the initial pivot point to be the * middle of the array. */ int ind = (lowIndex + highIndex) / 2; int pivotIndex = a[ind]; byte[] pivotData = recordStore.getRecord(pivotIndex); // loop through the array until the indices cross while (left <= right) { /* * Starting on the left, scan right until the * first element greater than or equal to the * pivot element is found. */ while ((left < highIndex) && (inp_comparator.compare(recordStore.getRecord(a[left]), pivotData) == RecordComparator.PRECEDES)) { left++; } /* * Starting on the right, scan left until the * first element that is less than or equal to the * pivot element is found. */ while ((right > lowIndex) && (inp_comparator.compare(recordStore.getRecord(a[right]), pivotData) == RecordComparator.FOLLOWS)) { right--; } // if the indexes haven't crossed, swap the elements if (left <= right) { int tmp = a[left]; a[left] = a[right]; a[right] = tmp; left++; right--; } } // Sort the left side of the partition if (lowIndex < right) { QuickSort(a, lowIndex, right, inp_comparator); } // Sort the right side of the partition if (left < highIndex) { QuickSort(a, left, highIndex, inp_comparator); } } } }