1/*- 2 * See the file LICENSE for redistribution information. 3 * 4 * Copyright (c) 2000,2008 Oracle. All rights reserved. 5 * 6 * $Id: StoredClassCatalog.java,v 12.7 2008/01/08 20:58:35 bostic Exp $ 7 */ 8 9package com.sleepycat.bind.serial; 10 11import java.io.ByteArrayInputStream; 12import java.io.ByteArrayOutputStream; 13import java.io.IOException; 14import java.io.ObjectInputStream; 15import java.io.ObjectOutputStream; 16import java.io.ObjectStreamClass; 17import java.io.Serializable; 18import java.math.BigInteger; 19import java.util.HashMap; 20 21import com.sleepycat.compat.DbCompat; 22import com.sleepycat.db.Cursor; 23import com.sleepycat.db.CursorConfig; 24import com.sleepycat.db.Database; 25import com.sleepycat.db.DatabaseConfig; 26import com.sleepycat.db.DatabaseEntry; 27import com.sleepycat.db.DatabaseException; 28import com.sleepycat.db.EnvironmentConfig; 29import com.sleepycat.db.LockMode; 30import com.sleepycat.db.OperationStatus; 31import com.sleepycat.db.Transaction; 32import com.sleepycat.util.RuntimeExceptionWrapper; 33import com.sleepycat.util.UtfOps; 34 35/** 36 * A <code>ClassCatalog</code> that is stored in a <code>Database</code>. 37 * 38 * <p>A single <code>StoredClassCatalog</code> object is normally used along 39 * with a set of databases that stored serialized objects.</p> 40 * 41 * @author Mark Hayes 42 */ 43public class StoredClassCatalog implements ClassCatalog { 44 45 /* 46 * Record types ([key] [data]): 47 * 48 * [0] [next class ID] 49 * [1 / class ID] [ObjectStreamClass (class format)] 50 * [2 / class name] [ClassInfo (has 8 byte class ID)] 51 */ 52 private static final byte REC_LAST_CLASS_ID = (byte) 0; 53 private static final byte REC_CLASS_FORMAT = (byte) 1; 54 private static final byte REC_CLASS_INFO = (byte) 2; 55 56 private static final byte[] LAST_CLASS_ID_KEY = {REC_LAST_CLASS_ID}; 57 58 private Database db; 59 private HashMap classMap; 60 private HashMap formatMap; 61 private LockMode writeLockMode; 62 private boolean cdbMode; 63 private boolean txnMode; 64 65 /** 66 * Creates a catalog based on a given database. To save resources, only a 67 * single catalog object should be used for each unique catalog database. 68 * 69 * @param database an open database to use as the class catalog. It must 70 * be a BTREE database and must not allow duplicates. 71 * 72 * @throws DatabaseException if an error occurs accessing the database. 73 * 74 * @throws IllegalArgumentException if the database is not a BTREE database 75 * or if it configured to allow duplicates. 76 */ 77 public StoredClassCatalog(Database database) 78 throws DatabaseException, IllegalArgumentException { 79 80 db = database; 81 DatabaseConfig dbConfig = db.getConfig(); 82 EnvironmentConfig envConfig = db.getEnvironment().getConfig(); 83 84 writeLockMode = (DbCompat.getInitializeLocking(envConfig) || 85 envConfig.getTransactional()) ? LockMode.RMW 86 : LockMode.DEFAULT; 87 cdbMode = DbCompat.getInitializeCDB(envConfig); 88 txnMode = dbConfig.getTransactional(); 89 90 if (!DbCompat.isTypeBtree(dbConfig)) { 91 throw new IllegalArgumentException( 92 "The class catalog must be a BTREE database."); 93 } 94 if (DbCompat.getSortedDuplicates(dbConfig) || 95 DbCompat.getUnsortedDuplicates(dbConfig)) { 96 throw new IllegalArgumentException( 97 "The class catalog database must not allow duplicates."); 98 } 99 100 /* 101 * Create the class format and class info maps. Note that these are not 102 * synchronized, and therefore the methods that use them are 103 * synchronized. 104 */ 105 classMap = new HashMap(); 106 formatMap = new HashMap(); 107 108 DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY); 109 DatabaseEntry data = new DatabaseEntry(); 110 if (dbConfig.getReadOnly()) { 111 /* Check that the class ID record exists. */ 112 OperationStatus status = db.get(null, key, data, null); 113 if (status != OperationStatus.SUCCESS) { 114 throw new IllegalStateException 115 ("A read-only catalog database may not be empty"); 116 } 117 } else { 118 /* Add the initial class ID record if it doesn't exist. */ 119 data.setData(new byte[1]); // zero ID 120 /* Use putNoOverwrite to avoid phantoms. */ 121 db.putNoOverwrite(null, key, data); 122 } 123 } 124 125 // javadoc is inherited 126 public synchronized void close() 127 throws DatabaseException { 128 129 if (db != null) { 130 db.close(); 131 } 132 db = null; 133 formatMap = null; 134 classMap = null; 135 } 136 137 // javadoc is inherited 138 public synchronized byte[] getClassID(ObjectStreamClass classFormat) 139 throws DatabaseException, ClassNotFoundException { 140 141 ClassInfo classInfo = getClassInfo(classFormat); 142 return classInfo.getClassID(); 143 } 144 145 // javadoc is inherited 146 public synchronized ObjectStreamClass getClassFormat(byte[] classID) 147 throws DatabaseException, ClassNotFoundException { 148 149 return getClassFormat(classID, new DatabaseEntry()); 150 } 151 152 /** 153 * Internal function for getting the class format. Allows passing the 154 * DatabaseEntry object for the data, so the bytes of the class format can 155 * be examined afterwards. 156 */ 157 private ObjectStreamClass getClassFormat(byte[] classID, 158 DatabaseEntry data) 159 throws DatabaseException, ClassNotFoundException { 160 161 /* First check the map and, if found, add class info to the map. */ 162 163 BigInteger classIDObj = new BigInteger(classID); 164 ObjectStreamClass classFormat = 165 (ObjectStreamClass) formatMap.get(classIDObj); 166 if (classFormat == null) { 167 168 /* Make the class format key. */ 169 170 byte[] keyBytes = new byte[classID.length + 1]; 171 keyBytes[0] = REC_CLASS_FORMAT; 172 System.arraycopy(classID, 0, keyBytes, 1, classID.length); 173 DatabaseEntry key = new DatabaseEntry(keyBytes); 174 175 /* Read the class format. */ 176 177 OperationStatus status = db.get(null, key, data, LockMode.DEFAULT); 178 if (status != OperationStatus.SUCCESS) { 179 throw new ClassNotFoundException("Catalog class ID not found"); 180 } 181 try { 182 ObjectInputStream ois = 183 new ObjectInputStream( 184 new ByteArrayInputStream(data.getData(), 185 data.getOffset(), 186 data.getSize())); 187 classFormat = (ObjectStreamClass) ois.readObject(); 188 } catch (IOException e) { 189 throw new RuntimeExceptionWrapper(e); 190 } 191 192 /* Update the class format map. */ 193 194 formatMap.put(classIDObj, classFormat); 195 } 196 return classFormat; 197 } 198 199 /** 200 * Get the ClassInfo for a given class name, adding it and its 201 * ObjectStreamClass to the database if they are not already present, and 202 * caching both of them using the class info and class format maps. When a 203 * class is first loaded from the database, the stored ObjectStreamClass is 204 * compared to the current ObjectStreamClass loaded by the Java class 205 * loader; if they are different, a new class ID is assigned for the 206 * current format. 207 */ 208 private ClassInfo getClassInfo(ObjectStreamClass classFormat) 209 throws DatabaseException, ClassNotFoundException { 210 211 /* 212 * First check for a cached copy of the class info, which if 213 * present always contains the class format object 214 */ 215 String className = classFormat.getName(); 216 ClassInfo classInfo = (ClassInfo) classMap.get(className); 217 if (classInfo != null) { 218 return classInfo; 219 } else { 220 /* Make class info key. */ 221 char[] nameChars = className.toCharArray(); 222 byte[] keyBytes = new byte[1 + UtfOps.getByteLength(nameChars)]; 223 keyBytes[0] = REC_CLASS_INFO; 224 UtfOps.charsToBytes(nameChars, 0, keyBytes, 1, 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, LockMode.DEFAULT); 230 if (status != OperationStatus.SUCCESS) { 231 /* 232 * Not found in the database, write class info and class 233 * format. 234 */ 235 classInfo = putClassInfo(new ClassInfo(), className, key, 236 classFormat); 237 } else { 238 /* 239 * Read class info to get the class format key, then read class 240 * format. 241 */ 242 classInfo = new ClassInfo(data); 243 DatabaseEntry formatData = new DatabaseEntry(); 244 ObjectStreamClass storedClassFormat = 245 getClassFormat(classInfo.getClassID(), formatData); 246 247 /* 248 * Compare the stored class format to the current class format, 249 * and if they are different then generate a new class ID. 250 */ 251 if (!areClassFormatsEqual(storedClassFormat, 252 getBytes(formatData), 253 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, 274 DatabaseEntry classKey, 275 ObjectStreamClass classFormat) 276 throws DatabaseException, ClassNotFoundException { 277 278 /* An intent-to-write cursor is needed for CDB. */ 279 CursorConfig cursorConfig = null; 280 if (cdbMode) { 281 cursorConfig = new CursorConfig(); 282 DbCompat.setWriteCursor(cursorConfig, true); 283 } 284 Cursor cursor = null; 285 Transaction txn = null; 286 try { 287 if (txnMode) { 288 txn = db.getEnvironment().beginTransaction(null, null); 289 } 290 cursor = db.openCursor(txn, cursorConfig); 291 292 /* Get the current class ID. */ 293 DatabaseEntry key = new DatabaseEntry(LAST_CLASS_ID_KEY); 294 DatabaseEntry data = new DatabaseEntry(); 295 OperationStatus status = cursor.getSearchKey(key, data, 296 writeLockMode); 297 if (status != OperationStatus.SUCCESS) { 298 throw new IllegalStateException("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(ObjectStreamClass format1, 419 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) { return false; } 428 } 429 430 /* 431 * We can return the same byte[] for 0 length arrays. 432 */ 433 private static byte[] ZERO_LENGTH_BYTE_ARRAY = new byte[0]; 434 435 private static byte[] getBytes(DatabaseEntry dbt) { 436 byte[] b = dbt.getData(); 437 if (b == null) { 438 return null; 439 } 440 if (dbt.getOffset() == 0 && b.length == dbt.getSize()) { 441 return b; 442 } 443 int len = dbt.getSize(); 444 if (len == 0) { 445 return ZERO_LENGTH_BYTE_ARRAY; 446 } else { 447 byte[] t = new byte[len]; 448 System.arraycopy(b, dbt.getOffset(), t, 0, t.length); 449 return t; 450 } 451 } 452 453 private static byte[] getObjectBytes(Object o) 454 throws IOException { 455 456 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 457 ObjectOutputStream oos = new ObjectOutputStream(baos); 458 oos.writeObject(o); 459 return baos.toByteArray(); 460 } 461} 462