package it.yup.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import java.util.Vector;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
import javax.microedition.rms.RecordStoreFullException;
import javax.microedition.rms.RecordStoreNotFoundException;
//#mdebug
//@
//@import javax.microedition.rms.InvalidRecordIDException;
//@
//@import javax.microedition.rms.RecordStoreException;
//@import javax.microedition.rms.RecordStoreNotOpenException;
//@
//#enddebug
public class RMSIndex {
private final static int CHUNK_MAXSIZE = 50;
/** chunk_id -> chunk */
private Vector chunk_index = new Vector();
private Vector current_chunk = null;
private Item current_index = null;
/*
* The max length that can be used for a record
*/
private int recordMaxLength = 64535;
/*
*
*/
class RecordType {
static final byte CHUNK_INDEX = 0;
static final byte CHUNK = 1;
static final byte DATA_RECORD = 2;
static final byte SPLITTED_HEAD = 3;
static final byte SPLITTED_RECORD = 4;
}
class Item {
byte key[];
int id;
// this field is multiplexed:
// - used in the chunk_index to store the number of items present in the pointed chunk.
// - and to contain the type of the record (DATA_RECORD or SPLITTED_HEAD) in the other case
short num = 0;
}
private class DefaultUTF8Comparator implements Comparator {
public int compare(byte[] a, byte[] b) {
try {
// XXX change in order to support phones without utf-8
return (new String(a, "utf-8"))
.compareTo(new String(b, "utf-8"));
} catch (UnsupportedEncodingException e) {
return 0;
}
}
}
public interface Comparator {
public int compare(byte[] a, byte[] b);
}
public static boolean rmExist(String dbName) {
RecordStore tempRs = null;
try {
tempRs = RecordStore.openRecordStore(dbName, false);
return true;
} catch (RecordStoreFullException e) {
return true;
} catch (RecordStoreNotFoundException e) {
return false;
} catch (RecordStoreException e) {
return true;
} catch (Exception e) {
return false;
} finally {
if (tempRs != null) {
try {
tempRs.closeRecordStore();
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println("In rmExists" + e.getMessage()
//@ + e.getClass());
// #enddebug
}
}
}
}
private class KeyEnumeration implements Enumeration {
int t_i; // top index
int c_i; // chunk index
Vector current_chunk = null;
Item next = null;
public KeyEnumeration() {
if (chunk_index.size() > 0) {
t_i = -1;
c_i = -1;
// this forces to load the first item
current_chunk = new Vector();
next_item();
}
}
public boolean hasMoreElements() {
return next != null;
}
public Object nextElement() {
if (next == null) { throw new NoSuchElementException(); }
byte data[] = next.key;
next_item();
return data;
}
private void next_item() {
c_i++;
next = null;
while (true) {
if (c_i < current_chunk.size()) {
// load from current chunk
next = (Item) current_chunk.elementAt(c_i);
break;
} else {
// find a new chunk
t_i++;
if (t_i < chunk_index.size()) {
c_i = 0;
Item item = (Item) chunk_index.elementAt(t_i);
try {
current_chunk = loadChunk(item.id);
} catch (Exception e) {
throw new NoSuchElementException();
}
} else {
break;
}
}
}
}
}
private Comparator comparator;
/*
* The Record Store
*/
private RecordStore rs;
/*
* The name of Record Store
*/
private String name;
private void deleteSplittedRecords(int id) {
try {
byte[] oldBytes = rs.getRecord(id);
DataInputStream is = new DataInputStream(new ByteArrayInputStream(
oldBytes));
byte oldType = is.readByte();
short oldKeyLength = is.readShort();
byte[] oldKey = new byte[oldKeyLength];
is.read(oldKey, 0, oldKeyLength);
short oldChunkSize = is.readShort();
int ithRid = 0;
for (int i = 0; i < oldChunkSize; i++) {
ithRid = is.readInt();
rs.deleteRecord(ithRid);
}
} catch (Exception e) {
// #mdebug
//@ // XXX clean dirty records! how ??!?
//@ e.printStackTrace();
//@ System.out.println("In deleting splitted records" + e.getMessage()
//@ + e.getClass());
// #enddebug
}
}
private void join(Item firstItem, Item secondItem) {
// #mdebug
//@ System.out.println("join");
// #enddebug
try {
Vector firstChunk, secondChunk;
if (firstItem == current_index) {
firstChunk = current_chunk;
secondChunk = loadChunk(secondItem.id);
} else {
firstChunk = loadChunk(firstItem.id);
secondChunk = current_chunk;
}
chunk_index.removeElement(secondItem);
Enumeration en = secondChunk.elements();
while (en.hasMoreElements()) {
firstChunk.addElement(en.nextElement());
}
saveChunk(firstItem.id, firstChunk);
current_chunk = firstChunk;
current_index = firstItem;
current_index.num = (short) current_chunk.size();
// remove the unused chunk
rs.deleteRecord(secondItem.id);
saveChunk(1, chunk_index);
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
}
}
/**
* Get an item inside a chunk
* @param chunk
* @param key
* @param exact
* @return
*/
private Item get_item(Vector chunk, byte key[], boolean exact) {
int last = get_offset(chunk, key, exact);
if (last != -1) {
return (Item) chunk.elementAt(last);
} else {
return null;
}
}
private void sorted_insert(Vector chunk, Item ii) {
int pos = get_offset(chunk, ii.key, false);
if (pos == -1) {
chunk.addElement(ii);
} else {
Item ij = (Item) chunk.elementAt(pos);
if (comparator.compare(ii.key, ij.key) > 0) {
chunk.insertElementAt(ii, pos + 1);
} else {
chunk.insertElementAt(ii, pos);
}
}
}
/**
* Get the offset of an item within a sector
* @param chunk
* @param key
* @param exact
* @return
*/
private int get_offset(Vector chunk, byte key[], boolean exact) {
int min = 0;
int max = chunk.size();
int last = -1;
while (max > min && (last != min + (max - min) / 2)) {
last = min + (max - min) / 2;
Item item = (Item) chunk.elementAt(last);
int res = comparator.compare(item.key, key);
if (res == 0) {
return last;
} else {
if (res < 0) {
min = last;
} else {
max = last;
}
}
}
return exact ? -1 : last;
}
/**
* Load a sector from the record store
* @param index
* @return
* @throws Exception
*/
private Vector loadChunk(int index) throws Exception {
Vector chunk = new Vector();
try {
byte buf[] = rs.getRecord(index);
DataInputStream is = new DataInputStream(new ByteArrayInputStream(
buf));
byte type = is.readByte();
while (is.available() > 0) {
Item item = new Item();
short len = is.readShort();
item.key = new byte[len];
is.readFully(item.key);
item.id = is.readInt();
item.num = is.readShort();
chunk.addElement(item);
}
return chunk;
} catch (Exception e) {
// TODO Auto-generated catch block
// #mdebug
//@ e.printStackTrace();
//@ System.out.println("In loading a chuck " + e.getMessage()
//@ + e.getClass());
// #enddebug
}
return chunk;
}
/**
* Save a sector to the record store
* @param rid
* @param v
* @return
* @throws Exception
*/
private int saveChunk(int rid, Vector v) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(baos);
Enumeration en = v.elements();
while (en.hasMoreElements()) {
Item ii = (Item) en.nextElement();
os.writeShort(ii.key.length);
os.write(ii.key);
os.writeInt(ii.id);
os.writeShort(ii.num);
}
byte buf[] = baos.toByteArray();
// if (rid >= 0) {
// rs.setRecord(rid, buf, 0, buf.length);
// } else {
// rid = rs.addRecord(buf, 0, buf.length);
// }
if (rid == 1) {
physicalWrite(1, null, buf, 0, buf.length, RecordType.CHUNK_INDEX,
null);
} else {
rid = physicalWrite(rid, null, buf, 0, buf.length,
RecordType.CHUNK, null);
}
return rid;
}
private int physicalWrite(int recordId, byte key[], byte data[], int start,
int len, byte type, Item item) {
// I write in this order
// -----------------------------------------------------------------------------
// | headerBuf | keyLength | keyBuf (or ordinal in split ) | dataBuf |
// -----------------------------------------------------------------------------
byte headerBuf = type;
byte[] keyBuf = key;
short keyLength = (short) (keyBuf != null ? keyBuf.length : -1);
byte[] dataBuf = null;
if (type == RecordType.CHUNK
|| type == RecordType.CHUNK_INDEX
|| type == RecordType.SPLITTED_HEAD
|| type == RecordType.SPLITTED_RECORD
|| (type == RecordType.DATA_RECORD && data.length <= this.recordMaxLength)) {
dataBuf = data;
}
if (type == RecordType.SPLITTED_RECORD) {
keyLength = -1;
}
// first delete the previous entry;
// this or the previous one could be split
if (recordId > 0 && type == RecordType.DATA_RECORD && item != null
&& item.num == RecordType.SPLITTED_HEAD) {
// only the "tail" is cut the head is used for this to update
deleteSplittedRecords(recordId);
}
if (type == RecordType.DATA_RECORD
&& data.length > this.recordMaxLength) {
Vector mapChunks = new Vector();
int offset = recordMaxLength;
short index = 0;
while (offset < data.length) {
int ithRid = physicalWrite(-1, new byte[] {
(byte) (index >> 8), (byte) (index & 0xff) }, data,
offset,
Math.min(recordMaxLength, data.length - offset),
RecordType.SPLITTED_RECORD, null);
mapChunks.addElement(new Integer(ithRid));
offset += recordMaxLength;
}
try {
// serialize mapChunks
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(baos);
os.writeShort(mapChunks.size());
Enumeration en = mapChunks.elements();
while (en.hasMoreElements()) {
Integer ithRid = (Integer) en.nextElement();
os.writeInt(ithRid.intValue());
}
os.write(data, 0, recordMaxLength);
dataBuf = baos.toByteArray();
headerBuf = RecordType.SPLITTED_HEAD;
len = dataBuf.length;
} catch (Exception e) {
// #mdebug
//@ // XXX clean dirty records! how ??!?
//@ e.printStackTrace();
//@ System.out.println("Error in serialize split chunks "
//@ + e.getMessage() + e.getClass());
// #enddebug
}
}
// final write
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(baos);
os.write(headerBuf);
if (keyLength > 0) os.writeShort(keyLength);
if (keyBuf != null) os.write(keyBuf);
os.write(dataBuf, start, len);
byte[] finalBuf = baos.toByteArray();
if (recordId >= 0) rs.setRecord(recordId, finalBuf, 0,
finalBuf.length);
else
recordId = rs.addRecord(finalBuf, 0, finalBuf.length);
// save the type of record in item
if (item != null) item.num = headerBuf;
return recordId;
} catch (Exception e) {
// #mdebug
//@ // XXX clean dirty records! how ??!?
//@ e.printStackTrace();
//@ System.out.println("Error in final write " + e.getMessage()
//@ + e.getClass());
// #enddebug
}
return -1;
}
private byte[] physicalRead(Item ii) {
try {
byte[] buf = rs.getRecord(ii.id);
DataInputStream is = new DataInputStream(new ByteArrayInputStream(
buf));
byte type = is.readByte();
ii.num = type;
byte[] realData = null;
short keyLen;
byte[] key;
switch (type) {
case RecordType.DATA_RECORD:
keyLen = is.readShort();
key = new byte[keyLen];
is.read(key);
realData = new byte[is.available()];
is.read(realData);
return realData;
case RecordType.SPLITTED_HEAD:
keyLen = is.readShort();
key = new byte[keyLen];
is.read(key, 0, keyLen);
short chunkSize = is.readShort();
int ithRid = 0;
int rids[] = new int[chunkSize];
for (int i = 0; i < chunkSize; i++)
rids[i] = is.readInt();
byte headBytes[] = new byte[is.available()];
is.read(headBytes);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream os = new DataOutputStream(baos);
os.write(headBytes);
for (int i = 0; i < chunkSize; i++) {
byte[] ithRecord = rs.getRecord(rids[i]);
byte ithType = ithRecord[0];
short ordinal = (short) (ithRecord[1] << 8 + ithRecord[2]);
os.write(ithRecord, 3, ithRecord.length - 3);
}
realData = baos.toByteArray();
return realData;
default:
break;
}
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
}
return null;
}
public int[] getSizes() {
try {
return new int[] { this.rs.getSize(), this.rs.getSizeAvailable(),
this.rs.getNumRecords(), this.rs.getRecordSize(1) };
} catch (Exception e) {
// TODO Auto-generated catch block
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
}
return new int[] { 0, 0 };
}
/**
* Store an item
* @param key
* @param data
*/
public synchronized void store(byte key[], byte data[]) {
try {
Item ci = get_item(chunk_index, key, false);
if (ci == null) {
// first record setup all
// create a new chunk and add the record
current_chunk = new Vector();
Item ii = new Item();
//ii.id = rs.addRecord(data, 0, data.length);
ii.id = physicalWrite(-1, key, data, 0, data.length,
RecordType.DATA_RECORD, ii);
ii.key = key;
current_chunk.addElement(ii);
// Now save the index of the just created chunk
current_index = new Item();
current_index.key = key;
// save the chunk and get its record
current_index.id = saveChunk(-1, current_chunk);
current_index.num = (short) current_chunk.size();
chunk_index.addElement(current_index);
// save the index
saveChunk(1, chunk_index);
} else {
if (ci != current_index) {
current_chunk = loadChunk(ci.id);
current_index = ci;
current_index.num = (short) current_chunk.size();
}
Item ii = get_item(current_chunk, key, true);
if (ii != null) {
// item found, simple update
//rs.setRecord(ii.id, data, 0, data.length);
// if the file was split and now not (or on the contrary)
// I need to save the current_chunk!!
short oldNum = ii.num;
physicalWrite(ii.id, key, data, 0, data.length,
RecordType.DATA_RECORD, ii);
if (ii.num != oldNum) {
saveChunk(ci.id, current_chunk);
}
} else {
// add the new record
ii = new Item();
//ii.id = rs.addRecord(data, 0, data.length);
ii.id = physicalWrite(-1, key, data, 0, data.length,
RecordType.DATA_RECORD, ii);
ii.key = key;
// insert the record into the chunk
sorted_insert(current_chunk, ii);
if (current_chunk.size() <= CHUNK_MAXSIZE) {
// just save
saveChunk(ci.id, current_chunk);
ci.num = (short) current_chunk.size();
if (comparator.compare(ii.key, ci.key) < 0) {
ci.key = ii.key;
saveChunk(1, chunk_index);
}
} else {
// split the chunk before saving
// #mdebug
//@ System.out.println("split");
// #enddebug
Vector new_chunk = new Vector();
int end = current_chunk.size();
for (int i = current_chunk.size() / 2; i < end; i++) {
new_chunk.addElement(current_chunk.elementAt(i));
}
for (int i = current_chunk.size() / 2; i < end; i++) {
current_chunk
.removeElementAt(current_chunk.size() - 1);
}
saveChunk(ci.id, current_chunk);
ci.num = (short) current_chunk.size();
Item new_index = new Item();
new_index.id = saveChunk(-1, new_chunk);
new_index.key = ((Item) new_chunk.elementAt(0)).key;
new_index.num = (short) new_chunk.size();
sorted_insert(chunk_index, new_index);
saveChunk(1, chunk_index);
// #mdebug
//@ for (int i = 0; i < chunk_index.size(); i++) {
//@ System.out.println("~"
//@ + i
//@ + "~:"
//@ + new String(((Item) chunk_index
//@ .elementAt(i)).key));
//@ }
// #enddebug
}
}
}
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
}
}
public synchronized byte[] load(byte key[]) {
try {
Item ci = get_item(chunk_index, key, false);
if (ci != current_index) {
current_chunk = loadChunk(ci.id);
current_index = ci;
current_index.num = (short) current_chunk.size();
}
if (current_chunk==null)
return null;
Item ii = get_item(current_chunk, key, true);
if (ii != null) {
return physicalRead(ii);
} else {
return null;
}
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
}
return null;
}
public synchronized void delete(byte[] key) {
try {
Item ci = get_item(chunk_index, key, false);
if (ci != current_index) {
current_chunk = loadChunk(ci.id);
current_index = ci;
current_index.num = (short) current_chunk.size();
}
int pos = get_offset(current_chunk, key, true);
if (pos >= 0) {
Item ii = (Item) current_chunk.elementAt(pos);
if (ii.num == RecordType.SPLITTED_HEAD) {
deleteSplittedRecords(ii.id);
}
rs.deleteRecord(ii.id);
current_chunk.removeElementAt(pos);
if (current_chunk.size() == 0) {
// remove the chunk
rs.deleteRecord(ci.id);
chunk_index.removeElement(ci);
saveChunk(1, chunk_index);
current_index = null;
} else {
if (pos == 0) {
ci.key = ((Item) current_chunk.elementAt(0)).key;
saveChunk(1, chunk_index);
}
saveChunk(ci.id, current_chunk);
ci.num = (short) current_chunk.size();
}
// check join
if (current_index != null) {
if (current_index != chunk_index.elementAt(0)) {
Item prev_index = (Item) chunk_index
.elementAt(chunk_index.indexOf(current_index) - 1);
if (current_index.num + prev_index.num <= RMSIndex.CHUNK_MAXSIZE) {
join(prev_index, current_index);
}
} else if (current_index != chunk_index
.elementAt(chunk_index.size() - 1)) {
Item next_index = (Item) chunk_index
.elementAt(chunk_index.indexOf(current_index) + 1);
if (current_index.num + next_index.num <= RMSIndex.CHUNK_MAXSIZE) {
join(current_index, next_index);
}
}
}
}
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
}
}
public boolean close() {
try {
this.rs.closeRecordStore();
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
return false;
}
return true;
}
public Enumeration keys() {
return new KeyEnumeration();
}
public RMSIndex(String name) {
this.name = name;
comparator = new DefaultUTF8Comparator();
//init(name);
}
public RMSIndex(String name, int recordMaxLength) {
this(name);
this.recordMaxLength = recordMaxLength;
}
public boolean open() {
try {
rs = RecordStore.openRecordStore(name, true);
if (rs.getNumRecords() == 0) {
// first time we open the store, reserve an area for the chunk index
//rs.addRecord(new byte[] {}, 0, 0);
physicalWrite(-1, null, new byte[] {}, 0, 0,
RecordType.CHUNK_INDEX, null);
} else {
// load the chunk index
chunk_index = loadChunk(1);
}
} catch (Exception e) {
// #mdebug
//@ e.printStackTrace();
//@ System.out.println(e.getMessage() + e.getClass());
// #enddebug
return false;
}
return true;
}
}