001: /*-
002: * See the file LICENSE for redistribution information.
003: *
004: * Copyright (c) 2000,2008 Oracle. All rights reserved.
005: *
006: * $Id: StoredClassCatalog.java,v 1.46.2.2 2008/01/07 15:14:05 cwl Exp $
007: */
008:
009: package com.sleepycat.bind.serial;
010:
011: import java.io.ByteArrayInputStream;
012: import java.io.ByteArrayOutputStream;
013: import java.io.IOException;
014: import java.io.ObjectInputStream;
015: import java.io.ObjectOutputStream;
016: import java.io.ObjectStreamClass;
017: import java.io.Serializable;
018: import java.math.BigInteger;
019: import java.util.HashMap;
020:
021: import com.sleepycat.compat.DbCompat;
022: import com.sleepycat.je.Cursor;
023: import com.sleepycat.je.CursorConfig;
024: import com.sleepycat.je.Database;
025: import com.sleepycat.je.DatabaseConfig;
026: import com.sleepycat.je.DatabaseEntry;
027: import com.sleepycat.je.DatabaseException;
028: import com.sleepycat.je.EnvironmentConfig;
029: import com.sleepycat.je.LockMode;
030: import com.sleepycat.je.OperationStatus;
031: import com.sleepycat.je.Transaction;
032: import com.sleepycat.util.RuntimeExceptionWrapper;
033: import com.sleepycat.util.UtfOps;
034:
035: /**
036: * A <code>ClassCatalog</code> that is stored in a <code>Database</code>.
037: *
038: * <p>A single <code>StoredClassCatalog</code> object is normally used along
039: * with a set of databases that stored serialized objects.</p>
040: *
041: * @author Mark Hayes
042: */
043: public class StoredClassCatalog implements ClassCatalog {
044:
045: /*
046: * Record types ([key] [data]):
047: *
048: * [0] [next class ID]
049: * [1 / class ID] [ObjectStreamClass (class format)]
050: * [2 / class name] [ClassInfo (has 8 byte class ID)]
051: */
052: private static final byte REC_LAST_CLASS_ID = (byte) 0;
053: private static final byte REC_CLASS_FORMAT = (byte) 1;
054: private static final byte REC_CLASS_INFO = (byte) 2;
055:
056: private static final byte[] LAST_CLASS_ID_KEY = { REC_LAST_CLASS_ID };
057:
058: private Database db;
059: private HashMap classMap;
060: private HashMap formatMap;
061: private LockMode writeLockMode;
062: private boolean cdbMode;
063: private boolean txnMode;
064:
065: /**
066: * Creates a catalog based on a given database. To save resources, only a
067: * single catalog object should be used for each unique catalog database.
068: *
069: * @param database an open database to use as the class catalog. It must
070: * be a BTREE database and must not allow duplicates.
071: *
072: * @throws DatabaseException if an error occurs accessing the database.
073: *
074: * @throws IllegalArgumentException if the database is not a BTREE database
075: * or if it configured to allow duplicates.
076: */
077: public StoredClassCatalog(Database database)
078: throws DatabaseException, IllegalArgumentException {
079:
080: db = database;
081: DatabaseConfig dbConfig = db.getConfig();
082: EnvironmentConfig envConfig = db.getEnvironment().getConfig();
083:
084: writeLockMode = (DbCompat.getInitializeLocking(envConfig) || envConfig
085: .getTransactional()) ? LockMode.RMW : LockMode.DEFAULT;
086: cdbMode = DbCompat.getInitializeCDB(envConfig);
087: txnMode = dbConfig.getTransactional();
088:
089: if (!DbCompat.isTypeBtree(dbConfig)) {
090: throw new IllegalArgumentException(
091: "The class catalog must be a BTREE database.");
092: }
093: if (DbCompat.getSortedDuplicates(dbConfig)
094: || DbCompat.getUnsortedDuplicates(dbConfig)) {
095: throw new IllegalArgumentException(
096: "The class catalog database must not allow duplicates.");
097: }
098:
099: /*
100: * Create the class format and class info maps. Note that these are not
101: * synchronized, and therefore the methods that use them are
102: * synchronized.
103: */
104: classMap = new HashMap();
105: formatMap = new HashMap();
106:
107: DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY);
108: DatabaseEntry data = new DatabaseEntry();
109: if (dbConfig.getReadOnly()) {
110: /* Check that the class ID record exists. */
111: OperationStatus status = db.get(null, key, data, null);
112: if (status != OperationStatus.SUCCESS) {
113: throw new IllegalStateException(
114: "A read-only catalog database may not be empty");
115: }
116: } else {
117: /* Add the initial class ID record if it doesn't exist. */
118: data.setData(new byte[1]); // zero ID
119: /* Use putNoOverwrite to avoid phantoms. */
120: db.putNoOverwrite(null, key, data);
121: }
122: }
123:
124: // javadoc is inherited
125: public synchronized void close() throws DatabaseException {
126:
127: if (db != null) {
128: db.close();
129: }
130: db = null;
131: formatMap = null;
132: classMap = null;
133: }
134:
135: // javadoc is inherited
136: public synchronized byte[] getClassID(ObjectStreamClass classFormat)
137: throws DatabaseException, ClassNotFoundException {
138:
139: ClassInfo classInfo = getClassInfo(classFormat);
140: return classInfo.getClassID();
141: }
142:
143: // javadoc is inherited
144: public synchronized ObjectStreamClass getClassFormat(byte[] classID)
145: throws DatabaseException, ClassNotFoundException {
146:
147: return getClassFormat(classID, new DatabaseEntry());
148: }
149:
150: /**
151: * Internal function for getting the class format. Allows passing the
152: * DatabaseEntry object for the data, so the bytes of the class format can
153: * be examined afterwards.
154: */
155: private ObjectStreamClass getClassFormat(byte[] classID,
156: DatabaseEntry data) throws DatabaseException,
157: ClassNotFoundException {
158:
159: /* First check the map and, if found, add class info to the map. */
160:
161: BigInteger classIDObj = new BigInteger(classID);
162: ObjectStreamClass classFormat = (ObjectStreamClass) formatMap
163: .get(classIDObj);
164: if (classFormat == null) {
165:
166: /* Make the class format key. */
167:
168: byte[] keyBytes = new byte[classID.length + 1];
169: keyBytes[0] = REC_CLASS_FORMAT;
170: System.arraycopy(classID, 0, keyBytes, 1, classID.length);
171: DatabaseEntry key = new DatabaseEntry(keyBytes);
172:
173: /* Read the class format. */
174:
175: OperationStatus status = db.get(null, key, data,
176: LockMode.DEFAULT);
177: if (status != OperationStatus.SUCCESS) {
178: throw new ClassNotFoundException(
179: "Catalog class ID not found");
180: }
181: try {
182: ObjectInputStream ois = new ObjectInputStream(
183: new ByteArrayInputStream(data.getData(), data
184: .getOffset(), data.getSize()));
185: classFormat = (ObjectStreamClass) ois.readObject();
186: } catch (IOException e) {
187: throw new RuntimeExceptionWrapper(e);
188: }
189:
190: /* Update the class format map. */
191:
192: formatMap.put(classIDObj, classFormat);
193: }
194: return classFormat;
195: }
196:
197: /**
198: * Get the ClassInfo for a given class name, adding it and its
199: * ObjectStreamClass to the database if they are not already present, and
200: * caching both of them using the class info and class format maps. When a
201: * class is first loaded from the database, the stored ObjectStreamClass is
202: * compared to the current ObjectStreamClass loaded by the Java class
203: * loader; if they are different, a new class ID is assigned for the
204: * current format.
205: */
206: private ClassInfo getClassInfo(ObjectStreamClass classFormat)
207: throws DatabaseException, ClassNotFoundException {
208:
209: /*
210: * First check for a cached copy of the class info, which if
211: * present always contains the class format object
212: */
213: String className = classFormat.getName();
214: ClassInfo classInfo = (ClassInfo) classMap.get(className);
215: if (classInfo != null) {
216: return classInfo;
217: } else {
218: /* Make class info key. */
219: char[] nameChars = className.toCharArray();
220: byte[] keyBytes = new byte[1 + UtfOps
221: .getByteLength(nameChars)];
222: keyBytes[0] = REC_CLASS_INFO;
223: UtfOps.charsToBytes(nameChars, 0, keyBytes, 1,
224: nameChars.length);
225: DatabaseEntry key = new DatabaseEntry(keyBytes);
226:
227: /* Read class info. */
228: DatabaseEntry data = new DatabaseEntry();
229: OperationStatus status = db.get(null, key, data,
230: LockMode.DEFAULT);
231: if (status != OperationStatus.SUCCESS) {
232: /*
233: * Not found in the database, write class info and class
234: * format.
235: */
236: classInfo = putClassInfo(new ClassInfo(), className,
237: key, classFormat);
238: } else {
239: /*
240: * Read class info to get the class format key, then read class
241: * format.
242: */
243: classInfo = new ClassInfo(data);
244: DatabaseEntry formatData = new DatabaseEntry();
245: ObjectStreamClass storedClassFormat = getClassFormat(
246: classInfo.getClassID(), formatData);
247:
248: /*
249: * Compare the stored class format to the current class format,
250: * and if they are different then generate a new class ID.
251: */
252: if (!areClassFormatsEqual(storedClassFormat,
253: getBytes(formatData), classFormat)) {
254: classInfo = putClassInfo(classInfo, className, key,
255: classFormat);
256: }
257:
258: /* Update the class info map. */
259: classInfo.setClassFormat(classFormat);
260: classMap.put(className, classInfo);
261: }
262: }
263: return classInfo;
264: }
265:
266: /**
267: * Assign a new class ID (increment the current ID record), write the
268: * ObjectStreamClass record for this new ID, and update the ClassInfo
269: * record with the new ID also. The ClassInfo passed as an argument is the
270: * one to be updated.
271: */
272: private ClassInfo putClassInfo(ClassInfo classInfo,
273: String className, DatabaseEntry classKey,
274: ObjectStreamClass classFormat) throws DatabaseException,
275: ClassNotFoundException {
276:
277: /* An intent-to-write cursor is needed for CDB. */
278: CursorConfig cursorConfig = null;
279: if (cdbMode) {
280: cursorConfig = new CursorConfig();
281: DbCompat.setWriteCursor(cursorConfig, true);
282: }
283: Cursor cursor = null;
284: Transaction txn = null;
285: try {
286: if (txnMode) {
287: txn = db.getEnvironment().beginTransaction(null, null);
288: }
289: cursor = db.openCursor(txn, cursorConfig);
290:
291: /* Get the current class ID. */
292: DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY);
293: DatabaseEntry data = new DatabaseEntry();
294: OperationStatus status = cursor.getSearchKey(key, data,
295: writeLockMode);
296: if (status != OperationStatus.SUCCESS) {
297: throw new IllegalStateException(
298: "Class ID not initialized");
299: }
300: byte[] idBytes = getBytes(data);
301:
302: /* Increment the ID by one and write the updated record. */
303: idBytes = incrementID(idBytes);
304: data.setData(idBytes);
305: cursor.put(key, data);
306:
307: /*
308: * Write the new class format record whose key is the ID just
309: * assigned.
310: */
311: byte[] keyBytes = new byte[1 + idBytes.length];
312: keyBytes[0] = REC_CLASS_FORMAT;
313: System.arraycopy(idBytes, 0, keyBytes, 1, idBytes.length);
314: key.setData(keyBytes);
315:
316: ByteArrayOutputStream baos = new ByteArrayOutputStream();
317: ObjectOutputStream oos;
318: try {
319: oos = new ObjectOutputStream(baos);
320: oos.writeObject(classFormat);
321: } catch (IOException e) {
322: throw new RuntimeExceptionWrapper(e);
323: }
324: data.setData(baos.toByteArray());
325:
326: cursor.put(key, data);
327:
328: /*
329: * Write the new class info record, using the key passed in; this
330: * is done last so that a reader who gets the class info record
331: * first will always find the corresponding class format record.
332: */
333: classInfo.setClassID(idBytes);
334: classInfo.toDbt(data);
335:
336: cursor.put(classKey, data);
337:
338: /*
339: * Update the maps before closing the cursor, so that the cursor
340: * lock prevents other writers from duplicating this entry.
341: */
342: classInfo.setClassFormat(classFormat);
343: classMap.put(className, classInfo);
344: formatMap.put(new BigInteger(idBytes), classFormat);
345: return classInfo;
346: } finally {
347: if (cursor != null) {
348: cursor.close();
349: }
350: if (txn != null) {
351: txn.commit();
352: }
353: }
354: }
355:
356: private static byte[] incrementID(byte[] key) {
357:
358: BigInteger id = new BigInteger(key);
359: id = id.add(BigInteger.valueOf(1));
360: return id.toByteArray();
361: }
362:
363: /**
364: * Holds the class format key for a class, maintains a reference to the
365: * ObjectStreamClass. Other fields can be added when we need to store more
366: * information per class.
367: */
368: private static class ClassInfo implements Serializable {
369:
370: private byte[] classID;
371: private transient ObjectStreamClass classFormat;
372:
373: ClassInfo() {
374: }
375:
376: ClassInfo(DatabaseEntry dbt) {
377:
378: byte[] data = dbt.getData();
379: int len = data[0];
380: classID = new byte[len];
381: System.arraycopy(data, 1, classID, 0, len);
382: }
383:
384: void toDbt(DatabaseEntry dbt) {
385:
386: byte[] data = new byte[1 + classID.length];
387: data[0] = (byte) classID.length;
388: System.arraycopy(classID, 0, data, 1, classID.length);
389: dbt.setData(data);
390: }
391:
392: void setClassID(byte[] classID) {
393:
394: this .classID = classID;
395: }
396:
397: byte[] getClassID() {
398:
399: return classID;
400: }
401:
402: ObjectStreamClass getClassFormat() {
403:
404: return classFormat;
405: }
406:
407: void setClassFormat(ObjectStreamClass classFormat) {
408:
409: this .classFormat = classFormat;
410: }
411: }
412:
413: /**
414: * Return whether two class formats are equal. This determines whether a
415: * new class format is needed for an object being serialized. Formats must
416: * be identical in all respects, or a new format is needed.
417: */
418: private static boolean areClassFormatsEqual(
419: ObjectStreamClass format1, byte[] format1Bytes,
420: ObjectStreamClass format2) {
421: try {
422: if (format1Bytes == null) { // using cached format1 object
423: format1Bytes = getObjectBytes(format1);
424: }
425: byte[] format2Bytes = getObjectBytes(format2);
426: return java.util.Arrays.equals(format2Bytes, format1Bytes);
427: } catch (IOException e) {
428: return false;
429: }
430: }
431:
432: /*
433: * We can return the same byte[] for 0 length arrays.
434: */
435: private static byte[] ZERO_LENGTH_BYTE_ARRAY = new byte[0];
436:
437: private static byte[] getBytes(DatabaseEntry dbt) {
438: byte[] b = dbt.getData();
439: if (b == null) {
440: return null;
441: }
442: if (dbt.getOffset() == 0 && b.length == dbt.getSize()) {
443: return b;
444: }
445: int len = dbt.getSize();
446: if (len == 0) {
447: return ZERO_LENGTH_BYTE_ARRAY;
448: } else {
449: byte[] t = new byte[len];
450: System.arraycopy(b, dbt.getOffset(), t, 0, t.length);
451: return t;
452: }
453: }
454:
455: private static byte[] getObjectBytes(Object o) throws IOException {
456:
457: ByteArrayOutputStream baos = new ByteArrayOutputStream();
458: ObjectOutputStream oos = new ObjectOutputStream(baos);
459: oos.writeObject(o);
460: return baos.toByteArray();
461: }
462: }
|