com.sleepycat.bind.serial.StoredClassCatalog Maven / Gradle / Ivy
The newest version!
/*-
* Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle Berkeley
* DB Java Edition made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle Berkeley DB Java Edition for a copy of the
* license and additional information.
*/
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.je.Transaction;
import com.sleepycat.util.RuntimeExceptionWrapper;
import com.sleepycat.util.UtfOps;
/**
* A ClassCatalog
that is stored in a Database
.
*
* A single StoredClassCatalog
object is normally used along
* with a set of databases that stored serialized objects.
*
* @author Mark Hayes
*
* @see Class Evolution
*/
public class StoredClassCatalog implements ClassCatalog {
/*
* Record types ([key] [data]):
*
* [0] [next class ID]
* [1 / class ID] [ObjectStreamClass (class format)]
* [2 / class name] [ClassInfo (has 8 byte class ID)]
*/
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;
private boolean txnMode;
/**
* 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 database an open database to use as the class catalog. It must
* be a BTREE database and must not allow duplicates.
*
* @throws DatabaseException if an error occurs accessing the database.
*
* @throws IllegalArgumentException if 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 = (DbCompat.getInitializeLocking(envConfig) ||
envConfig.getTransactional()) ? LockMode.RMW
: LockMode.DEFAULT;
cdbMode = DbCompat.getInitializeCDB(envConfig);
txnMode = dbConfig.getTransactional();
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.");
}
/*
* Create the class format and class info maps. Note that these are not
* synchronized, and therefore the methods that use them are
* synchronized.
*/
classMap = new HashMap();
formatMap = new HashMap();
DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY);
DatabaseEntry data = new DatabaseEntry();
if (dbConfig.getReadOnly()) {
/* Check that the class ID record exists. */
OperationStatus status = db.get(null, key, data, null);
if (status != OperationStatus.SUCCESS) {
throw DbCompat.unexpectedState
("A read-only catalog database may not be empty");
}
} else {
/* Add the initial class ID record if it doesn't exist. */
data.setData(new byte[1]); // zero ID
/*
* Query the record before writing it to the database, to avoid
* ReplicaWriteException while opening a StoredClassCatalog on the
* replicas.
*/
OperationStatus status = db.get(null, key, data, null);
if (status == OperationStatus.NOTFOUND) {
db.putNoOverwrite(null, key, data);
}
}
}
// javadoc is inherited
public synchronized void close()
throws DatabaseException {
if (db != null) {
db.close();
}
db = null;
formatMap = null;
classMap = null;
}
// javadoc is inherited
public synchronized byte[] getClassID(ObjectStreamClass classFormat)
throws DatabaseException, ClassNotFoundException {
ClassInfo classInfo = getClassInfo(classFormat);
return classInfo.getClassID();
}
// javadoc is inherited
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 {
/* First check the map and, if found, add class info to the map. */
BigInteger classIDObj = new BigInteger(classID);
ObjectStreamClass classFormat = formatMap.get(classIDObj);
if (classFormat == null) {
/* Make the class format key. */
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);
/* Read the class format. */
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 RuntimeExceptionWrapper.wrapIfNeeded(e);
}
/* Update the class format map. */
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 {
/*
* First check for a cached copy of the class info, which if
* present always contains the class format object.
*/
String className = classFormat.getName();
ClassInfo classInfo = classMap.get(className);
if (classInfo != null) {
return classInfo;
} else {
/* Make class info key. */
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);
/* Read class info. */
DatabaseEntry data = new DatabaseEntry();
OperationStatus status = db.get(null, key, data, LockMode.DEFAULT);
if (status != OperationStatus.SUCCESS) {
/*
* Not found in the database, write class info and class
* format.
*/
classInfo = putClassInfo(new ClassInfo(), className, key,
classFormat);
} else {
/*
* Read class info to get the class format key, then read class
* format.
*/
classInfo = new ClassInfo(data);
DatabaseEntry formatData = new DatabaseEntry();
ObjectStreamClass storedClassFormat =
getClassFormat(classInfo.getClassID(), formatData);
/*
* Compare the stored class format to the current class format,
* and if they are different then generate a new class ID.
*/
if (!areClassFormatsEqual(storedClassFormat,
getBytes(formatData),
classFormat)) {
classInfo = putClassInfo(classInfo, className, key,
classFormat);
}
/* Update the class info map. */
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 {
/* An intent-to-write cursor is needed for CDB. */
CursorConfig cursorConfig = null;
if (cdbMode) {
cursorConfig = new CursorConfig();
DbCompat.setWriteCursor(cursorConfig, true);
}
Cursor cursor = null;
Transaction txn = null;
try {
if (txnMode) {
txn = db.getEnvironment().beginTransaction(null, null);
}
cursor = db.openCursor(txn, cursorConfig);
/* Get the current class ID. */
DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY);
DatabaseEntry data = new DatabaseEntry();
OperationStatus status = cursor.getSearchKey(key, data,
writeLockMode);
if (status != OperationStatus.SUCCESS) {
throw DbCompat.unexpectedState("Class ID not initialized");
}
byte[] idBytes = getBytes(data);
/* Increment the ID by one and write the updated record. */
idBytes = incrementID(idBytes);
data.setData(idBytes);
cursor.put(key, data);
/*
* Write the new class format record whose key is the ID just
* assigned.
*/
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 RuntimeExceptionWrapper.wrapIfNeeded(e);
}
data.setData(baos.toByteArray());
cursor.put(key, data);
/*
* Write the new class info record, using the key passed in; this
* is done last so that a reader who gets the class info record
* first will always find the corresponding class format record.
*/
classInfo.setClassID(idBytes);
classInfo.toDbt(data);
cursor.put(classKey, data);
/*
* Update the maps before closing the cursor, so that the cursor
* lock prevents other writers from duplicating this entry.
*/
classInfo.setClassFormat(classFormat);
classMap.put(className, classInfo);
formatMap.put(new BigInteger(idBytes), classFormat);
return classInfo;
} finally {
if (cursor != null) {
cursor.close();
}
if (txn != null) {
txn.commit();
}
}
}
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 {
static final long serialVersionUID = 3845446969989650562L;
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) { // using cached format1 object
format1Bytes = getObjectBytes(format1);
}
byte[] format2Bytes = getObjectBytes(format2);
return java.util.Arrays.equals(format2Bytes, format1Bytes);
} catch (IOException e) { return false; }
}
/*
* We can return the same byte[] for 0 length arrays.
*/
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();
}
/**
* For BDB JE, returns the ClassLoader property of the catalog database
* environment. This ensures that the Environment's ClassLoader property
* is used for loading all user-supplied classes.
*
* For BDB, this method returns null because no Environment ClassLoader
* property is available. This method may be overridden to return a
* ClassLoader.
*/
public ClassLoader getClassLoader() {
try {
return DbCompat.getClassLoader(db.getEnvironment());
} catch (DatabaseException e) {
/*
* DatabaseException is declared to be thrown by getEnvironment in
* DB (not JE), but this should never happen in practice.
*/
throw new RuntimeException(e);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy