/*
*
*
* Copyright 1990-2007 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.
*/
package com.sun.midp.rms;
import java.io.IOException;
import javax.microedition.rms.*;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;
/**
* This class implements a record offset cache. It enables us to quickly
* find offsets for records whose IDs we know.
*/
class OffsetCache extends IntToIntMapper {
/**
* A special value for LastSeenOffset.
* There's a database header at offset 0, and 0 means we have not seen any
* records yet.
*/
static final int NO_OFFSET = 0;
/**
* After this offset there's no cached record IDs.
* (An OffsetCache may cache offsets only for already seen records.)
* The value NO_OFFSET is a special value for this variable:
* NO_OFFSET means we have not seen any records yet.
*/
public int LastSeenOffset = NO_OFFSET;
/**
* Constructs an empty OffsetCache with the specified initial capacity and
* capacity increment.
*
* @param initialCapacity the initial capacity
* @param defaultElement the offset value that gets returned for
* record IDs that are not there
* @param capacityIncrement the amount by which the capacity is
* increased when the mapper overflows.
* (0 means "to be doubled")
* @exception IllegalArgumentException if the specified initial capacity
* is negative
*/
OffsetCache(int initialCapacity, int defaultElement, int capacityIncrement)
{
super(initialCapacity, defaultElement, capacityIncrement);
}
}
/**
* A class implementing a index of the record store.
*/
class RecordStoreIndex {
/** The Record Store that this object indexes */
private AbstractRecordStoreImpl recordStore;
/** The Record Store database file */
private AbstractRecordStoreFile dbFile;
/** specifies record ID to offset mapping */
private OffsetCache recordIdOffsets;
/**
* This value will be returned by recordIdOffsets.elementAt() when
* record id is not found
*/
private static final int INVALID_OFFSET = -1;
/**
* Capacity increment for the offset cache. Zero indicates that capacity
* has to be doubled each time.
*/
private static final int CACHE_CAPACITY_INCREMENT = 0;
/** the initial record offset cache capacity */
private static final int INITIAL_CACHE_CAPACITY = 0x20;
/**
* Constructor for creating an index object for the given Record Store.
*
* @param rs record store that this object indexes
* @param suiteId unique ID of the suite that owns the store
* @param recordStoreName a string to name the record store
*/
RecordStoreIndex(AbstractRecordStoreImpl rs, int suiteId,
String recordStoreName) throws IOException {
recordStore = rs;
dbFile = rs.getDbFile();
}
/**
* Closes the index file.
*
* @exception IOException if there are any file errors
*/
void close() throws IOException {
}
/**
* Deletes index filed of named record store. There is no index
* file in case of linear index implementation, thus makes
* nothing.
*
* Called from RecordStoreImpl where record store files need to
* be deleted.
*
* @param suiteId not used
* @param recordStoreName not used
* @return always <code>true</code>
*/
static boolean deleteIndex(int suiteId, String recordStoreName) {
return true;
}
/**
* Returns all of the recordId's currently in the record store index.
*
* @return an array of the recordId's currently in the index.
*/
int[] getRecordIDs() {
int numRecordIDs = recordStore.getNumRecords();
int[] recordIDs = new int[numRecordIDs];
byte[] header = new byte[AbstractRecordStoreImpl.BLOCK_HEADER_SIZE];
int currentOffset = AbstractRecordStoreImpl.DB_HEADER_SIZE;
int currentSize = 0;
int currentRecordId = 0;
int dbSize = recordStore.getSize();
int idx = 0;
while (idx < numRecordIDs) {
if (currentOffset >= dbSize) {
// reached end of db file
break;
}
try {
// seek to the next offset
currentOffset += currentSize;
dbFile.seek(currentOffset);
// read the block header
if (dbFile.read(header) !=
AbstractRecordStoreImpl.BLOCK_HEADER_SIZE) {
// error reading header
break;
}
} catch (java.io.IOException ioe) {
break;
}
currentRecordId = RecordStoreUtil.getInt(header, 0);
if (currentRecordId > 0) {
recordIDs[idx++] = currentRecordId;
}
currentSize = RecordStoreUtil.
calculateBlockSize(RecordStoreUtil.getInt(header, 4));
}
return recordIDs;
}
/**
* Finds the record header for the given record and returns the
* offset to the header.
*
* @param recordId the ID of the record to use in this operation
* @param header a buffer that receives the header of the block
*
* @exception IOException if there is an error accessing the db file
* @exception InvalidRecordIDException if the recordId is invalid
*
* @return the offset in the db file of the block added
*/
int getRecordHeader(int recordId, byte[] header)
throws IOException, InvalidRecordIDException {
if (recordId <= 0) {
throw new InvalidRecordIDException("error finding record data");
}
if (null != recordIdOffsets)
{
int offset = recordIdOffsets.elementAt(recordId);
if (offset != recordIdOffsets.defaultValue) {
dbFile.seek(offset);
// read the block header
if (dbFile.read(header) !=
AbstractRecordStoreImpl.BLOCK_HEADER_SIZE) {
// did not find the recordId at the returned offset
// throw away the index then!
// (in principle, this should never happen)
recordIdOffsets = null;
}
else
{
int foundId = RecordStoreUtil.getInt(header, 0);
if (foundId == recordId) {
return offset;
}
}
}
}
if (null == recordIdOffsets) {
recordIdOffsets = new OffsetCache(INITIAL_CACHE_CAPACITY,
INVALID_OFFSET,
CACHE_CAPACITY_INCREMENT);
}
return getRecordHeader_NoCache(recordId, header);
}
/**
* A helper function for getRecordHeader().
* Finds the record header for the given record and returns the
* offset to the header without using the cache.
*
* @param recordId the ID of the record to use in this operation
* @param header a buffer that receives the header of the block
* @return the offset in the db file of the block
* @exception IOException
* @exception InvalidRecordIDException
*/
protected int getRecordHeader_NoCache(int recordId, byte[] header)
throws IOException, InvalidRecordIDException {
final int beginningOffset = AbstractRecordStoreImpl.DB_HEADER_SIZE;
final int endOffset = recordStore.getSize();
int resultOffset = INVALID_OFFSET;
if (null != recordIdOffsets
&&recordIdOffsets.LastSeenOffset != recordIdOffsets.NO_OFFSET)
{
resultOffset =
getRecordHeader_SearchFromTo(recordId,
header,
recordIdOffsets.LastSeenOffset,
endOffset);
}
if (INVALID_OFFSET == resultOffset) {
resultOffset =
getRecordHeader_SearchFromTo(recordId,
header,
beginningOffset,
endOffset);
}
if (INVALID_OFFSET == resultOffset) {
throw new InvalidRecordIDException("error finding record");
}
return resultOffset;
}
/**
* A helper function for getRecordHeader().
*
* @param recordId the ID of the record to use in this operation
* @param header a buffer that receives the header of the block
* @param offsetFrom offset in the file to start search from
* @param offsetUpto offset in the file to abandon search at
* @return the offset in the db file of the block, or
* -1 (INVALID_OFFSET) in place
* of InvalidRecordIDException
* @exception IOException
* @exception InvalidRecordIDException
*/
protected int getRecordHeader_SearchFromTo(int recordId,
byte[] header,
int offsetFrom,
int offsetUpto)
throws IOException, InvalidRecordIDException {
// (we could start from last known index; I tried this, but noticed
// no improvement)
// search through the data blocks for a matching record
int currentId = 0;
int currentOffset = offsetFrom;
int currentSize = 0;
while (currentId != recordId) {
// seek to the next offset
currentOffset += currentSize;
// IMPL_NOTE: is it correct to invoke getSize() once?
// IMPL_NOTE: So far it is -- the db file is locked...
if (currentOffset >= offsetUpto) {
// reached the end and did not find the recordId
return INVALID_OFFSET;
}
dbFile.seek(currentOffset);
// read the block header
if (dbFile.read(header) !=
AbstractRecordStoreImpl.BLOCK_HEADER_SIZE) {
// did not find the recordId
return INVALID_OFFSET; // have a chance to retry
}
currentId = RecordStoreUtil.getInt(header, 0);
currentSize = RecordStoreUtil.
calculateBlockSize(RecordStoreUtil.getInt(header, 4));
if (null != recordIdOffsets) {
recordIdOffsets.setElementAt(currentOffset, recordId);
recordIdOffsets.LastSeenOffset = currentOffset;
// if we had to repeat search from the beginning, there has been
// something wrong, and it's ok to forget where we have been.
// Normally, LastSeenOffset will only grow.
}
}
return currentOffset;
}
/**
* Searches for a free block large enough for the record.
*
* @param header a block header with the size set to the record data size
*
* @exception IOException if there is an error accessing the db file
*
* @return the offset in the db file of the block added
*/
int getFreeBlock(byte[] header) throws IOException {
int targetSize = RecordStoreUtil.
calculateBlockSize(RecordStoreUtil.getInt(header, 4));
int currentId = 0;
int currentOffset = AbstractRecordStoreImpl.DB_HEADER_SIZE;
int currentSize = 0;
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
"getFreeBlock recordId = " +
RecordStoreUtil.getInt(header, 0) +
" numBytes = " + RecordStoreUtil.getInt(header, 4) +
" targetSize = " + targetSize);
}
// search through the data blocks for a free block that is large enough
while (currentOffset < recordStore.getSize()) {
// seek to the next offset
dbFile.seek(currentOffset);
// read the block header
if (dbFile.read(header) !=
AbstractRecordStoreImpl.BLOCK_HEADER_SIZE) {
// did not find the recordId
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 for a free block big enough to hold the data
if (currentId < 0 && currentSize >= targetSize) {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
"found free block at offset " +
currentOffset);
}
// a free block
return currentOffset;
}
// added the block size to the currentOffset
currentOffset += currentSize;
}
return 0;
}
/**
* Updates the index of the given block and its offset.
*
* Called from RecordStoreImpl.compactRecords()
* after the block has been moved.
* Called from RecordStoreImpl.writeBlock()
* after the block has been written.
*
* @param blockOffset the offset in db file to the block to update
* @param header the header of the block to update
*
* @exception java.io.IOException if there is an error
* accessing the db file
*/
void updateBlock(int blockOffset, byte[] header) throws IOException {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
"updateBlock recordId = " +
RecordStoreUtil.getInt(header, 0) +
" numBytes = " + RecordStoreUtil.getInt(header, 4) +
" blockOffset = " + blockOffset);
}
int recordId = RecordStoreUtil.getInt(header, 0);
if (null != recordIdOffsets) {
recordIdOffsets.setElementAt(blockOffset, recordId);
}
}
/**
* Removes the given block from the index.
*
* Called from RecordStoreUtil.compactRecords() when a free block
* is removed.
*
* @param blockOffset the offset in db file to the block to remove
* @param header the header of the block to remove
*
* @exception IOException if there is an error accessing the db file
*/
void removeBlock(int blockOffset, byte[] header) throws IOException {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
"removeBlock recordId = " +
RecordStoreUtil.getInt(header, 0) +
" numBytes = " + RecordStoreUtil.getInt(header, 4) +
" blockOffset = " + blockOffset);
}
// blocks get moved, LastSeenOffset may point into the middle
// of a record.
// In principle, all moved blocks will notify us via UpdateBlock(),
// so LastSeenOffset=min(LastSeenOffset,blockOffset) should be ok.
if (null != recordIdOffsets) {
recordIdOffsets.LastSeenOffset = recordIdOffsets.NO_OFFSET;
}
}
/**
* The record is deleted from the record store index.
*
* Called from RecordStoreImpl.deleteRecord(int recordId)
*
* @param recordId the ID of the record index to delete
*
* @exception java.io.IOException if there is an error
* accessing the db index
*/
void deleteRecordIndex(int recordId) throws IOException {
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_RMS,
"deleteRecordIndex(" + recordId + ")");
}
if (null != recordIdOffsets) {
recordIdOffsets.removeElementAt(recordId);
}
}
}
|