FileDocCategorySizeDatePackage
RecordStore.javaAPI DocJ2ME MIDP 2.078052Thu Nov 07 12:02:28 GMT 2002javax.microedition.rms

RecordStore.java

/*
 * @(#)RecordStore.java	1.69 02/10/03 @(#)
 *
 * Portiona Copyright (c) 2000-2002 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Copyright 2000 Motorola, Inc. All Rights Reserved.
 * This notice does not imply publication.
 */

package javax.microedition.rms; 

import com.sun.midp.rms.RecordStoreFile;

/**
 * A class representing a record store. A record store consists of a
 * collection of records which will remain persistent across multiple
 * invocations of the MIDlet. The platform is responsible for
 * making its best effort to maintain the integrity of the
 * MIDlet's record stores throughout the normal use of the
 * platform, including reboots, battery changes, etc.
 *
 * <p>Record stores are created in platform-dependent locations, which
 * are not exposed to the MIDlets. The naming space for record stores
 * is controlled at the MIDlet suite granularity. MIDlets within a
 * MIDlet suite are allowed to create multiple record stores, as long
 * as they are each given different names. When a MIDlet suite is
 * removed from a platform all the record stores associated with its
 * MIDlets will also be removed. MIDlets within a MIDlet suite can
 * access each other's record stores directly. New APIs in MIDP 2.0
 * allow for the explicit sharing of record stores if the MIDlet
 * creating the RecordStore chooses to give such permission.</p>
 *
 * <p> Sharing is accomplished through the ability to name a
 * RecordStore created by another MIDlet suite.</p>
 *
 * <P> RecordStores are uniquely named using the unique name of the
 * MIDlet suite plus the name of the RecordStore. MIDlet suites are
 * identified by the MIDlet-Vendor and MIDlet-Name attributes from the
 * application descriptor.</p>
 *
 * <p> Access controls are defined when RecordStores to be shared are
 * created. Access controls are enforced when RecordStores are
 * opened. The access modes allow private use or shareable
 * with any other MIDlet suite.</p>
 *
 * <p>Record store names are case sensitive and may consist of any
 * combination of between one and 32 Unicode characters
 * inclusive. Record store names must be unique within the scope of a
 * given MIDlet suite. In other words, MIDlets within a MIDlet suite
 * are not allowed to create more than one record store with the same
 * name, however a MIDlet in one MIDlet suite is allowed to have a
 * record store with the same name as a MIDlet in another MIDlet
 * suite. In that case, the record stores are still distinct and
 * separate.</p>
 *
 * <p>No locking operations are provided in this API. Record store
 * implementations ensure that all individual record store operations
 * are atomic, synchronous, and serialized, so no corruption will
 * occur with multiple accesses. However, if a MIDlet uses multiple
 * threads to access a record store, it is the MIDlet's responsibility
 * to coordinate this access or unintended consequences may result.
 * Similarly, if a platform performs transparent synchronization of a
 * record store, it is the platform's responsibility to enforce
 * exclusive access to the record store between the MIDlet and
 * synchronization engine.</p>
 *
 * <p>Records are uniquely identified within a given record store by
 * their recordId, which is an integer value. This recordId is used as
 * the primary key for the records. The first record created in a
 * record store will have recordId equal to one (1). Each subsequent
 * record added to a RecordStore will be assigned a recordId one
 * greater than the record added before it. That is, if two records
 * are added to a record store, and the first has a recordId of 'n',
 * the next will have a recordId of 'n + 1'. MIDlets can create other
 * sequences of the records in the RecordStore by using the
 * <code>RecordEnumeration</code> class.</p>
 *
 * <p>This record store uses long integers for time/date stamps, in
 * the format used by System.currentTimeMillis(). The record store is
 * time stamped with the last time it was modified. The record store
 * also maintains a <em>version</em> number, which is an integer that
 * is incremented for each operation that modifies the contents of the
 * RecordStore.  These are useful for synchronization engines as well
 * as other things.</p>
 *
 * @since MIDP 1.0
 */

public class RecordStore 
{
    /*  
     * RecordStore Constructors 
     */

    /**
     * MIDlets must use <code>openRecordStore()</code> to get 
     * a <code>RecordStore</code> object. If this constructor 
     * is not declared (as private scope), Javadoc (and Java) 
     * will assume a public constructor. 
     */ 
    private RecordStore() {
    }

    /**
     * Deletes the named record store. MIDlet suites are only allowed
     * to delete their own record stores. If the named record store is
     * open (by a MIDlet in this suite or a MIDlet in a different
     * MIDlet suite) when this method is called, a
     * RecordStoreException will be thrown.  If the named record store
     * does not exist a RecordStoreNotFoundException will be
     * thrown. Calling this method does NOT result in recordDeleted
     * calls to any registered listeners of this RecordStore.
     *
     * @param recordStoreName the MIDlet suite unique record store to
     * 		delete
     *
     * @exception RecordStoreException if a record store-related
     * 		exception occurred
     * @exception RecordStoreNotFoundException if the record store
     * 		could not be found
     */
    public static void deleteRecordStore(String recordStoreName) 
        throws RecordStoreException, RecordStoreNotFoundException 
    {
	String uidPath = RecordStoreFile.getUniqueIdPath(recordStoreName);
        // Check the record store cache for a db with the same name
	synchronized (dbCacheLock) {
	    RecordStore db;
	    for (int n = 0; n < dbCache.size(); n++) {
		db = (RecordStore)dbCache.elementAt(n);
		if (db.uniqueIdPath.equals(uidPath)) {
		    // cannot delete an open record store
		    throw new RecordStoreException("deleteRecordStore error:"
						   + " record store is"
						   + " still open");
		}
	    }
	    // this record store is not currently open
	    if (RecordStoreFile.exists(uidPath)) {
		boolean success = RecordStoreFile.deleteFile(uidPath);
		if (!success) {
		    throw new RecordStoreException("deleteRecordStore " +
						   "failed");
		}
	    } else {
	        throw new RecordStoreNotFoundException("deleteRecordStore " +
						       "error: file " +
						       "not found"); 
	    }
	}
    }
    
    /**
     * Open (and possibly create) a record store associated with the
     * given MIDlet suite. If this method is called by a MIDlet when
     * the record store is already open by a MIDlet in the MIDlet suite,
     * this method returns a reference to the same RecordStore object.
     *
     * @param recordStoreName the MIDlet suite unique name for the
     *          record store, consisting of between one and 32 Unicode
     *          characters inclusive.
     * @param createIfNecessary if true, the record store will be
     *		created if necessary
     *
     * @return <code>RecordStore</code> object for the record store
     *
     * @exception RecordStoreException if a record store-related
     *		exception occurred
     * @exception RecordStoreNotFoundException if the record store
     *		could not be found
     * @exception RecordStoreFullException if the operation cannot be
     *		completed because the record store is full
     * @exception IllegalArgumentException if
     *          recordStoreName is invalid
     */
    public static RecordStore openRecordStore(String recordStoreName, 
					      boolean createIfNecessary) 
        throws RecordStoreException, RecordStoreFullException, 
        RecordStoreNotFoundException 
    {
	String uidPath = RecordStoreFile.getUniqueIdPath(recordStoreName);
	synchronized (dbCacheLock) {

	    if (recordStoreName.length() > 32 ||
		recordStoreName.length() == 0) {
		throw new IllegalArgumentException();
	    }

	    // Cache record store objects and ensure that there is only
	    // one record store object in memory for any given record
	    // store file. This is good for memory use. This is NOT safe
	    // in the situation where multiple VM's may be executing code
	    // concurrently. In that case, you have to sync things through
	    // file locking or something similar.
	    
	    // Check the record store cache for a db with the same name
	    RecordStore db;	    
	    for (int n = 0; n < dbCache.size(); n++) {
		db = (RecordStore)dbCache.elementAt(n);
		if (db.uniqueIdPath.equals(uidPath)) {
		    db.opencount++;  // times rs has been opened
		    return db;  // return ref to cached record store
		}
	    }
	    
	    /*
	     * Record store not found in cache, so create it.
	     */
	    db = new RecordStore(uidPath, recordStoreName, createIfNecessary);

	    /*
	     * Now add the new record store to the cache
	     */
	    db.opencount = 1;
	    dbCache.addElement(db);
	    return db;
	}
    }

    /**
     * Open (and possibly create) a record store that can be shared
     * with other MIDlet suites. The RecordStore is owned by the
     * current MIDlet suite. The authorization mode is set when the
     * record store is created, as follows:
     *
     * <ul>
     * <li><code>AUTHMODE_PRIVATE</code> - Only allows the MIDlet
     *          suite that created the RecordStore to access it. This
     *          case behaves identically to
     *          <code>openRecordStore(recordStoreName,
     *          createIfNecessary)</code>.</li>
     * <li><code>AUTHMODE_ANY</code> - Allows any MIDlet to access the
     *          RecordStore. Note that this makes your recordStore
     *          accessible by any other MIDlet on the device. This
     *          could have privacy and security issues depending on
     *          the data being shared. Please use carefully.</li>
     * </ul>
     *
     * <p>The owning MIDlet suite may always access the RecordStore and
     * always has access to write and update the store.</p>
     *
     * <p> If this method is called by a MIDlet when the record store
     * is already open by a MIDlet in the MIDlet suite, this method
     * returns a reference to the same RecordStore object.</p>
     *
     * @param recordStoreName the MIDlet suite unique name for the
     *          record store, consisting of between one and 32 Unicode
     *          characters inclusive.
     * @param createIfNecessary if true, the record store will be
     * 		created if necessary
     * @param authmode the mode under which to check or create access.
     * 		Must be one of AUTHMODE_PRIVATE or AUTHMODE_ANY.
     *		This argument is ignored if the RecordStore exists.
     * @param writable true if the RecordStore is to be writable by
     *		other MIDlet suites that are granted access.
     *		This argument is ignored if the RecordStore exists.
     *
     * @return <code>RecordStore</code> object for the record store
     *
     * @exception RecordStoreException if a record store-related
     * 		exception occurred
     * @exception RecordStoreNotFoundException if the record store
     * 		could not be found
     * @exception RecordStoreFullException if the operation
     *		cannot be completed because the record store is full
     * @exception IllegalArgumentException if authmode or
     *          recordStoreName is invalid
     * @since MIDP 2.0
     */
    public static RecordStore openRecordStore(String recordStoreName,
					      boolean createIfNecessary,
					      int authmode,
					      boolean writable)
	throws RecordStoreException, RecordStoreFullException,
               RecordStoreNotFoundException
    {
	RecordStore rs = RecordStore.openRecordStore(recordStoreName, 
						     createIfNecessary);
	rs.setMode(authmode, writable);
	return rs;
    }
    
    /**
     * Open a record store associated with the named MIDlet suite.
     * The MIDlet suite is identified by MIDlet vendor and MIDlet
     * name.  Access is granted only if the authorization mode of the
     * RecordStore allows access by the current MIDlet suite.  Access
     * is limited by the authorization mode set when the record store
     * was created:
     *
     * <ul>
     * <li><code>AUTHMODE_PRIVATE</code> - Succeeds only if vendorName
     * 		and suiteName identify the current MIDlet suite; this
     * 		case behaves identically to
     *		<code>openRecordStore(recordStoreName,
     *		createIfNecessary)</code>.</li>
     * <li><code>AUTHMODE_ANY</code> - Always succeeds.
     *          Note that this makes your recordStore
     *          accessible by any other MIDlet on the device. This
     *          could have privacy and security issues depending on
     *          the data being shared. Please use carefully.
     *		Untrusted MIDlet suites are allowed to share data but
     *		this is not recommended. The authenticity of the
     *		origin of untrusted MIDlet suites cannot be verified
     *		so shared data may be used unscrupulously.</li>
     * </ul>
     *
     * <p> If this method is called by a MIDlet when the record store
     * is already open by a MIDlet in the MIDlet suite, this method
     * returns a reference to the same RecordStore object.</p>
     *
     * <p> If a MIDlet calls this method to open a record store from
     * its own suite, the behavior is identical to calling:
     * <code>{@link #openRecordStore(String, boolean)
     * openRecordStore(recordStoreName, false)}</code></p>
     *
     * @param recordStoreName the MIDlet suite unique name for the
     *          record store, consisting of between one and 32 Unicode
     *          characters inclusive.
     * @param vendorName the vendor of the owning MIDlet suite
     * @param suiteName the name of the MIDlet suite
     *
     * @return <code>RecordStore</code> object for the record store
     *
     * @exception RecordStoreException if a record store-related
     *		exception occurred
     * @exception RecordStoreNotFoundException if the record store
     * 		could not be found
     * @exception SecurityException if this MIDlet Suite is not
     *  	allowed to open the specified RecordStore.
     * @exception IllegalArgumentException if recordStoreName is
     *          invalid
     * @since MIDP 2.0
     */
    public static RecordStore openRecordStore(String recordStoreName,
					      String vendorName,
					      String suiteName)
	throws RecordStoreException, RecordStoreNotFoundException
    {
	if (vendorName == null || suiteName == null) {
	    throw new IllegalArgumentException("vendorName and " +
					       "suiteName must be " +
					       "non null");
	}
	synchronized (dbCacheLock) {
	    
	    if (recordStoreName.length() > 32 ||
		recordStoreName.length() == 0) {
		throw new IllegalArgumentException();
	    }
	    
	    // Cache record store objects and ensure that there is only
	    // one record store object in memory for any given record
	    // store file. This is good for memory use. This is NOT safe
	    // in the situation where multiple VM's may be executing code
	    // concurrently. In that case, you have to sync things through
	    // file locking or something similar.
	    
	    // Check the record store cache for a db with the same name
	    RecordStore db;	   
	    String uidPath = RecordStoreFile.getUniqueIdPath(vendorName, 
							     suiteName, 
							     recordStoreName);
	    for (int n = 0; n < dbCache.size(); n++) {
		db = (RecordStore)dbCache.elementAt(n);
		if (db.uniqueIdPath.equals(uidPath)) {
		    if (db.checkOwner() == false && 
			db.dbAuthMode == AUTHMODE_PRIVATE) {
			throw new SecurityException();
		    } else {
			db.opencount++;  // times rs has been opened
			return db;  // return ref to cached record store
		    }
		}
	    }
	    /*
	     * Record store not found in cache, so create it.
	     */
	    db = new RecordStore(uidPath, recordStoreName, false);
	    
	    /*
	     * Now add the new record store to the cache
	     */
	    db.opencount = 1;
	    dbCache.addElement(db);
	    
	    if (db.checkOwner() == false && 
		db.dbAuthMode == AUTHMODE_PRIVATE) {
		db.closeRecordStore();
		throw new SecurityException();
	    } else {
		return db;
	    }
	}
    }

    /**
     * Authorization to allow access only to the current MIDlet
     * suite. AUTHMODE_PRIVATE has a value of 0.
     */
    public final static int AUTHMODE_PRIVATE = 0;

    /**
     * Authorization to allow access to any MIDlet
     * suites. AUTHMODE_ANY has a value of 1.
     */
    public final static int AUTHMODE_ANY = 1;

    /**
     * Internal indicator for AUTHMODE_ANY with read only access
     * AUTHMODE_ANY_RO has a value of 2.
     */
    private final static int AUTHMODE_ANY_RO = 2;

    /**
     * Changes the access mode for this RecordStore. The authorization
     * mode choices are:
     *
     * <ul>
     * <li><code>AUTHMODE_PRIVATE</code> - Only allows the MIDlet
     *          suite that created the RecordStore to access it. This
     *          case behaves identically to
     *          <code>openRecordStore(recordStoreName,
     *          createIfNecessary)</code>.</li>
     * <li><code>AUTHMODE_ANY</code> - Allows any MIDlet to access the
     *          RecordStore. Note that this makes your recordStore
     *          accessible by any other MIDlet on the device. This
     *          could have privacy and security issues depending on
     *          the data being shared. Please use carefully.</li>
     * </ul>
     *
     * <p>The owning MIDlet suite may always access the RecordStore and
     * always has access to write and update the store. Only the
     * owning MIDlet suite can change the mode of a RecordStore.</p>
     *
     * @param authmode the mode under which to check or create access.
     * 		Must be one of AUTHMODE_PRIVATE or AUTHMODE_ANY.
     * @param writable true if the RecordStore is to be writable by
     *		other MIDlet suites that are granted access
     *
     * @exception RecordStoreException if a record store-related
     *		exception occurred
     * @exception SecurityException if this MIDlet Suite is not
     *		allowed to change the mode of the RecordStore
     * @exception IllegalArgumentException if authmode is invalid
     * @since MIDP 2.0
     */
    public void setMode(int authmode,
			boolean writable)
    throws RecordStoreException
    {
	synchronized (rsLock) {
	    if (checkOwner() == false) {
		throw new SecurityException();
	    } else if (authmode != AUTHMODE_PRIVATE &&
		       authmode != AUTHMODE_ANY) {
		throw new IllegalArgumentException();
	    } else {
		dbAuthMode = authmode;
		if ((authmode == AUTHMODE_ANY) && (writable == false)) {
		    dbAuthMode = AUTHMODE_ANY_RO;
		}
	    }
	    // flush record store header here
	    storeDBState();
	}
    }

    /**
     * This method is called when the MIDlet requests to have the
     * record store closed. Note that the record store will not
     * actually be closed until closeRecordStore() is called as many
     * times as openRecordStore() was called. In other words, the
     * MIDlet needs to make a balanced number of close calls as open
     * calls before the record store is closed.
     *
     * <p>When the record store is closed, all listeners are removed
     * and all RecordEnumerations associated with it become invalid.
     * If the MIDlet attempts to perform
     * operations on the RecordStore object after it has been closed,
     * the methods will throw a RecordStoreNotOpenException.
     *
     * @exception RecordStoreNotOpenException if the record store is
     *		not open
     * @exception RecordStoreException if a different record
     *		store-related exception occurred
     */
    public void closeRecordStore()
	throws RecordStoreNotOpenException, RecordStoreException 
    {
	synchronized (rsLock) {
	    synchronized (dbCacheLock) {
		checkOpen();
		/*
		 * Find the record store within the record store cache.
		 * A linear seagrch is OK assuming there won't be many 
		 * concurrently open record stores.
		 */
		RecordStore db = null;
		for (int n = 0; n < dbCache.size(); n++) {
		    db = (RecordStore)dbCache.elementAt(n);
		    if (db == this) {
			db.opencount--;
			break;
		    }
		}
		if (db.opencount <= 0) {  // free stuff - final close
		    dbCache.removeElement(db);
		    try {
			// closing now...no need to listen
			if (!recordListener.isEmpty()) {
			    recordListener.removeAllElements();
			}			
			// close native fd 
			if (dbFirstFreeBlockOffset != 0) {
			    compactRecords();  // compact before close
			    // truncate file to compacted size
			    dbraf.truncate(dbDataEnd);
			} 
			dbraf.close();
		    } catch (java.io.IOException ioe) {
			throw new RecordStoreException("error closing .db " +
						       "file");
		    } finally {
			dbraf = null;
			recHeadCache = null;
		    }
		}
	    }
	}
    }

    /**
     * Returns an array of the names of record stores owned by the
     * MIDlet suite. Note that if the MIDlet suite does not
     * have any record stores, this function will return null.
     *
     * The order of RecordStore names returned is implementation
     * dependent.
     *
     * @return array of the names of record stores owned by the
     * MIDlet suite. Note that if the MIDlet suite does not
     * have any record stores, this function will return null.
     */
    public static String[] listRecordStores() 
    {
        // static calls synchronize on dbCacheLock
        synchronized (dbCacheLock) {
	  String[] returnstrings = RecordStoreFile.listRecordStores();
	  return returnstrings;
	}
    }

    /**
     * Returns the name of this RecordStore.
     *
     * @return the name of this RecordStore
     *
     * @exception RecordStoreNotOpenException if the record store is not open
     */
    public String getName() 
	throws RecordStoreNotOpenException 
    {
	checkOpen();
	return recordStoreName;
    }

    /**
     * Each time a record store is modified (by
     * <code>addRecord</code>, <code>setRecord</code>, or
     * <code>deleteRecord</code> methods) its <em>version</em> is
     * incremented. This can be used by MIDlets to quickly tell if
     * anything has been modified.
     *
     * The initial version number is implementation dependent.
     * The increment is a positive integer greater than 0.
     * The version number increases only when the RecordStore is updated.
     *
     * The increment value need not be constant and may vary with each
     * update.
     *
     * @return the current record store version
     *
     * @exception RecordStoreNotOpenException if the record store is
     *            not open
     */
    public int getVersion() 
	throws RecordStoreNotOpenException 
    {
	checkOpen();
	return dbVersion;
    }

    /**
     * Returns the number of records currently in the record store.
     *
     * @return the number of records currently in the record store
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     */
    public int getNumRecords() 
	throws RecordStoreNotOpenException 
    {
	checkOpen();
	return dbNumLiveRecords;
    }
    
    /**
     * Returns the amount of space, in bytes, that the record store
     * occupies. The size returned includes any overhead associated
     * with the implementation, such as the data structures
     * used to hold the state of the record store, etc.
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     *
     * @return the size of the record store in bytes
     */
    public int getSize() 
	throws RecordStoreNotOpenException 
    {
	checkOpen();
	// return the file size of the record store file
	return dbDataEnd;
    }

    /**
     * Returns the amount of additional room (in bytes) available for
     * this record store to grow. Note that this is not necessarily
     * the amount of extra MIDlet-level data which can be stored,
     * as implementations may store additional data structures with
     * each record to support integration with native applications,
     * synchronization, etc.
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     *
     * @return the amount of additional room (in bytes) available for
     *          this record store to grow
     */
    public int getSizeAvailable() 
	throws RecordStoreNotOpenException 
    {
	checkOpen();
	int rv = RecordStoreFile.spaceAvailable() - 
	    DB_BLOCK_SIZE - DB_RECORD_HEADER_LENGTH;
	return (rv < 0) ? 0 : rv;
    }
    
    /**
     * Returns the last time the record store was modified, in the
     * format used by System.currentTimeMillis().
     *
     * @return the last time the record store was modified, in the
     * 		format used by System.currentTimeMillis()
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     */
    public long getLastModified() 
	throws RecordStoreNotOpenException 
    {
	checkOpen();

	return dbLastModified;
    }

    /**
     * Adds the specified RecordListener. If the specified listener
     * is already registered, it will not be added a second time.
     * When a record store is closed, all listeners are removed.
     *
     * @param listener the RecordChangedListener
     * @see #removeRecordListener
     */
    public void addRecordListener(RecordListener listener) 
    {
	synchronized (rsLock) {
	    if (!recordListener.contains(listener)) {
		recordListener.addElement(listener);
	    }
	}
    }

    /**
     * Removes the specified RecordListener. If the specified listener
     * is not registered, this method does nothing.
     *
     * @param listener the RecordChangedListener
     * @see #addRecordListener
     */
    public void removeRecordListener(RecordListener listener) 
    {
	synchronized (rsLock) {
	    recordListener.removeElement(listener);
	}
    }
    
    /**
     * Returns the recordId of the next record to be added to the
     * record store. This can be useful for setting up pseudo-relational
     * relationships. That is, if you have two or more
     * record stores whose records need to refer to one another, you can
     * predetermine the recordIds of the records that will be created
     * in one record store, before populating the fields and allocating
     * the record in another record store. Note that the recordId returned
     * is only valid while the record store remains open and until a call
     * to <code>addRecord()</code>.
     *
     * @return the recordId of the next record to be added to the
     *          record store
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     * @exception RecordStoreException if a different record
     *		store-related exception occurred
     */
    public int getNextRecordID() 
	throws RecordStoreNotOpenException, RecordStoreException 
    {
	checkOpen();
	return dbNextRecordID;
    }

    /**
     * Adds a new record to the record store. The recordId for this
     * new record is returned. This is a blocking atomic operation.
     * The record is written to persistent storage before the
     * method returns.
     *
     * @param data the data to be stored in this record. If the record
     * 		is to have zero-length data (no data), this parameter may be
     * 		null.
     * @param offset the index into the data buffer of the first
     * 		relevant byte for this record
     * @param numBytes the number of bytes of the data buffer to use
     * 		for this record (may be zero)
     *
     * @return the recordId for the new record
     *
     * @exception RecordStoreNotOpenException if the record store is
     * 		not open
     * @exception RecordStoreException if a different record
     * 		store-related exception occurred
     * @exception RecordStoreFullException if the operation cannot be
     * 		completed because the record store has no more room
     * @exception SecurityException if the MIDlet has read-only access
     * 		to the RecordStore
     */
    public int addRecord(byte[] data, int offset, int numBytes) 
        throws RecordStoreNotOpenException, RecordStoreException, 
	RecordStoreFullException
    {
	synchronized (rsLock) {
	    checkOpen();
	    if (!checkWritable()) {
		throw new SecurityException();
	    }
	    if ((data == null) && (numBytes > 0)) {
		throw new NullPointerException("illegal arguments: null " +
					       "data,  numBytes > 0");
	    }
	    // get recordId for new record, update db's dbNextRecordID
	    int id = dbNextRecordID++;

	    /*
	     * Find the offset where this record should be stored and
	     * seek to that location in the file. allocateNewRecordStorage()
	     * allocates the space for this record.
	     */	    
	    RecordHeader rh = allocateNewRecordStorage(id, numBytes);
	    try {
		if (data != null) {
		    rh.write(data, offset);
		}
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error writing new record " 
					       + "data");
	    }
	    
	    // Update the state changes to the db file.
	    dbNumLiveRecords++;
	    dbVersion++;
	    storeDBState();
	    
	    // tell listeners a record has been added
	    notifyRecordAddedListeners(id);
	    
	    // Return the new record id
	    return id;
	}
    }
    
    /**
     * The record is deleted from the record store. The recordId for
     * this record is NOT reused.
     *
     * @param recordId the ID of the record to delete
     *
     * @exception RecordStoreNotOpenException if the record store is
     * 		not open
     * @exception InvalidRecordIDException if the recordId is invalid
     * @exception RecordStoreException if a general record store
     * 		exception occurs
     * @exception SecurityException if the MIDlet has read-only access
     * 		to the RecordStore
     */
    public void deleteRecord(int recordId) 
	throws RecordStoreNotOpenException, InvalidRecordIDException, 
	    RecordStoreException 
    {
	synchronized (rsLock) {
	    checkOpen();
	    if (!checkWritable()) {
		throw new SecurityException();
	    }
	    RecordHeader rh = null; // record header	    
	    try {
		rh = findRecord(recordId, false);
		freeRecord(rh); // calls rh.store		
		recHeadCache.invalidate(rh.id);
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error updating file after" + 
					       " record deletion");
	    }	    
	    // update database header info and sync to file
	    dbNumLiveRecords--;
	    dbVersion++;
	    storeDBState();
	    // tell listeners a record has been deleted
	    notifyRecordDeletedListeners(recordId);
	}
    }

    /**
     * Returns the size (in bytes) of the MIDlet data available
     * in the given record.
     *
     * @param recordId the ID of the record to use in this operation
     *
     * @return the size (in bytes) of the MIDlet data available
     *          in the given record
     *
     * @exception RecordStoreNotOpenException if the record store is
     * 		not open
     * @exception InvalidRecordIDException if the recordId is invalid
     * @exception RecordStoreException if a general record store
     * 		exception occurs
     */
    public int getRecordSize(int recordId) 
	throws RecordStoreNotOpenException, InvalidRecordIDException, 
	    RecordStoreException 
    {
	synchronized (rsLock) {
	    checkOpen();
	    try {
		// throws InvalidRecordIDException
		RecordHeader rh = findRecord(recordId, true);
		return (rh.dataLenOrNextFree);
	    } catch (java.io.IOException ioe) {
	        throw new RecordStoreException("error reading record data");
	    }
	}
    }
    
    /**
     * Returns the data stored in the given record.
     *
     * @param recordId the ID of the record to use in this operation
     * @param buffer the byte array in which to copy the data
     * @param offset the index into the buffer in which to start copying
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     * @exception InvalidRecordIDException if the recordId is invalid
     * @exception RecordStoreException if a general record store
     *          exception occurs
     * @exception ArrayIndexOutOfBoundsException if the record is
     *          larger than the buffer supplied
     *
     * @return the number of bytes copied into the buffer, starting at
     *          index <code>offset</code>
     * @see #setRecord
     */
    public int getRecord(int recordId, byte[] buffer, int offset) 
	throws RecordStoreNotOpenException, InvalidRecordIDException, 
	RecordStoreException
    {
	synchronized (rsLock) {
	    checkOpen();
	    
	    RecordHeader rh;
	    try {
		// throws InvalidRecordIDException
		rh = findRecord(recordId, true);
		rh.read(buffer, offset);
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error reading record data");
	    }
	    return rh.dataLenOrNextFree;
	}
    }
    
    /**
     * Returns a copy of the data stored in the given record.
     *
     * @param recordId the ID of the record to use in this operation
     *
     * @exception RecordStoreNotOpenException if the record store is
     *		not open
     * @exception InvalidRecordIDException if the recordId is invalid
     * @exception RecordStoreException if a general record store
     *		exception occurs
     *
     * @return the data stored in the given record. Note that if the
     * 		record has no data, this method will return null.
     * @see #setRecord
     */
    public byte[] getRecord(int recordId) 
	throws RecordStoreNotOpenException, InvalidRecordIDException, 
	    RecordStoreException 
    {
	synchronized (rsLock) {
	    checkOpen();

	    int size = 0;
	    byte[] data = null;
	    try {
		// throws InvalidRecordIDException
		RecordHeader rh = findRecord(recordId, true); 
		if (rh.dataLenOrNextFree == 0) {
		    return null;
		}
		data = new byte[rh.dataLenOrNextFree];
		rh.read(data, 0);
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error reading record data");
	    } 
	    return data;
	}
    }

    /**
     * Sets the data in the given record to that passed in. After
     * this method returns, a call to <code>getRecord(int recordId)</code>
     * will return an array of numBytes size containing the data
     * supplied here.
     *
     * @param recordId the ID of the record to use in this operation
     * @param newData the new data to store in the record
     * @param offset the index into the data buffer of the first
     * 		relevant byte for this record
     * @param numBytes the number of bytes of the data buffer to use
     * 		for this record
     *
     * @exception RecordStoreNotOpenException if the record store is
     * 		not open
     * @exception InvalidRecordIDException if the recordId is invalid
     * @exception RecordStoreException if a general record store
     * 		exception occurs
     * @exception RecordStoreFullException if the operation cannot be
     * 		completed because the record store has no more room
     * @exception SecurityException if the MIDlet has read-only access
     * 		to the RecordStore
     * @see #getRecord
     */
    public void setRecord(int recordId, byte[] newData, 
			  int offset, int numBytes) 
	throws RecordStoreNotOpenException, InvalidRecordIDException, 
	    RecordStoreException, RecordStoreFullException 
    {
	synchronized (rsLock) {
	    checkOpen();
	    if (!checkWritable()) {
		throw new SecurityException();
	    }
	    
	    if ((newData == null) && (numBytes > 0)) {
		throw new NullPointerException();
	    }
	    
	    RecordHeader rh = null;
	    RecordHeader newrh = null;
	    
	    try {
		rh = findRecord(recordId, false); // throws InvalidRIDException
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error finding record data");
	    }
	    /*
	     * The size of the data and space allocated to the
	     * current record is known here, as is the new size.
	     * Determine if the new data will fit, or if this 
	     * record will have to be stored elsewhere.
	     */
	    if (numBytes <= rh.blockSize - DB_RECORD_HEADER_LENGTH) {
		/*
		 * The new data should fit within the existing record
		 * location in the file. Store the new data and
		 * patch up the record's header fields.
		 */
		int allocSize = getAllocSize(numBytes);
		if (rh.blockSize - allocSize >= 
		    DB_BLOCK_SIZE + DB_RECORD_HEADER_LENGTH) {
		    splitRecord(rh, allocSize); // sets rh.blockSize
	        }
		rh.dataLenOrNextFree = numBytes;		
		try {
		    rh.store(); // write the new record header
		    recHeadCache.insert(rh);  // add to cache
		
		    if (newData != null) {
			rh.write(newData, offset);
		    }
		} catch (java.io.IOException ioe) {
		    throw new RecordStoreException("error writing record" +
						   " data");
		}
	    } else {
		/*
		 * The new data is longer than the old data.  It needs to
		 * be relocated to elsewhere within <code>dbfile</code>. 
		 * Search the free list to see if there's a space where to
		 * store it.  Otherwise append it to the end of the file.
		 */

		freeRecord(rh); // calls rh.store()

		newrh = allocateNewRecordStorage(recordId, numBytes);

		try {
		    if (newData != null) {
			newrh.write(newData, offset);
			// NOTE: I/O exception leaves space allocated & unused
		    }
		} catch (java.io.IOException ioe) {
		    throw new RecordStoreException("error moving record " +
						   "data");
		}
	    }
	    
	    // update database header info and sync to file
	    dbVersion++;
	    storeDBState();
	    notifyRecordChangedListeners(recordId);
	}
    }

    /**
     * Returns an enumeration for traversing a set of records in the
     * 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 is 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.  If a filter is used with a null
     * comparator, the enumeration will traverse the filtered records
     * in an undefined order.
     *
     * The first call to <code>RecordEnumeration.nextRecord()</code>
     * returns the record data from the first record in the sequence.
     * Subsequent calls to <code>RecordEnumeration.nextRecord()</code>
     * return the next consecutive record's data. To return the record
     * data from the previous consecutive from any
     * given point in the enumeration, call <code>previousRecord()</code>.
     * On the other hand, if after creation the first call is to
     * <code>previousRecord()</code>, the record data of the last element
     * of the enumeration will be returned. Each subsequent call to
     * <code>previousRecord()</code> will step backwards through the
     * sequence.
     *
     * @param filter if non-null, will be used to determine what
     *          subset of the record store records will be used
     * @param 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 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. Note that any changes to records in the
     *          record store are accurately reflected when the record
     *          is later retrieved, either directly or through the
     *          enumeration. The thing that is risked by setting this
     *          parameter false is the filtering and sorting order of
     *          the enumeration when records are modified, added, or
     *          deleted.
     *
     * @exception RecordStoreNotOpenException if the record store is
     *          not open
     *
     * @see RecordEnumeration#rebuild
     *
     * @return an enumeration for traversing a set of records in the
     *          record store in an optionally specified order
     */
    public RecordEnumeration enumerateRecords(RecordFilter filter, 
					      RecordComparator comparator, 
					      boolean keepUpdated) 
	throws RecordStoreNotOpenException 
    {
	checkOpen();
	return new RecordEnumerationImpl(this, filter, 
					 comparator, keepUpdated);
    }

    /*
     * Private Memory Management Methods
     */

    /**
     * Find the record header for a record <code>recordId</code>.
     *
     * @param recordId the id of the desired record header.
     * @param addToCache true if this record should be added to cache 
     *        if found.
     *
     * @return the record header for the given record, or null if 
     *         the record cannot be found.
     */
    private RecordHeader findRecord(int recordId, boolean addToCache)
	throws InvalidRecordIDException, java.io.IOException
    {
	RecordHeader rh;
	int offset;
	
	int cur_offset = dbFirstRecordOffset;
	// if no records exist, throw an exception
	if (cur_offset == 0) {
	    throw new InvalidRecordIDException();
	}
	
	// look for the record in the cache
	rh = recHeadCache.get(recordId);
	if (rh != null) {
	    return rh;
	}
	
	/*
	 * requested record header is NOT in cache...
	 * search through the linked list of records
	 * in the file. 
	 */
	rh = new RecordHeader();	
	while (cur_offset != 0) {
	    rh.load(cur_offset);
	    if (rh.id == recordId) {
		break;
	    } else {
		cur_offset = rh.nextOffset;
	    }
	} 
	
	if (cur_offset == 0) { 
	    // hit the end of the linked list w/o finding record.
	    throw new InvalidRecordIDException();
	}	
	if (addToCache) {
	    recHeadCache.insert(rh);
	}
	return rh;
    }
    

    /**
     * Return the block allocation size for a record with <code>numBytes
     * </code> data bytes.  This includes space for the record header
     * and is a multiple of <code>DB_BLOCK_SIZE</code>.
     *
     * @param numBytes number of data bytes that will be stored in record
     *
     * @return the amount of space to allocate for this record.
     */
    private int getAllocSize(int numBytes) {
	int rv;
	int pad;
	rv = DB_RECORD_HEADER_LENGTH + numBytes;
	pad = DB_BLOCK_SIZE - (rv % DB_BLOCK_SIZE);
	if (pad != DB_BLOCK_SIZE) {
	    rv += pad;
	}
	return rv;
    }



    /**
     * Returns a new record header for record <code>id</code> large
     * enough to hold <code>dataSize</code> bytes of record data.
     *
     * Picks a free block using a first fit strategy that is 
     * large enough for a record header and the associated
     * record data.  The block will be a multiple of DB_BLOCK_SIZE.
     *
     * @param id the record id to assign to the returned record header.
     * @param dataSize length of record data that will be 
     *        stored with this record.
     *
     * @return a new record header 
     *         the record store's backing file.
     */

    private RecordHeader allocateNewRecordStorage(int id, int dataSize)
	throws RecordStoreException, RecordStoreFullException
    {
	int allocSize = getAllocSize(dataSize);
	boolean foundBlock = false;

	/*
	 * Traverse the free block linked list in the file, looking
	 * for the first fit
	 */
	RecordHeader block = new RecordHeader();
	try {
	    int offset = dbFirstFreeBlockOffset;
	    while (offset != 0) {
		block.load(offset);
		// If block is big enough, use it.
		if (block.blockSize >= allocSize) {
		    foundBlock = true;
		    break; // use this free block
		}
		offset = block.dataLenOrNextFree; // next free block
	    }
	} catch (java.io.IOException ioe) {
	    throw new RecordStoreException("error finding first fit block");
	}
	
	if (foundBlock == false) {
	    
	    /*
	     * No free block was found that would hold this record, so
	     * find the last (biggest offset) record in the file and
	     * append this record after it.
	     */
	    
	    // Is there room to grow the file?
	    if (RecordStoreFile.spaceAvailable() < allocSize) {
		throw new RecordStoreFullException();
	    }
	    
	    block = new RecordHeader(dbDataEnd, id, 
				     dbFirstRecordOffset,
				     allocSize, dataSize);
	    try {
		block.store();
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error writing "+
					       "new record data"); 
	    }
	    dbFirstRecordOffset = dbDataEnd;
	    dbDataEnd += allocSize;
	} else { 
	    // block is where the new record should be stored
	    if (block.id != -1) {
		throw new RecordStoreException("ALLOC ERR " + block.id +
					       " is not a free block!");
	    }
	
	    removeFreeBlock(block);  // remove from free block list	    
	    
	    block.id = id;
	    if (block.blockSize - allocSize >=
		DB_BLOCK_SIZE + DB_RECORD_HEADER_LENGTH) {
		splitRecord(block, allocSize); // sets block.blockSize
	    }
	    block.dataLenOrNextFree = dataSize;
	    try {
		block.store(); 
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("error writing free block "+
					       "after alloc"); 
	    }
	}
	// add new record to cache
	recHeadCache.insert(block);
	return block;
    }


    /**
     * Splits a free block off the tail end of a large
     * record block which contains extra free space.
     * After calling this method, the caller must call 
     * <code>storeDBState</code>.
     *
     * On return, <code>recHead.blockSize</code> will contain
     * allocSize.
     *
     * @param recHead the current record header
     * @param allocSize the size that <code>recHEad</code>
     *        will have as its <code>blockSize</code> variable
     *        when the call returns.
     *
     * @exception RecordStoreException if there is an error
     *            splitting the record
     */
    private void splitRecord(RecordHeader recHead, int allocSize)
	throws RecordStoreException
    {
	RecordHeader newfb;	
	int extraSpace = recHead.blockSize - allocSize;
	int oldBlockSize = recHead.blockSize;
	recHead.blockSize = allocSize;
	
	// only split records inside the linked list
	if (recHead.offset != dbFirstRecordOffset) {
	    int fboffset = recHead.offset + allocSize;
	    newfb = new RecordHeader(fboffset, -1, recHead.offset,
				     extraSpace, 0);
	    try {
		freeRecord(newfb); // write new free block to disk
		RecordHeader prh = new RecordHeader(recHead.offset +
						    oldBlockSize);
		prh.nextOffset = fboffset;
		prh.store();
		recHeadCache.invalidate(prh.id);
		storeDBState();
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("splitRecord error");
	    }
	} else { 	    
	    // drop free space at the end of the file
	    dbDataEnd = recHead.offset + recHead.blockSize;
	}
    }
	  
    
    /**
     * Free a record into the Free list.  
     * Turns the RecordHeader block <code>rh</code> into a free 
     * block, then adds it to the free block linked list.  
     *
     * After calling this method the caller must call 
     * <code>storeDBState</code>.
     *
     * @param rh RecordHeader of record to make into a free block
     *
     * @exception RecordStoreException if there is an IO error updating the 
     *            free list
     */
    private void freeRecord(RecordHeader rh) 
	throws RecordStoreException
    {
	if (rh.offset == dbFirstRecordOffset) {
	    // don't put free blocks at the end of the record file
	    dbFirstRecordOffset = rh.nextOffset;
	    dbDataEnd = rh.offset;
	} else {
	    rh.id = -1;  // indicate this is a free block
	    rh.dataLenOrNextFree = dbFirstFreeBlockOffset;
	    // insert this new free block at front of free list
	    dbFirstFreeBlockOffset = rh.offset;
	    try {
		rh.store();
	    } catch (java.io.IOException ioe) {
		throw new RecordStoreException("free record failed");
	    }
	}
    }

    /**
     * Remove a free block from the free block linked list
     *
     * @param blockToFree record header for the free block to remove
     * 
     * @exception recordStoreException if error occurs during the
     *            update.
     */
    private void removeFreeBlock(RecordHeader blockToFree) 
	throws RecordStoreException 
    {
	RecordHeader block = new RecordHeader();
	RecordHeader prev = new RecordHeader();
	RecordHeader tmp = null;
	try {
	    int offset = dbFirstFreeBlockOffset;
	    while (offset != 0) {
		block.load(offset);
		if (block.offset == blockToFree.offset) {
		    if (block.id != -1) {
			throw new RecordStoreException("removeFreeBlock id" +
						       " is not -1");
		    }
		    if (prev.offset == 0) {
			// Set next free block as new freelist head
			dbFirstFreeBlockOffset = block.dataLenOrNextFree;
		    } else {
			/*
			 * Update previous block's pointer to the
			 * block this block was pointing to
			 */
			prev.dataLenOrNextFree = block.dataLenOrNextFree;
			prev.store();
		    }
		}
		offset = block.dataLenOrNextFree;
		// avoid creating lots of garbage!
		tmp = prev;
		prev = block;
		block = tmp;
	    }
	} catch (java.io.IOException ioe) {
	    throw new RecordStoreException("removeFreeBlock block not found");
	}	
    }

    /**
     * Helper method that stores the internal state variables
     * into the record store file.
     *
     * checkopen should have been called.  will not work
     * if dbraf is not open
     *
     * Updates dbLastModified time to current system time
     */
    private void storeDBState() throws RecordStoreException
    {
	try {
	    // set modification time
	    dbLastModified = System.currentTimeMillis();
	    // Capture the db state into the byte array
	    RecordStore.putInt(dbNumLiveRecords, dbState, RS_NUM_LIVE);
	    RecordStore.putInt(dbAuthMode, dbState, RS_AUTHMODE); 
	    RecordStore.putInt(dbVersion, dbState, RS_VERSION);
	    RecordStore.putInt(dbNextRecordID, dbState, RS_NEXT_ID);
	    RecordStore.putInt(dbFirstRecordOffset, dbState, RS_REC_START);
	    RecordStore.putInt(dbFirstFreeBlockOffset, dbState, RS_FREE_START);
	    RecordStore.putLong(dbLastModified, dbState, RS_LAST_MODIFIED);
	    RecordStore.putInt(dbDataStart, dbState, RS_DATA_START);
	    RecordStore.putInt(dbDataEnd, dbState, RS_DATA_END);
	    // Write the state to the db file
	    dbraf.seek(SIGNATURE_LENGTH); // skip RS header 8 bytes
	    int numbytes = DB_INIT.length - SIGNATURE_LENGTH;
	    dbraf.write(dbState, SIGNATURE_LENGTH, numbytes);
	} catch (java.io.IOException ioe) {
	    throw new RecordStoreException("error writing record store " +
					   "attributes");
	}
    }

    /*
     * Package Private Methods
     */

    /**
     * Get the open status of this record store.  (Package accessable
     * for use by record enumeration objects.)
     *
     * @return true if record store is open, false otherwise. 
     */
    boolean isOpen() {
	if (dbraf == null) {
	    return false;
	}
	return true;
    }

    /*
     * Private Utility Methods
     */

    /**
     * Throws a RecordStoreNotOpenException if the RecordStore
     * is closed.  (A RecordStore is closed if the RecordStoreFile
     * instance variable <code>dbraf</code> is null. 
     *
     * @exception RecordStoreNotOpenException if RecordStore is closed
     */
    private void checkOpen() throws RecordStoreNotOpenException
    {
	if (dbraf == null) {
	    throw new RecordStoreNotOpenException();
	}
    }

    /**
     * Notifies all registered listeners that a record changed.
     *
     * @param recordId the record id of the changed record.
     */
    private void notifyRecordChangedListeners(int recordId)
    {
	for (int i = 0; i < recordListener.size(); i++) {
	    RecordListener rl = (RecordListener)recordListener.elementAt(i);
	    rl.recordChanged(this, recordId);
	}
    }

    /**
     * Notifies all registered listeners that a record was added.
     *
     * @param recordId the record id of the added record.
     */
    private void notifyRecordAddedListeners(int recordId)
    {
	for (int i = 0; i < recordListener.size(); i++) {
	    RecordListener rl = (RecordListener)recordListener.elementAt(i);
	    rl.recordAdded(this, recordId);
	}
    }
    
    /**
     * Notifies all registered listeners that a record was deleted.
     *
     * @param recordId the record id of the changed record.
     */
    private void notifyRecordDeletedListeners(int recordId)
    {
	for (int i = 0; i < recordListener.size(); i++) {
	    RecordListener rl = (RecordListener)recordListener.elementAt(i);
	    rl.recordDeleted(this, recordId);
	}
    }


    /**
     * A convenience method for converting a byte array into
     * an int (assumes big-endian byte ordering).
     *
     * @param data the byte array returned from the database.
     * @param offset the offset into the array of the first byte to start from.
     *
     * @return an int corresponding to the first four bytes 
     *         of the array passed in.
     */
    static int getInt(byte[] data, int offset)
    {
	int r = data[offset++];
	r = (r << 8) | ((int)(data[offset++]) & 0xff);
	r = (r << 8) | ((int)(data[offset++]) & 0xff);
	r = (r << 8) | ((int)(data[offset++]) & 0xff);
	return r;
    }

    /**
     * A convenience method for converting a byte array into
     * a long (assumes big-endian byte ordering).
     *
     * @param data the byte array returned from the database.
     * @param offset the offset into the array of the first byte to start from.
     * @return a long corresponding to the first eight bytes 
     *         of the array passed in.
     */
    static long getLong(byte[] data, int offset)
    {
	long r = data[offset++];
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	r = (r << 8) | ((long)(data[offset++]) & 0xff);
	return r;
    }


    /**
     * A convenience method for converting an integer into
     * a byte array.
     *
     * @param i the integer to turn into a byte array.
     * @param data a place to store the bytes of <code>i</code>.
     * @param offset starting point within <code>data<code> to 
     *        store <code>i</code>.
     *
     * @return the number of bytes written to the array.
     */
    static int putInt(int i, byte[] data, int offset)
    {
	data[offset++] = (byte)((i >> 24) & 0xff);
	data[offset++] = (byte)((i >> 16) & 0xff);
	data[offset++] = (byte)((i >> 8) & 0xff);
	data[offset] = (byte)(i & 0xff);
	return 4;
    }


    /**
     * A convenience method for converting a long into
     * a byte array.
     *
     * @param l the <code>long<code> to turn into a byte array.
     * @param data a place to store the bytes of <code>l</code>.
     * @param offset Starting point within <code>data</code> to 
     *        store <code>l</code>.
     *
     * @return the number of bytes written to the array.
     */
    static int putLong(long l, byte[] data, int offset)
    {
	data[offset++] = (byte)((l >> 56) & 0xff);
	data[offset++] = (byte)((l >> 48) & 0xff);
	data[offset++] = (byte)((l >> 40) & 0xff);
	data[offset++] = (byte)((l >> 32) & 0xff);
	data[offset++] = (byte)((l >> 24) & 0xff);
	data[offset++] = (byte)((l >> 16) & 0xff);
	data[offset++] = (byte)((l >> 8) & 0xff);
	data[offset] = (byte)(l & 0xff);
	return 8;
    }

    /**
     * Returns all of the recordId's currently in the record store.
     *
     * MUST be called after obtaining rsLock, e.g in a 
     * <code>synchronized (rsLock) {</code> block.
     *
     * @return an array of the recordId's currently in the record store
     *         or null if the record store is closed.
     */
    int[] getRecordIDs() 
    {
	if (dbraf == null) { // lower overhead than checkOpen()
 	    return null;
	}

	int index = 0;
	int[] tmp = new int[dbNumLiveRecords];
	int offset = dbFirstRecordOffset; // start at beginning of file
	RecordHeader rh = new RecordHeader(); 

	try {
	    while (offset != 0) {
		rh.load(offset);
		if (rh.id > 0) {
		    tmp[index++] = rh.id;
		}
		offset = rh.nextOffset;
	    }
	} catch (java.io.IOException ioe) {
	    return null;
	}
	return tmp;
    }

    /**
     * Remove free blocks from the record store and compact records
     * with data into as small a space in <code>rsFile</code> as
     * possible.  Operates from smallest to greatest offset in 
     * <code>rsFile</code>, copying data in chunks towards the 
     * beginning of the file, and updating record store meta-data
     * as it progresses.
     *
     * Warning: This is a slow operation that scales linearly
     * with rsFile size.  
     *
     * @exception RecordStoreNotOpenException if this record store 
     *            is closed
     * @exception RecordStoreException if an error occurs during record 
     *            store compaction
     */

    private void compactRecords() throws RecordStoreNotOpenException, 
	RecordStoreException {
	int offset = dbDataStart;  // after record store header structure
	int target = 0;
	int bytesLeft;
	int numToMove;
	byte[] chunkBuffer = new byte[DB_COMPACTBUFFER_SIZE];
	
	RecordHeader rh = new RecordHeader();
	
	int prevRec = 0;
	while (offset < dbDataEnd) {
	    try {
		rh.load(offset);
	    } catch (java.io.IOException ioe) {
		// NOTE - should throw some exception here
		System.out.println("Unexpected IOException in CompactRS!");
	    }
	    
	    if (rh.id == -1) {          // a free block
		if (target == 0) {
		    target = offset; 
		} // else skip free block
		offset += rh.blockSize;
	    } else {                    // a record block
		if (target == 0) {
		    // No move needed so far.
		    prevRec = offset;
		    offset += rh.blockSize;
		} else {
		    int old_offset = target;
		    // Move a record back in the file
		    rh.offset = target;
		    rh.nextOffset = prevRec;
		    try {
			rh.store();
			offset += DB_RECORD_HEADER_LENGTH;
			target += DB_RECORD_HEADER_LENGTH;
			bytesLeft = (rh.blockSize - DB_RECORD_HEADER_LENGTH);
			while (bytesLeft > 0) {
			    if (bytesLeft < DB_COMPACTBUFFER_SIZE) {
				numToMove = bytesLeft;
			    } else {
				numToMove = DB_COMPACTBUFFER_SIZE;
			    }
			    dbraf.seek(offset);
			    dbraf.read(chunkBuffer, 0, numToMove);
			    dbraf.seek(target);
			    dbraf.write(chunkBuffer, 0, numToMove);
			    offset += numToMove;
			    target += numToMove;
			    bytesLeft -= numToMove;
			} 
		    } catch (java.io.IOException ioe) {
			// NOTE - should throw some exception here
			System.out.println("Unexpected IOException " +
					   "in CompactRS!");
		    }
		    prevRec = old_offset;
		}
	    }
	}
	if (rh.offset != 0) {
	    dbDataEnd = rh.offset + rh.blockSize;
	}
	dbFirstRecordOffset = rh.offset;
	dbFirstFreeBlockOffset = 0;
	storeDBState();
    }

    /*
     * Internal Classes  
     */

    /**
     * A class representing a RecordHeader, which may be the start of a 
     * record block or a free block.  (In a free block, only the 
     * <code> NextOffset </code> and <code> BlockSize </code> fields
     * are valid.)  Currently it is a conveinience structure, with
     * fields visible and directly modifyable by the enclosing
     * RecordStore class.
     */

    private class RecordHeader {
	/*
	 * Each record is laid out in the file system as follows:
	 * Bytes - Usage
	 * 00-03 - Record ID
	 * 04-07 - Next record offset
	 * 08-11 - Record size...total (in bytes, big endian).
	 * 12-15 - Length of Data.  This is stored separately for the case 
	 *         where a record has gotten smaller.
	 * 16-xx - Data
	 *
	 * Each free block is laid out in the file system as follows:
	 * Bytes - Usage
	 * 00-03 - Free block ID (set to -1)
	 * 04-07 - Next record offset (not used by free block)
	 * 08-11 - Free block size in bytes (in bytes, big endian)
	 * 12-15 - Next free block offset (zero if this is the last free block)
	 * 16-xx - Data (not used by free block)
	 */

	/** REC_ID offset */
	private static final int REC_ID = 0;
	/** NEXT_OFFSET offset */
	private static final int NEXT_OFFSET = 4;
	/** BLOCK_SIZE offset */
	private static final int BLOCK_SIZE = 8;
	/** 
	 * data length offset of record block or next free block 
	 * offset of free block 
	 */
	private static final int DATALEN_OR_NEXTFREE = 12;
	/** DATA_OFFSET offset (offset to record data) */
	private static final int DATA_OFFSET = 16; 
	
	/** offset of the record block or free block in RSFile */
	int offset;
	/** record id -or- -1 if free block */
	int id;
	/** next record offset */
	int nextOffset;
	/** record size -or- free block size */
	int blockSize;  
	/** record length -or- next free block offset */
	int dataLenOrNextFree;
	
	/**
	 * default RecordHeader constructor - creates an empty header
	 */
	RecordHeader() { }
	
	/**
	 * Creates a new RecordHeader and initializes it with data
	 * read from a RecordStoreFile at offset <code> offset </code>.
	 *
	 * @param _offset seek offset in RecordStoreFile of desired 
	 *        RecordHeader.
	 *
	 * @exception IOException if there is an error reading the
	 *            underlying RecordStoreFile.
	 */
	RecordHeader(int _offset) throws java.io.IOException {
	    load(_offset);
	}

	/**
	 * Creates a new RecordHeader and initializes it with data 
	 * provided in the parameters.  This RecordHeader will not
	 * be stored (on disk/in persistant storage) until the 
	 * <code> store </code> method is called.
	 *
	 * @param _offset offset in RecordStoreFile where this 
	 *        RecordHeader should be stored.
	 * @param _id record id of this new RecordHeader
	 *
	 * @param next_offset next RecordHeader in linked list of
	 *        records.
	 * @param size total size of the storage block allocated for
	 *        this record or free block.
	 * @param len_or_free length of data in this record (may be shorter
	 *        than <code>size</code> -or- next free block if this
	 *        header is a free block header
	 */
	RecordHeader(int _offset, int _id, int next_offset, 
		     int size, int len_or_free) {
	    offset = _offset;
	    id = _id;
	    nextOffset = next_offset;
	    blockSize = size;
	    dataLenOrNextFree = len_or_free;
	}

	/**
	 * Re-uses a RecordHeader and initializes it with data
	 * read from a RecordStoreFile at offset <code> offset </code>.
	 *
	 * @param _offset seek offset in RecordStoreFile of desired 
	 *        RecordHeader.
	 *
	 * @exception IOException if there is an error reading the
	 *            underlying RecordStoreFile
	 */	
	void load(int _offset) throws java.io.IOException {
	    offset = _offset;
	    
	    // read rec header from file.
	    dbraf.seek(offset);
	    dbraf.read(recHeadBuf, 0, DB_RECORD_HEADER_LENGTH);
	    
	    id = RecordStore.getInt(recHeadBuf, REC_ID);
	    nextOffset = RecordStore.getInt(recHeadBuf, NEXT_OFFSET);
	    blockSize = RecordStore.getInt(recHeadBuf, BLOCK_SIZE);
	    dataLenOrNextFree = RecordStore.getInt(recHeadBuf, 
					       DATALEN_OR_NEXTFREE);
	}
	
	/**
	 * Flushes an in memory RecordHeader instance to storage in a 
	 * a RecordStoreFile at <code>offset</code>.
	 * @exception IOException if there is an error writing the
	 *            underlying RecordStoreFile
	 */	
	void store() throws java.io.IOException {
	    RecordStore.putInt(id, recHeadBuf, REC_ID);
	    RecordStore.putInt(nextOffset, recHeadBuf, NEXT_OFFSET);
	    RecordStore.putInt(blockSize, recHeadBuf, BLOCK_SIZE);
	    RecordStore.putInt(dataLenOrNextFree, recHeadBuf, 
			       DATALEN_OR_NEXTFREE);
	    
	    // write record header;
	    dbraf.seek(offset);
	    dbraf.write(recHeadBuf, 0, DB_RECORD_HEADER_LENGTH);
	}

	/**
	 * Reads data associated with this record from storage (a
	 * RecordStoreFile) at <code>offset</code>.
	 *
	 * Assumes CALLER has ensured <code>dataLenOrNextFree</code> is set 
	 * correctly, and that <code>dataLenOrNextFree</code> bytes will 
	 * fit into the array.
	 *
	 * @param buf data is read into this buffer.
	 * @param _offset position in <code>buf</code> to start reading
	 *        data into.
	 *
	 * @return number of bytes read.
	 *
	 * @exception IOException if there is an error reading the
	 *            underlying RecordStoreFile
	 */	
	int read(byte[] buf, int _offset) throws java.io.IOException {
	    dbraf.seek(offset + DATA_OFFSET);
	    return dbraf.read(buf, _offset, dataLenOrNextFree);
	}

	/**
	 * Writes data associated with this record to storage (a 
	 * RecordStoreFile) at <code>offset</code>.
	 *
	 * Assumes CALLER has ensured <code>dataLenOrNextFree</code> is
	 * set correctly.
	 *
	 * @param buf data to store in this record.
	 * @param _offset point in <code>buf</code> to begin write from.
	 *
	 * @exception IOException if there is an error writing the
	 *            underlying RecordStoreFile.
	 */	
	void write(byte[] buf, int _offset) throws java.io.IOException {
	    dbraf.seek(offset + DATA_OFFSET);
	    dbraf.write(buf, _offset, dataLenOrNextFree);
	}
    }


    /**
     * RecordHeaderCache providing a per RecordStore, in memory cache of 
     * recent  RecordHeader lookups in order to absorb in many cases the
     * need to search for records on disk.  Since the on disk data model
     * for record storage is a linked list, this can improve performance
     * when a record is accessed frequently.  
     *
     * (Currently implemented in a simple, direct mapped way.  Could be
     * modified to be random replacement/linear lookup, etc.  These design
     * considerations should be determined by measuring cache performance
     * on representative RMS use cases.)
     */
    private class RecordHeaderCache {

	/** a cache of RecordHeader objects */
	private RecordHeader[] mCache;
	
	/**
	 * Returns a new RecordHeaderCache able to hold up to <code>
	 * size</code> record headers.
	 *
	 * @param size max number of RecordHeader objects allowed 
	 *        in <code>mCache</code>.
	 */
	RecordHeaderCache(int size) {
	    mCache = new RecordHeader[size];
	}
	
	/**
	 * Returns a RecordHeader for record <code>rec_id</code> or 
	 * null if the desired record header is not in the cache
	 *
	 * @param rec_id record id of the desired record header.
	 *
	 * @return a RecordHeader object for record 
	 *         <code>rec_id</code>.
	 */
	RecordHeader get(int rec_id) {
	    int idx = rec_id % mCache.length;
	    RecordHeader rh = (RecordHeader)mCache[idx];
	    if ((mCache[idx] != null) && (mCache[idx].id != rec_id)) {
 		return null;
	    }
	    return rh;
	}
	
	/**
	 * Inserts a new RecordHeader into the cache.
	 *
	 * @param rh a RecordHeader to add to the cache.
	 */
	void insert(RecordHeader rh) {
	    int idx = rh.id % mCache.length;
	    mCache[idx] = rh;
	}
	
	/**
	 * Removes a RecordHeader from the cache if it exists,
	 * otherwise does nothing.  Used in the case that the
	 * cached RecordHeader is no longer valid.
	 *
	 * @param rec_id the record ID of the RecordHeader to 
	 *        invalidate
	 */
	void invalidate(int rec_id) {
	    if (rec_id > 0) {
		int idx = rec_id % mCache.length;
		if ((mCache[idx] != null) && 
		    (mCache[idx].id == rec_id)) {
		mCache[idx] = null;
		}
	    }
	}
    }

    /**
     * Apps must use <code>openRecordStore()</code> to get
     * a <code>RecordStore</code> object. This constructor
     * is used internally for creating RecordStore objects.
     *
     * <code>dbCacheLock</code> must be held before calling
     * this constructor.
     *
     * @param uidPath unique storage id for this record store
     * @param recordStoreName a string to name the record store
     * @param create if true, create the record store if it doesn't exist
     *
     * @exception RecordStoreException if something goes wrong setting up
     *            the new RecordStore.
     * @exception RecordStoreNotFoundException if can't find the record store
     *            and create is set to false.
     * @exception RecordStoreFullException if there is no room in storage
     *            to create a new record store
     */
    
    private RecordStore(String uidPath, String recordStoreName, boolean create)
        throws RecordStoreException, RecordStoreNotFoundException
    {
	this.recordStoreName = recordStoreName;
	this.uniqueIdPath = uidPath;
	
	recHeadCache = new RecordHeaderCache(CACHE_SIZE);
	rsLock = new Object();
	recordListener = new java.util.Vector(3); 
	
	boolean exists = RecordStoreFile.exists(uidPath);

	// Check for errors between app and record store existance.
	if (!create && !exists) {
	    throw new RecordStoreNotFoundException("cannot find record " 
						   + "store file");
	}
	/* 
	 * If a new RecordStore will be created in storage, 
	 * check to see if the space required is available.
	 */
	if (create && !exists) {
	    int space = RecordStoreFile.spaceAvailable();
	    if (space - DB_INIT.length < 0) { 
		throw new RecordStoreFullException();
	    }
	}

	// Create a RecordStoreFile for storing the record store.
	try {
	    dbraf = new RecordStoreFile(uidPath);
	    /*
	     * At this point we've opened the RecordStoreFile.  If we
	     * created a new record store, initialize the db attributes.
	     */
	    if (create && !exists) {
		// Initialize record store attributes
		dbraf.seek(RS_SIGNATURE);
		// Update the timestamp
		RecordStore.putLong(System.currentTimeMillis(),
				    DB_INIT, RS_LAST_MODIFIED);
		RecordStore.putInt(48, DB_INIT, RS_DATA_START);
		RecordStore.putInt(48, DB_INIT, RS_DATA_END);
		dbraf.write(DB_INIT);
	    } else {
		/*
		 * Create a buffer and read the database attributes
		 * Read the record store attributes. Set up internal state.
		 */
		byte[] buf = new byte[DB_INIT.length];
		dbraf.seek(RS_SIGNATURE);
		dbraf.read(buf);
		/*
		 * Verify that the file is actually a record store
		 * by verifying the record store "signature."
		 */
		for (int i = 0; i < SIGNATURE_LENGTH; i++) {
		    if (buf[i] != DB_INIT[i]) {
			throw new RecordStoreException("invalid record "+
						       "store contents");
		    }
		}

		// Convert byte array to internal state variables.
		dbNumLiveRecords = RecordStore.getInt(buf, RS_NUM_LIVE);
		dbVersion = RecordStore.getInt(buf, RS_VERSION);
		dbAuthMode = RecordStore.getInt(buf, RS_AUTHMODE);
		dbNextRecordID = RecordStore.getInt(buf, RS_NEXT_ID);
		dbFirstRecordOffset = RecordStore.getInt(buf, RS_REC_START);
		dbFirstFreeBlockOffset = RecordStore.getInt(buf, 
							    RS_FREE_START);
		dbLastModified = RecordStore.getLong(buf, RS_LAST_MODIFIED);
		dbDataStart = RecordStore.getInt(buf, RS_DATA_START);
		dbDataEnd = RecordStore.getInt(buf, RS_DATA_END);
	    }
	    
	} catch (java.io.IOException ioe) {
	    try {
		if (dbraf != null) {
		    dbraf.close();
		}
	    } catch (java.io.IOException ioe2) { 
		// ignore exception within exception block
	    } finally {
		dbraf = null;
	    }
	    throw new RecordStoreException("error opening record store " + 
					   "file");
	} 
    }
    
    /** 
     * Internal method to check record store owner vs. the vendor and suite
     * of the currently running midlet
     * 
     * @return <code>true</code> if vendor and suite name both match, 
     * <code>false</code> otherwise
     */
    private boolean checkOwner() {

	// varies with currently running midlet suite
	String myUid = RecordStoreFile.getUniqueIdPath(recordStoreName);
	// fixed at the time dbraf is created
	String rsfUid = dbraf.getUniqueIdPath();

	if (myUid.equals(rsfUid)) {
	    return true;
	} else {
	    return false;
	}
    }
    
    /** 
     * Internal method to determine if writing to this record store
     * is allowed for the calling MIDlet.  Returns <code>true</code>
     * if <code>checkOwner()</code> returns <code>true</code> or
     * <code>dbAuthMode</code> == 1 when <code>checkOwner()</code>
     * returns <code>false</code>.
     * 
     * @return <code>true</code> if the record modification request
     * should be allowed, <code>false</code> otherwise
     */
    private boolean checkWritable() {
	if (checkOwner()) {
	    return true;
	} else {
	    if (dbAuthMode == AUTHMODE_ANY) { // Read-Write mode
		return true;
	    }
	}
	return false;
    }

    /** pre initialized RecordStore header structure */
    private static final byte[] DB_INIT = {
	(byte)'m', (byte)'i', (byte)'d', (byte)'p', 
	(byte)'-', (byte)'r', (byte)'m', (byte)'s',
	0, 0, 0, 0, // num live records
	0, 0, 0, 0, // AUTHMODE_PRIVATE by default
	0, 0, 0, 0, // version
	0, 0, 0, 1, // next record id
	0, 0, 0, 0, // first record offset
	0, 0, 0, 0, // first free-space offset
	0, 0, 0, 0, // last modified (1st half)
	0, 0, 0, 0, // last modified (2nd half)
	0, 0, 0, 0, // start of data offset
	0, 0, 0, 0  // end of data offset
    };

    /** length of the signature string in bytes */
    private static final int SIGNATURE_LENGTH = 8;
    
    /** size of a per record meta-data object */
    private static final int DB_RECORD_HEADER_LENGTH = 16;

    /** 
     * storage space allocated in multiples of <code>DB_BLOCK_SIZE</code>,
     * which can not be smaller than DB_RECORD_HEADER_LENGTH and
     * must be a multiple of DB_RECORD_HEADER_LENGTH
     */
    private static final int DB_BLOCK_SIZE = 16; 

    /** size of the buffer for compacting record store */
    private static final int DB_COMPACTBUFFER_SIZE = 64;
    
    /** cache of open RecordStore instances */
    private static java.util.Vector dbCache = new java.util.Vector(3);

    /** lock to protect static dbcache state */
    private static final Object dbCacheLock = new Object();
    
    /** name of this record store */
    private String recordStoreName;

    /** unique storage id for this record store */
    private String uniqueIdPath;

    /** number of open instances of this record store */
    private int opencount;
    
    /** RecordStoreFile where this record store is stored */
    private RecordStoreFile dbraf;
    
    /** lock used to synchronize this record store */
    Object rsLock;

    /** recordListeners of this record store */
    private java.util.Vector recordListener;
    
    /** cache of record headers */
    private RecordHeaderCache recHeadCache;

    /** number of direct mapped cache entries */
    private static int CACHE_SIZE = 8;

    /** static buffer used in loading/storing RecordHeader data */
    private static byte[] recHeadBuf = new byte[DB_RECORD_HEADER_LENGTH];

    /*
     * This implementation assumes (and enforces) that there is only
     * one instance of a RecordStore object for any given database file 
     * in the file system. This assumption is enforceable when there 
     * is only one VM running, but will lead to data corruption problems
     * if multiple VM's are used to read/write into the same database.
     *
     * As a consequence of this assumption, the following database
     * attributes are read from the RecordStoreFile when the RecordStore
     * is opened, maintained in memory for performance efficiency, and
     * reflected back to the file only when necessary for the sake of
     * error resilience.
     */

    /** next record's id */
    private int dbNextRecordID = 1;
    
    /** record store version */
    private int dbVersion;	

    /** 
     * authorization mode of this record store 
     * 0:  AUTHMODE_PRIVATE,  Read/Write
     * 1:  AUTHMODE_ANY,      Read/Write
     * 2:  AUTHMODE_ANY_RO,   Read-Only
     */
    private int dbAuthMode;
    
    /** count of live records */
    private int dbNumLiveRecords;

    /** time record store was last modified (in milliseconds */
    private long dbLastModified;
    
    /** offset of first record */
    private int dbFirstRecordOffset;
    
    /** offset of first free block */
    private int dbFirstFreeBlockOffset; 

    /** offset of the first data block */
    private int dbDataStart = 48;

    /** offset of the last data block */
    private int dbDataEnd = 48;

    /** static buffer used in loading/storing dbState */
    private static byte[] dbState = new byte[DB_INIT.length];
	    

    /*
     * The layout of the database file is as follows:
     *
     * Bytes - Usage
     * 00-07 - Signature = 'midp-rms'
     * 08-11 - Number of live records in the database (big endian)
     * 12-15 - Authmode and Writable state info
     * 16-19 - Database "version" - a monotonically increasing revision 
     *         number (big endian)
     * 20-23 - Next record ID to use (big endian)
     * 24-27 - First record offset (bytes from beginning of file - big endian)
     * 28-31 - First free-block offset (bytes from beginning of file - big 
     *         endian)
     * 32-39 - Last modified (64-bit long, big endian, milliseconds since 
     *         jan 1970)
     * 40-43 - Start of Data storage
     * 44-47 - End of Data storage
     * 48-xx - Record storage
     */

    /** RS_SIGNATURE offset */
    private static final int RS_SIGNATURE = 0;
    /** RS_NUM_LIVE offset */
    private static final int RS_NUM_LIVE = 8;
    /** RS_AUTHMODE offset */
    private static final int RS_AUTHMODE = 12;
    /** RS_VERSION offset */
    private static final int RS_VERSION = 16;
    /** RS_NEXT_ID offset */
    private static final int RS_NEXT_ID = 20;
    /** RS_REC_START offset */
    private static final int RS_REC_START = 24;
    /** RS_FREE_START offset */
    private static final int RS_FREE_START = 28;
    /** RS_LAST_MODIFIED offset */
    private static final int RS_LAST_MODIFIED = 32;
    /** RS_START_OF_DATA offset */
    private static final int RS_DATA_START = 40;
    /** RS_END_OF_DATA offset */
    private static final int RS_DATA_END = 44;

}