package com.sleepycat.bind.serial;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.HashMap;
import com.sleepycat.compat.DbCompat;
import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.util.RuntimeExceptionWrapper;
import com.sleepycat.util.UtfOps;
import de.ovgu.cide.jakutil.*;
/**
* A <code>ClassCatalog</code> that is stored in a <code>Database</code>.
* <p>
* A single <code>StoredClassCatalog</code> object is normally used along with
* a set of databases that stored serialized objects.
* </p>
* @author Mark Hayes
*/
public class StoredClassCatalog implements ClassCatalog {
private static final byte REC_LAST_CLASS_ID=(byte)0;
private static final byte REC_CLASS_FORMAT=(byte)1;
private static final byte REC_CLASS_INFO=(byte)2;
private static final byte[] LAST_CLASS_ID_KEY={REC_LAST_CLASS_ID};
private Database db;
private HashMap classMap;
private HashMap formatMap;
private LockMode writeLockMode;
private boolean cdbMode;
/**
* Creates a catalog based on a given database. To save resources, only a
* single catalog object should be used for each unique catalog database.
* @param databasean open database to use as the class catalog. It must be a
* BTREE database and must not allow duplicates.
* @throws DatabaseExceptionif an error occurs accessing the database.
* @throws IllegalArgumentExceptionif the database is not a BTREE database or if it configured
* to allow duplicates.
*/
public StoredClassCatalog( Database database) throws DatabaseException, IllegalArgumentException {
db=database;
DatabaseConfig dbConfig=db.getConfig();
EnvironmentConfig envConfig=db.getEnvironment().getConfig();
writeLockMode=hook_getLockMode(envConfig);
cdbMode=DbCompat.getInitializeCDB(envConfig);
if (!DbCompat.isTypeBtree(dbConfig)) {
throw new IllegalArgumentException("The class catalog must be a BTREE database.");
}
if (DbCompat.getSortedDuplicates(dbConfig) || DbCompat.getUnsortedDuplicates(dbConfig)) {
throw new IllegalArgumentException("The class catalog database must not allow duplicates.");
}
classMap=new HashMap();
formatMap=new HashMap();
DatabaseEntry key=new DatabaseEntry(LAST_CLASS_ID_KEY);
DatabaseEntry data=new DatabaseEntry();
if (dbConfig.getReadOnly()) {
OperationStatus status=db.get(null,key,data,null);
if (status != OperationStatus.SUCCESS) {
throw new IllegalStateException("A read-only catalog database may not be empty");
}
}
else {
data.setData(new byte[1]);
db.putNoOverwrite(null,key,data);
}
}
private LockMode hook_getLockMode( EnvironmentConfig envConfig) throws DatabaseException {
return (DbCompat.getInitializeLocking(envConfig)) ? LockMode.RMW : LockMode.DEFAULT;
}
public synchronized void close() throws DatabaseException {
if (db != null) {
db.close();
}
db=null;
formatMap=null;
classMap=null;
}
public synchronized byte[] getClassID( ObjectStreamClass classFormat) throws DatabaseException, ClassNotFoundException {
ClassInfo classInfo=getClassInfo(classFormat);
return classInfo.getClassID();
}
public synchronized ObjectStreamClass getClassFormat( byte[] classID) throws DatabaseException, ClassNotFoundException {
return getClassFormat(classID,new DatabaseEntry());
}
/**
* Internal function for getting the class format. Allows passing the
* DatabaseEntry object for the data, so the bytes of the class format can
* be examined afterwards.
*/
private ObjectStreamClass getClassFormat( byte[] classID, DatabaseEntry data) throws DatabaseException, ClassNotFoundException {
BigInteger classIDObj=new BigInteger(classID);
ObjectStreamClass classFormat=(ObjectStreamClass)formatMap.get(classIDObj);
if (classFormat == null) {
byte[] keyBytes=new byte[classID.length + 1];
keyBytes[0]=REC_CLASS_FORMAT;
System.arraycopy(classID,0,keyBytes,1,classID.length);
DatabaseEntry key=new DatabaseEntry(keyBytes);
OperationStatus status=db.get(null,key,data,LockMode.DEFAULT);
if (status != OperationStatus.SUCCESS) {
throw new ClassNotFoundException("Catalog class ID not found");
}
try {
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(data.getData(),data.getOffset(),data.getSize()));
classFormat=(ObjectStreamClass)ois.readObject();
}
catch ( IOException e) {
throw new RuntimeExceptionWrapper(e);
}
formatMap.put(classIDObj,classFormat);
}
return classFormat;
}
/**
* Get the ClassInfo for a given class name, adding it and its
* ObjectStreamClass to the database if they are not already present, and
* caching both of them using the class info and class format maps. When a
* class is first loaded from the database, the stored ObjectStreamClass is
* compared to the current ObjectStreamClass loaded by the Java class
* loader; if they are different, a new class ID is assigned for the current
* format.
*/
private ClassInfo getClassInfo( ObjectStreamClass classFormat) throws DatabaseException, ClassNotFoundException {
String className=classFormat.getName();
ClassInfo classInfo=(ClassInfo)classMap.get(className);
if (classInfo != null) {
return classInfo;
}
else {
char[] nameChars=className.toCharArray();
byte[] keyBytes=new byte[1 + UtfOps.getByteLength(nameChars)];
keyBytes[0]=REC_CLASS_INFO;
UtfOps.charsToBytes(nameChars,0,keyBytes,1,nameChars.length);
DatabaseEntry key=new DatabaseEntry(keyBytes);
DatabaseEntry data=new DatabaseEntry();
OperationStatus status=db.get(null,key,data,LockMode.DEFAULT);
if (status != OperationStatus.SUCCESS) {
classInfo=putClassInfo(new ClassInfo(),className,key,classFormat);
}
else {
classInfo=new ClassInfo(data);
DatabaseEntry formatData=new DatabaseEntry();
ObjectStreamClass storedClassFormat=getClassFormat(classInfo.getClassID(),formatData);
if (!areClassFormatsEqual(storedClassFormat,getBytes(formatData),classFormat)) {
classInfo=putClassInfo(classInfo,className,key,classFormat);
}
classInfo.setClassFormat(classFormat);
classMap.put(className,classInfo);
}
}
return classInfo;
}
/**
* Assign a new class ID (increment the current ID record), write the
* ObjectStreamClass record for this new ID, and update the ClassInfo record
* with the new ID also. The ClassInfo passed as an argument is the one to
* be updated.
*/
private ClassInfo putClassInfo( ClassInfo classInfo, String className, DatabaseEntry classKey, ObjectStreamClass classFormat) throws DatabaseException, ClassNotFoundException {
CursorConfig cursorConfig=null;
if (cdbMode) {
cursorConfig=new CursorConfig();
DbCompat.setWriteCursor(cursorConfig,true);
}
Cursor cursor=null;
try {
cursor=db.openCursor(null,cursorConfig);
DatabaseEntry key=new DatabaseEntry(LAST_CLASS_ID_KEY);
DatabaseEntry data=new DatabaseEntry();
OperationStatus status=cursor.getSearchKey(key,data,writeLockMode);
if (status != OperationStatus.SUCCESS) {
throw new IllegalStateException("Class ID not initialized");
}
byte[] idBytes=getBytes(data);
idBytes=incrementID(idBytes);
data.setData(idBytes);
cursor.put(key,data);
byte[] keyBytes=new byte[1 + idBytes.length];
keyBytes[0]=REC_CLASS_FORMAT;
System.arraycopy(idBytes,0,keyBytes,1,idBytes.length);
key.setData(keyBytes);
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos;
try {
oos=new ObjectOutputStream(baos);
oos.writeObject(classFormat);
}
catch ( IOException e) {
throw new RuntimeExceptionWrapper(e);
}
data.setData(baos.toByteArray());
cursor.put(key,data);
classInfo.setClassID(idBytes);
classInfo.toDbt(data);
cursor.put(classKey,data);
classInfo.setClassFormat(classFormat);
classMap.put(className,classInfo);
formatMap.put(new BigInteger(idBytes),classFormat);
return classInfo;
}
finally {
if (cursor != null) {
cursor.close();
}
hook_commitTransaction();
}
}
private void hook_commitTransaction() throws DatabaseException {
}
private static byte[] incrementID( byte[] key){
BigInteger id=new BigInteger(key);
id=id.add(BigInteger.valueOf(1));
return id.toByteArray();
}
/**
* Holds the class format key for a class, maintains a reference to the
* ObjectStreamClass. Other fields can be added when we need to store more
* information per class.
*/
private static class ClassInfo implements Serializable {
private byte[] classID;
private transient ObjectStreamClass classFormat;
ClassInfo(){
}
ClassInfo( DatabaseEntry dbt){
byte[] data=dbt.getData();
int len=data[0];
classID=new byte[len];
System.arraycopy(data,1,classID,0,len);
}
void toDbt( DatabaseEntry dbt){
byte[] data=new byte[1 + classID.length];
data[0]=(byte)classID.length;
System.arraycopy(classID,0,data,1,classID.length);
dbt.setData(data);
}
void setClassID( byte[] classID){
this.classID=classID;
}
byte[] getClassID(){
return classID;
}
ObjectStreamClass getClassFormat(){
return classFormat;
}
void setClassFormat( ObjectStreamClass classFormat){
this.classFormat=classFormat;
}
}
/**
* Return whether two class formats are equal. This determines whether a new
* class format is needed for an object being serialized. Formats must be
* identical in all respects, or a new format is needed.
*/
private static boolean areClassFormatsEqual( ObjectStreamClass format1, byte[] format1Bytes, ObjectStreamClass format2){
try {
if (format1Bytes == null) {
format1Bytes=getObjectBytes(format1);
}
byte[] format2Bytes=getObjectBytes(format2);
return java.util.Arrays.equals(format2Bytes,format1Bytes);
}
catch ( IOException e) {
return false;
}
}
private static byte[] ZERO_LENGTH_BYTE_ARRAY=new byte[0];
private static byte[] getBytes( DatabaseEntry dbt){
byte[] b=dbt.getData();
if (b == null) {
return null;
}
if (dbt.getOffset() == 0 && b.length == dbt.getSize()) {
return b;
}
int len=dbt.getSize();
if (len == 0) {
return ZERO_LENGTH_BYTE_ARRAY;
}
else {
byte[] t=new byte[len];
System.arraycopy(b,dbt.getOffset(),t,0,t.length);
return t;
}
}
private static byte[] getObjectBytes( Object o) throws IOException {
ByteArrayOutputStream baos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(baos);
oos.writeObject(o);
return baos.toByteArray();
}
}