/*
 *
 *
 * Portions Copyright  2003-2006 Sun Microsystems, Inc. All Rights Reserved.
 * 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 com.sun.midp.rms;

import java.io.IOException;
import javax.microedition.rms.*;

import com.sun.midp.security.Permissions;
import com.sun.midp.security.SecurityToken;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;

/**
 * A class implementing a MIDP a record store.
 */

public class RecordStoreImpl implements AbstractRecordStoreImpl {
    /** used to compact the records of the record store */
    private byte[] compactBuffer = new byte[COMPACT_BUFFER_SIZE];

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

    /** unique id for suite that owns this record store */
    int suiteId;

    /** lock used to synchronize this record store */
    Object rsLock;

    /** data block header stored here */
    byte[] dbHeader;

    /** record store index */
    private RecordStoreIndex dbIndex;

    /** record store data */
    private RecordStoreFile dbFile;

    /**
     * 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 token security token for authorization
     * @param suiteId ID of the MIDlet suite that owns the record store
     * @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(SecurityToken token,
                                         int suiteId,
                                         String recordStoreName)
        throws RecordStoreException, RecordStoreNotFoundException {

        token.checkIfPermissionAllowed(Permissions.MIDP);

        // check if file exists and delete it
        if (RecordStoreUtil.exists(suiteId, recordStoreName,
                                   RecordStoreFile.DB_EXTENSION)) {
            boolean success = RecordStoreIndex.deleteIndex(
                suiteId, recordStoreName);
            RecordStoreUtil.deleteFile(suiteId,
                recordStoreName, RecordStoreFile.DB_EXTENSION);

            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 RecordStoreImpl object.
     *
     * @param token security token for authorization
     * @param suiteId ID of the MIDlet suite that owns the record store
     * @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 RecordStoreImpl openRecordStore(SecurityToken token,
                                                  int suiteId,
                                                  String recordStoreName,
                                                  boolean createIfNecessary)
        throws RecordStoreException, RecordStoreFullException,
               RecordStoreNotFoundException {

        token.checkIfPermissionAllowed(Permissions.MIDP);

        return new RecordStoreImpl(suiteId, recordStoreName,
                                   createIfNecessary);
    }

    /**
     * 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
     */
    public void setMode(int authmode, boolean writable)
        throws RecordStoreException {

        synchronized (rsLock) {
            int newAuthMode = authmode;
            if ((authmode == RecordStore.AUTHMODE_ANY) &&
                    (writable == false)) {
                newAuthMode = AUTHMODE_ANY_RO;
            }

            RecordStoreUtil.putInt(newAuthMode, dbHeader, RS1_AUTHMODE);

            try {
                // write out the changes to the db header
                dbFile.seek(RS1_AUTHMODE);
                dbFile.write(dbHeader, RS1_AUTHMODE, 4);
                // dbFile.commitWrite();
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error writing record " +
                                               "store attributes");
            }
        }
    }

    /**
     * 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) {
            try {
                // close native fd
                compactRecords();  // compact before close

                dbFile.close();

                dbIndex.close();
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error closing .db file. "
						+ ioe);
            } finally {
                dbFile = 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.
     *
     * @param token security token for authorization
     * @param suiteId ID of the MIDlet suite that owns the record store
     *
     * @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(SecurityToken token,
                                            int suiteId) {
        token.checkIfPermissionAllowed(Permissions.MIDP);

        return RecordStoreFile.listRecordStores(suiteId);
    }

    /**
     * Get the authorization mode for this record store.
     *
     * @return authorization mode
     */
    public int getAuthMode() {
        return RecordStoreUtil.getInt(dbHeader, RS1_AUTHMODE);
    }

    /**
     * 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
     */
    public int getVersion() throws RecordStoreNotOpenException {
        return RecordStoreUtil.getInt(dbHeader, RS4_VERSION);
    }

    /**
     * Returns the number of records currently in the record store.
     *
     * @return the number of records currently in the record store
     */
    public int getNumRecords() {
        return RecordStoreUtil.getInt(dbHeader, RS3_NUM_LIVE);
    }

    /**
     * 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.
     *
     * @return the size of the record store in bytes
     */
    public int getSize() {
        return DB_HEADER_SIZE + RecordStoreUtil.getInt(dbHeader, RS6_DATA_SIZE);
    }

    /**
     * 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.
     *
     * @return the amount of additional room (in bytes) available for
     *          this record store to grow
     */
    public int getSizeAvailable() {
        int fileSpace = dbFile.spaceAvailable(suiteId) -
                        BLOCK_HEADER_SIZE - DB_HEADER_SIZE;
        int limitSpace = RMSConfig.STORAGE_SUITE_LIMIT -
                         RecordStoreUtil.getInt(dbHeader, RS6_DATA_SIZE) -
                         BLOCK_HEADER_SIZE - DB_HEADER_SIZE;

        int rv = (fileSpace < limitSpace) ? fileSpace : limitSpace;
        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()
     */
    public long getLastModified() {
        return RecordStoreUtil.getLong(dbHeader, RS5_LAST_MODIFIED);
    }

    /**
     * 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
     */
    public int getNextRecordID() {
        return RecordStoreUtil.getInt(dbHeader, RS2_NEXT_ID);
    }

    /**
     * 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) {
            int recordId = getNextRecordID();

            try {
                // add a block for this record
                addBlock(recordId, data, offset, numBytes);

                // update the db header
                RecordStoreUtil.putInt(recordId+1, dbHeader, RS2_NEXT_ID);
                RecordStoreUtil.putInt(getNumRecords()+1, dbHeader, RS3_NUM_LIVE);
                RecordStoreUtil.putInt(getVersion()+1, dbHeader, RS4_VERSION);
                RecordStoreUtil.putLong(System.currentTimeMillis(), dbHeader,
                        RS5_LAST_MODIFIED);

                // write out the changes to the db header
                dbFile.seek(RS2_NEXT_ID);
                dbFile.write(dbHeader, RS2_NEXT_ID, 3*4+8);
                // dbFile.commitWrite();
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error writing new record "
                                               + "data");
            }

            // Return the new record id
            return recordId;
        }
    }

    /**
     * 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) {
            try {
                byte[] header = new byte[BLOCK_HEADER_SIZE];
                int blockOffset = dbIndex.getRecordHeader(recordId, header);

                // free the block
                freeBlock(blockOffset, header);

                // update the db index
                dbIndex.deleteRecordIndex(recordId);

                // update the db header
                RecordStoreUtil.putInt(getNumRecords()-1, dbHeader, RS3_NUM_LIVE);
                RecordStoreUtil.putInt(getVersion()+1, dbHeader, RS4_VERSION);
                RecordStoreUtil.putLong(System.currentTimeMillis(), dbHeader,
                        RS5_LAST_MODIFIED);

                // save the updated db header
                dbFile.seek(RS3_NUM_LIVE);
                dbFile.write(dbHeader, RS3_NUM_LIVE, 2*4+8);
                // dbFile.commitWrite();

            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error updating file after" +
                                               " record deletion");
            }
        }
    }

    /**
     * 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) {
            byte[] header = new byte[BLOCK_HEADER_SIZE];

            try {
                dbIndex.getRecordHeader(recordId, header);
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error reading record data");
            }

            return RecordStoreUtil.getInt(header, 4);
        }
    }

    /**
     * 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) {
            try {
                byte[] header = new byte[BLOCK_HEADER_SIZE];
                int blockOffset = dbIndex.getRecordHeader(recordId, header);

                int dataSize = RecordStoreUtil.getInt(header, 4);

                dbFile.seek(blockOffset+BLOCK_HEADER_SIZE);
                return dbFile.read(buffer, offset, dataSize);
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error reading record data");
            }
        }
    }

    /**
     * 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) {
            try {
                byte[] header = new byte[BLOCK_HEADER_SIZE];
                int blockOffset = dbIndex.getRecordHeader(recordId, header);

                int dataSize = RecordStoreUtil.getInt(header, 4);
                if (dataSize == 0) {
                    return null;
                }

                byte[] buffer = new byte[dataSize];

                dbFile.seek(blockOffset+BLOCK_HEADER_SIZE);
                dbFile.read(buffer);

                return buffer;
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error reading record 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) {
            try {
                byte[] header = new byte[BLOCK_HEADER_SIZE];
                int blockOffset = dbIndex.getRecordHeader(recordId, header);

                int oldBlockSize =
                  RecordStoreUtil.calculateBlockSize(RecordStoreUtil.getInt(
                                                       header, 4));
                int newBlockSize = RecordStoreUtil.calculateBlockSize(numBytes);
                if (newBlockSize <= oldBlockSize) {
                    // reuse the old block
                    splitBlock(blockOffset, header, newData, offset, numBytes);
                } else {
                    // free the old record data
                    freeBlock(blockOffset, header);

                    // add a block that contains the new record data
                    addBlock(recordId, newData, offset, numBytes);
                }

                // update the db header
                RecordStoreUtil.putInt(getVersion()+1, dbHeader, RS4_VERSION);
                RecordStoreUtil.putLong(System.currentTimeMillis(), dbHeader,
                        RS5_LAST_MODIFIED);

                // write out the changes to the db header
                dbFile.seek(RS4_VERSION);
                dbFile.write(dbHeader, RS4_VERSION, 4+8);
                // dbFile.commitWrite();
            } catch (java.io.IOException ioe) {
                throw new RecordStoreException("error setting record data");
            }
        }
    }

    /**
     * Returns all of the recordId's currently in the record store.
     *
     * @return an array of the recordId's currently in the record store
     *         or null if the record store is closed.
     */
    public int[] getRecordIDs() {
        synchronized (rsLock) {
            return dbIndex.getRecordIDs();
        }
    }

    /**
     * Returns data base file associated with this record store
     *
     * @return data base file
     */
    public AbstractRecordStoreFile getDbFile() {
        return dbFile;
    }

    /**
     * Creates data base index file associated with this record store
     *
     * @param suiteId unique ID of the suite that owns the store
     * @param name a string to name the index file
     * @return data base index file
     * @exception IOException if failed to create a file
     */
    public AbstractRecordStoreFile createIndexFile(int suiteId, String name)
      throws IOException {
        return new RecordStoreFile(suiteId, name,
                                   AbstractRecordStoreFile.IDX_EXTENSION);
    }

    /**
     * Remove free blocks from the record store and compact records
     * with data into as small a space in <code>dbFile</code> as
     * possible.  Operates from smallest to greatest offset in
     * <code>dbFile</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 dbFile size.
     *
     * @exception RecordStoreNotOpenException if this record store
     *            is closed
     * @exception RecordStoreException if an error occurs during record
     *            store compaction
     */
    private void compactRecords()
        throws IOException {

        // check if the db can be compacted
        if (RecordStoreUtil.getInt(dbHeader, RS7_FREE_SIZE) == 0) {
            // no free space to compact
            return;
        }

        byte[] header = new byte[BLOCK_HEADER_SIZE];
        int currentId = 0;
        int currentOffset = RecordStoreImpl.DB_HEADER_SIZE;
        int currentSize = 0;
        int moveUpNumBytes = 0;

        // search through the data blocks for a free block that is large enough
        while (currentOffset < getSize()) {
            // seek to the next offset
            dbFile.seek(currentOffset);

            // read the block header
            if (dbFile.read(header) != RecordStoreImpl.BLOCK_HEADER_SIZE) {
                // could not read the block
                throw new IOException();
            }

            currentId = RecordStoreUtil.getInt(header, 0);
            currentSize =
              RecordStoreUtil.calculateBlockSize(RecordStoreUtil.getInt(
                                                   header, 4));

            if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                               "currentId = " + currentId +
                               " currentSize = " + currentSize);
            }

            // check if this is a free block or a record
            if (currentId < 0) {
                if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                    Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                                   "found free block at offset " +
                                   currentOffset);
                }

                // a free block, add to the moveUpNumBytes
                moveUpNumBytes += currentSize;

                // remove from the index
                dbIndex.removeBlock(currentOffset, header);
            } else {
                if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                    Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                                   "found record block at offset " +
                                   currentOffset);
                }

                // a record data block, check if it needs to be moved up
                if (moveUpNumBytes > 0) {

                    if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                        Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                                       "moveUpNumBytes = " + currentOffset);
                    }

                    int numMoved = 0;
                    while (numMoved < currentSize) {
                        int curRead = currentSize - numMoved;
                        if (curRead > COMPACT_BUFFER_SIZE) {
                            curRead = COMPACT_BUFFER_SIZE;
                        }

                        dbFile.seek(currentOffset + numMoved);
                        curRead = dbFile.read(compactBuffer, 0, curRead);
                        if (curRead == -1) {
                            throw new IOException();
                        }

                        dbFile.seek(currentOffset + numMoved - moveUpNumBytes);
                        dbFile.write(compactBuffer, 0, curRead);
                        // dbFile.commitWrite();
                        numMoved += curRead;
                    }

                    dbIndex.updateBlock(currentOffset - moveUpNumBytes, header);
                }
            }

            // added the block size to the currentOffset
            currentOffset += currentSize;
        }

        // check if the db file can be truncated
        if (moveUpNumBytes > 0) {
            RecordStoreUtil.putInt(
                RecordStoreUtil.getInt(dbHeader, RS6_DATA_SIZE) -
                    moveUpNumBytes, dbHeader, RS6_DATA_SIZE);
            RecordStoreUtil.putInt(0, dbHeader, RS7_FREE_SIZE);
            dbFile.seek(RS6_DATA_SIZE);
            dbFile.write(dbHeader, RS6_DATA_SIZE, 4+4);
            // dbFile.commitWrite();

            dbFile.truncate(getSize());

            if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                               "compactRecords, truncate to size " +
                               getSize());
            }
        }
    }

    /**
     * Set the record in the block to the data passed in and adds any remaining
     * space to the free list.
     *
     * @param blockOffset the offset in db file to the block to split
     * @param header the header of the block to split
     * @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 IOException if there is an error accessing the db file
     */
    private void splitBlock(int blockOffset, byte[] header,
                            byte[] newData, int offset,
                            int numBytes) throws IOException {

        if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
            Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                           "splitBlock("+RecordStoreUtil.getInt(header, 0) +
                           ") old numBytes = " + RecordStoreUtil.getInt(header, 4) +
                           " new numBytes=" + numBytes);
        }

        // calculate the size of the block
        int oldBlockSize =
          RecordStoreUtil.calculateBlockSize(RecordStoreUtil.getInt(
                                               header, 4));

        // calculate the size of the block
        int newBlockSize = RecordStoreUtil.calculateBlockSize(numBytes);

        // update the block header
        RecordStoreUtil.putInt(numBytes, header, 4);

        // seek to the location and write the block and header
        writeBlock(blockOffset, header, newData, offset, numBytes);

        // check if there is any left over free space
        int freeSize = oldBlockSize - newBlockSize - BLOCK_HEADER_SIZE;
        if (freeSize >= 0) {
            // update the block header and free block of extra space
            RecordStoreUtil.putInt(freeSize, header, 4);
            freeBlock(blockOffset+newBlockSize, header);
        }
    }

    /**
     * Adds a block for the record and data or sets an existing block to
     * the data.  Splits an exiting block if needed.
     *
     * @param recordId the ID of the record to use in this operation
     * @param data 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 RecordStoreFullException if the operation cannot be
     *          completed because the record store has no more room
     * @exception IOException if there is an error accessing the db file
     * @exception RecordStoreException if the new consumption is gong to
     *            exceed the resource limit
     *
     * @return the offset in the db file of the block added
     */
    private int addBlock(int recordId, byte[] data, int offset, int numBytes)
        throws IOException, RecordStoreFullException, RecordStoreException {

        if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
            Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                           "addBlock("+recordId+") numBytes="+numBytes);
        }

        // calculate the size of the block needed
        int blockSize = RecordStoreUtil.calculateBlockSize(numBytes);
        int blockOffset = 0;
        int freeBlocksSize = RecordStoreUtil.getInt(dbHeader, RS7_FREE_SIZE);

        // initialize the block header
        byte[] header = new byte[BLOCK_HEADER_SIZE];

        // check if there is the potential for a large enough free block
        if (freeBlocksSize >= blockSize) {

            if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                               "blockSize = " + blockSize +
                               "free size = " + freeBlocksSize);
            }

            // initialize the number of bytes and search for an
            // available free block
            RecordStoreUtil.putInt(numBytes, header, 4);
            blockOffset = dbIndex.getFreeBlock(header);
        }

        // set the recordId in to the block header
        RecordStoreUtil.putInt(recordId, header, 0);

        if (blockOffset > 0) {
            // search found a block, use it
            splitBlock(blockOffset, header, data, offset, numBytes);
        } else {
            // search failed, add a new block to the end of the db file
            int spaceAvailable = getSizeAvailable();
            /**
             * spaceAvailable returns the smaller number of total space
             * available and the storage limit per suite. If it is less than
             * the block size to be added, RecordStoreFullException is thrown
             */
            if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                               "spaceAvailable = "+spaceAvailable);
            }

            // Is there room to grow the file?
            if (spaceAvailable < blockSize) {
                 // Is there enough room totally: in storage and free blocks?
                 if (spaceAvailable + freeBlocksSize < blockSize) {
                     throw new RecordStoreFullException();
                 }
                 compactRecords();
            }

            blockOffset = getSize();

            // seek to the location and write the block and header
            RecordStoreUtil.putInt(numBytes, header, 4);
            writeBlock(blockOffset, header, data, offset, numBytes);

            // update the db data size
            RecordStoreUtil.putInt(RecordStoreUtil.getInt(dbHeader, RS6_DATA_SIZE) + blockSize,
                   dbHeader, RS6_DATA_SIZE);
            dbFile.seek(RS6_DATA_SIZE);
            dbFile.write(dbHeader, RS6_DATA_SIZE, 4);
            // dbFile.commitWrite();

            if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
                Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                               "blockOffset = "+blockOffset);
            }
        }

        return blockOffset;
    }

    /**
     * Mark the block at the given offset in db file as free.
     *
     * @param blockOffset the offset in db file to the block to free
     * @param header the header of the block to free
     *
     * @exception IOException if there is an error accessing the db file
     */
    private void freeBlock(int blockOffset, byte[] header) throws IOException {
        int dataSize = RecordStoreUtil.getInt(header, 4);

        // calculate the size of the block
        int blockSize = RecordStoreUtil.calculateBlockSize(dataSize);

        if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
            Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
                           "freeBlock(" + blockOffset + ") size = " +
                           blockSize);
        }

        // mark the block as free
        RecordStoreUtil.putInt(-1, header, 0);
        RecordStoreUtil.putInt(blockSize - BLOCK_HEADER_SIZE, header, 4);

        // save the updated block header
        writeBlock(blockOffset, header, null, 0, 0);

        // add to the db free size
        RecordStoreUtil.putInt(RecordStoreUtil.getInt(dbHeader, RS7_FREE_SIZE) + blockSize,
               dbHeader, RS7_FREE_SIZE);
        dbFile.seek(RS7_FREE_SIZE);
        dbFile.write(dbHeader, RS7_FREE_SIZE, 4);
        // dbFile.commitWrite();
    }

    /**
     * Writes a block to the db file at the given offset.
     *
     * @param blockOffset the offset in db file to write the block
     * @param header the header of the block to write
     * @param data 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 IOException if there is an error accessing the db file
     */
    private void writeBlock(int blockOffset, byte[] header,
                            byte[] data, int offset, int numBytes)
        throws IOException {

        int remainder;
        dbFile.seek(blockOffset);
        dbFile.write(header);
        if (data != null && numBytes > 0) {
            dbFile.write(data, offset, numBytes);
            remainder = numBytes % BLOCK_HEADER_SIZE;
            if (remainder != 0) {
                // DB_SIGNATURE used here as meaningless pad bytes
                dbFile.write(DB_SIGNATURE, 0, BLOCK_HEADER_SIZE - remainder);
            }
        }
        // flush the writes
        // dbFile.commitWrite();
        // update the index
        dbIndex.updateBlock(blockOffset, header);
    }

    /**
     * Creates a RecordStoreImpl instance; for internal use only.
     * Callers from outside must use <code>openRecordStore()</code>.
     *
     * @param suiteId unique ID of the suite that owns the 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 RecordStoreImpl(int suiteId, String recordStoreName,
                            boolean create)
        throws RecordStoreException, RecordStoreNotFoundException {

        this.suiteId = suiteId;
        rsLock = new Object();

        boolean exists = RecordStoreUtil.exists(suiteId, recordStoreName,
                                                RecordStoreFile.DB_EXTENSION);

        // Check for errors between app and record store existance.
        if (!create && !exists) {
            throw new RecordStoreNotFoundException("cannot find record "
                                                   + "store file");
        }

        /*
         * If a new RecordStoreImpl will be created in storage,
         * check to see if the space required is available.
         */
        if (create && !exists) {
            int space = RecordStoreFile.spaceAvailableNewRecordStore(suiteId);
            if (space - DB_HEADER_SIZE < 0) {
                throw new RecordStoreFullException();
            }
        }

        // Create a RecordStoreFile for storing the record store.
        try {
            dbFile = new RecordStoreFile(suiteId, recordStoreName,
                                         RecordStoreFile.DB_EXTENSION);

            // allocate a new header
            dbHeader = new byte[DB_HEADER_SIZE];

            if (exists) {
                // load header
                dbFile.read(dbHeader);

                /*
                 * Verify that the file is actually a record store
                 * by verifying the record store "signature."
                 */
                for (int i = 0; i < DB_SIGNATURE.length; i++) {
                    if (dbHeader[i] != DB_SIGNATURE[i]) {
                        throw new RecordStoreException("invalid record "+
                                                       "store contents");
                    }
                }
            } else {
                // initialize the header
                for (int i = 0; i < DB_SIGNATURE.length; i++) {
                    dbHeader[i] = DB_SIGNATURE[i];
                }

                RecordStoreUtil.putInt(1, dbHeader, RS2_NEXT_ID);
                RecordStoreUtil.putLong(System.currentTimeMillis(), dbHeader,
                        RS5_LAST_MODIFIED);

                // write the header to the file
                dbFile.write(dbHeader);
                dbFile.commitWrite();
            }

            // create the index object
            dbIndex = new RecordStoreIndex(this, suiteId, recordStoreName);

        } catch (java.io.IOException ioe) {
            try {
                if (dbFile != null) {
                    dbFile.close();
                }
            } catch (java.io.IOException ioe2) {
                // ignore exception within exception block
            }

            if (!exists) {
                // avoid preserving just created damaged files
                RecordStoreUtil.quietDeleteFile(suiteId,
                    recordStoreName, RecordStoreFile.DB_EXTENSION);
                RecordStoreIndex.deleteIndex(suiteId, recordStoreName);
            }

            dbFile = null;
            throw new RecordStoreException("error opening record store " +
                                           "file");
        }
    }
}
