package net.sourceforge.pain.db; import net.sourceforge.pain.util.*; import java.io.*; import java.util.*; /** * PAiN Db is a not thread safe semi-object oriented main memory and very buggy database.<br> * It's used by PAiN Mud Codebase @http://pain.sf.net as persistence engine.<br> * However, PAiN DB is <b>general purpose database</b>, it has great performance,<br> * it's simple, opensource and could be used in any java based opensource projects.<br> * <i>Distributed under the GPL licence</i> */ public final class PainDB { public static final String DB_VERSION = "0.22"; private static final int[] zeroPageNumsStub = new int[0]; private static final Object[] constructParams = new Object[0]; private DbObject[] objects = null; private final Map dbClassByClass = new HashMap(); private final Map dbClassByName = new HashMap(); private final DbIntBuffer dirty = new DbIntBuffer(1024); //indexIds of dirties private final DbIntBuffer freeIndexIds = new DbIntBuffer(); private int maxUsedIndexId = 0; // used only during startup, as optimization param private final DbIntBuffer pagesToDeallocate = new DbIntBuffer(); private long currentVersionId; private final DbObjectMapper objectMapper; private final DbPageMapper pageMapper; private final DbRuntimeMetaClass meta; private int rootIndex; private boolean active; DbTransactionContext activeTrans; private int transNo = 0; /** * allows use setters outside from transactions -> could not be rolled back (performance issue) * but if database will be closed before flush, this changes will be lost */ public boolean ALLOW_PLAIN_WRITE = true; /** * MANUAL_FLUSH mode is a kind of delayed commit, user should manually call flush (outside of transaction) * to flush all data to disk, if MANUAL_FLUSH_MODE is false every time T1(upper level) transaction * commited PainDB will automatically call flush method. * 'plain writes' always should be flushed manually */ public boolean MANUAL_FLUSH_MODE = true; /** * Opens specified database file. Creates if file do not exists. * @param fileName - name of database file * @throws Exception - if file is corrupted or is not paindb database file */ public PainDB(final String fileName) throws Exception { pageMapper = new DbPageMapper(fileName); objectMapper = new DbObjectMapper(this, pageMapper); meta = new DbRuntimeMetaClass(this); readDB(); active = true; } /** * flushes all changes done after previous flush to disk * Called automatically after each upper level transaction commit if * MANUAL_FLUSH_MODE is false * @throws IOException if any IO error occurs. * @throws RuntimeException if there is active transaction */ public void flush() throws IOException { checkDbState(); if (activeTrans != null) { throw new RuntimeException("active transaction found!"); } _flush(); } private synchronized void _flush() throws IOException { // flushing db metainfo final byte[] firstPage = pageMapper.getPageImage(); DbPacker.pack8(firstPage, 0, currentVersionId); DbPacker.pack4(firstPage, 8, maxUsedIndexId); DbPacker.pack4(firstPage, 12, rootIndex); System.arraycopy(pageMapper.ZERO_PAGE, 0, firstPage, 16, pageMapper.pageSize - 16); pageMapper.writePage(0, firstPage); // flushing all dirty objects final long time = System.currentTimeMillis(); final int size = dirty.getSize(); for (int i = 0; i < size; i++) { final DbObject obj = objects[dirty.data[i]]; if (obj == null || obj.globalState == DbConstants.STATE_OBJ_CLEAN) { // obj was deleted or dublicate entry in dirties with the same indexid (old deleted new craeted) continue; } objectMapper.writeObject(obj); obj.globalState = DbConstants.STATE_OBJ_CLEAN; } dirty.clear(); // WARN: pages deallocation done only on flush method call!! final int[] deallocatePages = pagesToDeallocate.data; final int nDeallocatePages = pagesToDeallocate.getSize(); for (int i = 0; i < nDeallocatePages; i++) { int page = deallocatePages[i]; pageMapper.writePage(page, pageMapper.ZERO_PAGE); } if (Log.isDebugEnabled()) { Log.debug("flush without disk flush time:" + (System.currentTimeMillis() - time)); } pageMapper.flush(); for (int i = 0; i < nDeallocatePages; i++) { pageMapper.deallocatePage(deallocatePages[i]); } pagesToDeallocate.clear(); } /** * @param classId - OID of the DbClass * @return DbClass instance or null if no DbClass with specified classId found */ public DbClass getClass(final Object classId) { final DbOid oid = (DbOid) classId; final DbObject image; if (!isIdInRange(oid.indexId)) { return null; } image = objects[oid.indexId]; if (image == null) { return null; } if (image.dbClass != meta) { // class image return null; } if (image.versionId != oid.versionId) { return null; } if (image.transContext != null && image.transContext.state == DbConstants.STATE_OBJ_DELETED) { return null; } return ((DbClassImage) image).getDbRuntimeClass(); } /** * @param objectId - serialized unique object id * @return DbObject for specified objectId or null if no object found */ public DbObject getObject(final Object objectId) { final DbOid oid = (DbOid) objectId; final DbObject result; if (!isIdInRange(oid.indexId)) { return null; } result = objects[oid.indexId]; if (result == null) { return null; } if (result.dbClass == meta) { // class image return null; } if (result.versionId != oid.versionId) { return null; } if (result.transContext != null && result.transContext.state == DbConstants.STATE_OBJ_DELETED) { return null; } return result; } /** * closes database. All database objects are DETACHED after this method call * flushes all changes if MANUAL_FLUSH_MODE = true; * Database should not have active transaction during this method call */ public void close() { checkDbState(); if (activeTrans != null) { throw new RuntimeException(); } if (!MANUAL_FLUSH_MODE) { try { flush(); } catch (IOException e) { throw new RuntimeException(e); } } for (Iterator it = dbClassByClass.values().iterator(); it.hasNext();) { final DbClassImpl dbClass = (DbClassImpl) it.next(); dbClass.setDbClosed(); } dbClassByClass.clear(); dbClassByName.clear(); try { pageMapper.close(); } catch (Exception e) { Log.error(e); } objects = null; active = false; } public DbClass getDbClass(final Class javaClazz) { return (DbClass) this.dbClassByClass.get(javaClazz); } /** * root is a simple mark on object, It's allowed to do not have root in DB. * @return database root object */ public DbObject getRoot() { checkDbState(); final int root = (activeTrans == null ? rootIndex : activeTrans.rootIndex); if (root == -1 || !isIdInRange(root)) { return null; } DbObject obj = getObjectByIndexId(root); if (obj != null && (obj.isDeleted() || obj.isDetached())) { return null; } return obj; } private void checkDbState() { if (!active) { throw new IllegalStateException("Db was closed!"); } } /** * root is a simple mark on object, db could have not root at all, any time user can null this mark */ public void setRoot(final DbObject obj) { checkDbState(); if (activeTrans == null && ALLOW_PLAIN_WRITE) { //plain write (no rollback ability) if (obj == null) { rootIndex = -1; } else { ensureOwner(obj); obj.ensureReal(); rootIndex = obj.indexId; } } else { // inside transaciton ensureTransaction(); if (obj == null) { activeTrans.rootIndex = -1; } else { ensureOwner(obj); obj.ensureReal(); rootIndex = obj.indexId; } } } private void ensureTransaction() { if (activeTrans == null) { throw new RuntimeException("Out of transaction"); } } DbClassImpl getDbClassMetaSchema() { return meta; } /** * reads all classes and object from file */ private void readDB() throws Exception { final byte[] firstPage = pageMapper.startup_readPage(0); pageMapper.startup_markPageAsUsed(0); currentVersionId = DbPacker.unpack8(firstPage, 0); rootIndex = -1; if (currentVersionId == 0) { objects = new DbObject[(int) (pageMapper.getFileSize() / pageMapper.pageSize)]; freeIndexIds.ensureCapacity(objects.length); for (int i = objects.length; --i >= 0;) { freeIndexIds.add(i); } Log.debug("New Database Created"); } else { int indexIdsSize = DbPacker.unpack4(firstPage, 8); rootIndex = DbPacker.unpack4(firstPage, 12); maxUsedIndexId = indexIdsSize; if (indexIdsSize == 0) { indexIdsSize = 1024; } else { indexIdsSize = (int) ((100 + indexIdsSize) * 1.3); } objects = new DbObject[indexIdsSize]; Log.debug("Reading db"); // 1) read all classes final int numberOfPages = pageMapper.getNumberOfPages(); for (int i = 1; i < numberOfPages; i++) { final byte[] data = pageMapper.startup_readPage(i); if (data == null) { continue; // was marked as used and removed from startup cache } if (DbObjectMapper.isClassSchemaStartPage(data)) { final DbRuntimeClass dbClass = objectMapper.readClassSchema(i); addClassInMaps(dbClass); pageMapper.startup_markPageAsUsed(dbClass.getPageNums()); objects[dbClass.image.indexId] = dbClass.image; } } // 2) read all objects for (int i = 0; i < numberOfPages; i++) { final byte[] data = pageMapper.startup_readPage(i); if (data == null) { continue; // was marked as used } if (DbObjectMapper.isObjectStartPage(data)) { final DbObject obj = objectMapper.readObject(i); // mapps indexId in objects[] pageMapper.startup_markPageAsUsed(obj.pageNums); if (obj.versionId > currentVersionId) { currentVersionId = obj.versionId; // new objects will have oid with newer version id } } } // 3) initializing all reference fields that needed to be initialized (collecitons) for (Iterator it = dbClassByClass.values().iterator(); it.hasNext();) { final DbClassImpl dbClass = (DbClassImpl) it.next(); dbClass.onDbLoaded(); } // 4) marking freeIds freeIndexIds.ensureCapacity(objects.length); for (int i = objects.length; --i >= 0;) { if (objects[i] == null) { freeIndexIds.add(i); } } } pageMapper.startup_complete(); currentVersionId++; _flush(); } /** * Internal use only, register class in all mapping structures * @param dbClass * @throws RuntimeException */ private void addClassInMaps(final DbRuntimeClass dbClass) throws RuntimeException { try { dbClassByClass.put(Class.forName(dbClass.getClassName()), dbClass); } catch (Exception e) { throw new RuntimeException(e); } dbClassByName.put(dbClass.getClassName(), dbClass); } void registerObject(final DbObject obj) throws RuntimeException { checkDbState(); if (!ALLOW_PLAIN_WRITE) { ensureTransaction(); } final Class objClass = obj.getClass(); DbClassImpl dbClass = objClass == DbRuntimeClass.class ? meta : (DbClassImpl) dbClassByClass.get(objClass); final DbOid oid; if (dbClass == null) { final DbClassSchema schema = obj.provideSchema(); // check if object schema is DbRuntimeClass Image -> no persistence presentation if (schema == DbClassImage.schema) { // new class image dbClass = meta; } else { // new object DbRuntimeClass dbRunClass = (DbRuntimeClass) dbClassByClass.get(obj.getClass()); if (dbRunClass == null) { dbRunClass = registerNewClass(obj.getClass(), schema); } dbClass = dbRunClass; } } oid = allocateOid(); obj.dbClass = dbClass; obj.indexId = oid.indexId; obj.versionId = oid.versionId; obj.pageNums = zeroPageNumsStub; obj.dataIndex = dbClass.data.allocateDataIndex(); objects[oid.indexId] = obj; if (activeTrans == null) { obj.globalState = DbConstants.STATE_OBJ_DIRTY; dirty.add(obj.indexId); // plain write } else { obj.globalState = DbConstants.STATE_OBJ_NEW; // will become dirty after transaction will commited addInTransaction(obj); // in transaction } dbClass.addToExtent(obj); } /** * Creates new DbClassImpl with creation dataObject = DbRuntimeClass.DbImage * @param clazz * @param schema * @return * @throws RuntimeException */ private synchronized DbRuntimeClass registerNewClass(final Class clazz, final DbClassSchema schema) throws RuntimeException { final DbClassImage image = new DbClassImage(this); image.setClassName(clazz.getName()); image.setFieldTypes(schema.fieldTypes); image.setFieldNames(schema.fieldNames); final DbRuntimeClass dbClass = new DbRuntimeClass(image); addClassInMaps(dbClass); if (!ALLOW_PLAIN_WRITE || activeTrans != null) { addInTransaction(dbClass, true); dbClass.transContext.state = DbConstants.STATE_CLASS_NEW; } return dbClass; } /** * allocates new object id, first of all tries to reuse oid with old indexId (with increased version), extends indexId space if needed) */ private synchronized DbOid allocateOid() { currentVersionId++; if (freeIndexIds.isEmpty()) { final int newSize = objects.length + objects.length / 10; final DbObject[] newObjects = new DbObject[newSize]; System.arraycopy(objects, 0, newObjects, 0, objects.length); for (int i = newObjects.length; --i >= objects.length;) { freeIndexIds.add(i); } objects = newObjects; } final int indexId = freeIndexIds.removeLast(); maxUsedIndexId = maxUsedIndexId < indexId ? indexId : maxUsedIndexId; return new DbOid(indexId, currentVersionId); } private boolean isIdInRange(final int indexId) { return indexId >= 0 && indexId < objects.length; } /** this method called only if object has not activeTransContext, so it was created before activeTrans * ( new objects get trans context during creation), so this object should be dirty for current trans */ void markDirty(final DbObject obj) { if (ALLOW_PLAIN_WRITE && activeTrans == null) { if (obj.globalState != DbConstants.STATE_OBJ_CLEAN) { return; } obj.globalState = DbConstants.STATE_OBJ_DIRTY; dirty.add(obj.indexId); } else { ensureTransaction(); addInTransaction(obj); } } /** object is already in current trans context*/ void markDeleted(final DbObject obj) { if (activeTrans == null && ALLOW_PLAIN_WRITE) { clearInverseReferences(obj); final DbClassImpl dbClass = obj.dbClass; dbClass.onMarkDeleted(obj); detachObject(obj); } else { ensureTransaction(); clearInverseReferences(obj); obj.transContext.state = DbConstants.STATE_OBJ_DELETED; obj.dbClass.onMarkDeleted(obj); } } /** * cleaning inverse references from collections on object.delete(); * this method triggers inverse collection owner to be dirty and backup * collection state before cleaning inverse */ private static void clearInverseReferences(final DbObject obj) { for (DbInverseRef ir = obj.inverseRef; ir != null; ir = ir.nextInverseRef) { ir.onTargetDelete(); } obj.inverseRef = null; } DbClassImpl getDbClassSchema(final int classId) { return ((DbClassImage) getObjectByIndexId(classId)).getDbRuntimeClass(); } /** * could return instance in deleted state! */ DbObject getObjectByIndexId(final int indexId) { return objects[indexId]; } /** * startup method * @param dbClass * @param oid * @param dataIndex * @param pageNums * @return * @throws Exception */ DbObject reflectDbObject(final DbClassImpl dbClass, final DbOid oid, final int dataIndex, final int[] pageNums) throws Exception { final DbObject obj; obj = (DbObject) dbClass.getReadConstructor().newInstance(constructParams); obj.dbClass = dbClass; obj.dataIndex = dataIndex; obj.globalState = DbConstants.STATE_OBJ_CLEAN; obj.pageNums = pageNums; obj.indexId = oid.indexId; obj.versionId = oid.versionId; objects[oid.indexId] = obj; dbClass.addToExtent(obj); return obj; } int getClassId(final DbObject obj) { return obj.dbClass == meta ? obj.indexId : ((DbRuntimeClass) (obj.dbClass)).getClassId(); } void ensureOwner(final DbObject value) { if (value.getDB() != this) { throw new RuntimeException("Invalid Db!"); } } void removeClass(final DbRuntimeClass dbClass) { if (!ALLOW_PLAIN_WRITE) {//else for the (activeTrans == null && ALLOW_PLAIN_WRITE) //ensure we are in trans context ensureTransaction(); //ensure class is not already deleted final DbClassTransContext classTransContext = dbClass.transContext; if (classTransContext != null && (classTransContext.state == DbConstants.STATE_CLASS_DELETED || classTransContext.state == DbConstants.STATE_CLASS_NEW_AND_DELETED)) { throw new RuntimeException("class is already was deleted!:" + dbClass.getClassName()); } //ensure class has activeTrans context if (classTransContext == null || classTransContext.trans != activeTrans) { addInTransaction(dbClass, false); // new class will have trans context } } //destroying class extent and remove all mappings for (Iterator it = dbClass.extentIterator(); it.hasNext();) { it.next(); it.remove(); } dbClass.image.delete(); // deallocating resources final Class objectClass = dbClass.getObjectClass(); dbClassByClass.remove(objectClass); dbClassByName.remove(objectClass.getName()); if (activeTrans != null) { //not plain write final DbClassTransContext classTransContext = dbClass.transContext; classTransContext.state = classTransContext.state == DbConstants.STATE_CLASS_NEW ? DbConstants.STATE_CLASS_NEW_AND_DELETED : DbConstants.STATE_CLASS_DELETED; } } /** * @return database file size * @throws IOException if any IO error occured during this method call */ public long getDBFileSize() throws IOException { return pageMapper.getFileSize(); } /** * @return true for just created database or for database without objects created */ public boolean isDatabaseEmpty() { return getNumberOfObjectsInDb() == 0; } /** * @return number of all objects, including classes. */ public int getNumberOfObjectsInDb() { checkDbState(); return objects.length - freeIndexIds.getSize(); } protected void finalize() { if (active) { Log.warn("closing db with finalize!"); close(); } } /** * starts the database transaction. * Recurrent calls of this method without commit will create subtransactions. * Its recommended to use {@link DbTransaction} wrapper class and do not call * this method manually */ public void beginTransaction() { checkDbState(); activeTrans = new DbTransactionContext(transNo++, activeTrans, activeTrans == null ? rootIndex : activeTrans.rootIndex); } /** * commits the database transaction. * Its recommended to use {@link DbTransaction} wrapper class and do not call * this method manually * This method will automatically flush all changes to disk if * there no upperlevel transaction and MANUAL_FLUSH_MODE was not set * @throws IOException if any IO problem occurs during flush */ public void commitTransaction() throws IOException { if (activeTrans == null) { throw new IllegalStateException("No active transaction found!"); } final DbTransactionContext upperTransaction = activeTrans.upperLevelTrans; if (upperTransaction != null) { //combining this if and else blocks insingle block is possible but will complicate code and drop performance (we will need to do this check during every object processing) commitTN(upperTransaction); activeTrans = upperTransaction; checkTrans(); } else { commitT1(); activeTrans = upperTransaction; if (!MANUAL_FLUSH_MODE) { _flush(); } } } private void checkTrans() { // HashSet set = new HashSet(); // for (DbClassImpl cl = activeTrans.firstClassInTrans; cl != null; cl = cl.transContext != null ? cl.transContext.nextClassInTrans : null) { // if (set.contains(cl)) { // Log.error("set already contains this class!!!:" + cl.getClassName()); // } // if (cl.transContext == null) { // Log.error(" trans context is null!!!:" + cl.getClassName()); // } else if (cl.transContext.trans != activeTrans) { // Log.error("invalid trans context!!!:" + cl.getClassName()); // } // set.add(cl); // } } /** commiting transaction with deep1 */ private void commitT1() { //upper transaction is null, we should: //delete all new&deleted objects and classes //move state from transContext to globalState for objects //remove all trans contexts DbClassImpl nextClass; for (DbClassImpl dbClass = activeTrans.firstClassInTrans; dbClass != null; dbClass = nextClass) { final DbClassTransContext classTransContext = dbClass.transContext; nextClass = classTransContext.nextClassInTrans; if (classTransContext.backupData != null) { classTransContext.backupData.clear(); } DbObject nextObject; for (DbObject obj = classTransContext.firstObjInTrans; obj != null; obj = nextObject) { final DbObjectTransContext objTransContext = obj.transContext; PAssert.that(objTransContext.prevTransContext == null); nextObject = objTransContext.nextObjInTrans; if (objTransContext.state == DbConstants.STATE_OBJ_DELETED) { //deleted or new_deleted detachObject(obj); } else { if (obj.globalState != DbConstants.STATE_OBJ_DIRTY) { //globalState == DIRTY if there was no flush between transactions dirty.add(obj.indexId); obj.globalState = DbConstants.STATE_OBJ_DIRTY; } obj.transContext = null; } } dbClass.transContext = null; } rootIndex = activeTrans.rootIndex; } /** * commiting transaction with deep = N * @param upperTransaction */ private void commitTN(final DbTransactionContext upperTransaction) { //here we should: //process all trans classes //remove all new&&deleted classes and it's objects //move all backupData obj data in upper context //move state of obj and classes to upper context DbClassImpl nextClassInActiveTrans; for (DbClassImpl dbClass = activeTrans.firstClassInTrans; dbClass != null; dbClass = nextClassInActiveTrans) { final DbClassTransContext classTransContext = dbClass.transContext; nextClassInActiveTrans = classTransContext.nextClassInTrans; // for new_deleted we should just clear resources final DbClassTransContext prevTransClassContext = classTransContext.prevTransContext; // if (classPrevTransContext is not equals upper we can reuse current without any change) if (prevTransClassContext == null || prevTransClassContext.trans != upperTransaction) { //prevTransClassContext is not match prev transaction //reusing current context, but now it's responsible for upper level transaction classTransContext.trans = upperTransaction; classTransContext.nextClassInTrans = upperTransaction.firstClassInTrans; upperTransaction.firstClassInTrans = dbClass; //remove new_deleted objects, review chain of class trans objects DbObject nextObject; DbObject obj = classTransContext.firstObjInTrans; classTransContext.firstObjInTrans = null;//we will rebuild it for (; obj != null; obj = nextObject) { final DbObjectTransContext objTransContext = obj.transContext; nextObject = objTransContext.nextObjInTrans; if (objTransContext.state == DbConstants.STATE_OBJ_DELETED && objTransContext.backupDataIndex == -1) { //NEW_AND_DELETED // new and deleted in current trans detachObject(obj); } else { objTransContext.nextObjInTrans = classTransContext.firstObjInTrans; classTransContext.firstObjInTrans = obj; objTransContext.trans = upperTransaction; } } } else { // prevClassContexts exists, we should add diff to it DbObject nextObject; for (DbObject obj = classTransContext.firstObjInTrans; obj != null; obj = nextObject) { final DbObjectTransContext objTransContext = obj.transContext; nextObject = objTransContext.nextObjInTrans; if (objTransContext.state == DbConstants.STATE_OBJ_DELETED && objTransContext.backupDataIndex == -1) { //NEW_AND_DELETED detachObject(obj); } else { final DbObjectTransContext prevObjTransContext = objTransContext.prevTransContext; if (prevObjTransContext != null && prevObjTransContext.trans == upperTransaction) { // ok backupData data is already exists in upper level, we should move only state prevObjTransContext.state = objTransContext.state; obj.transContext = prevObjTransContext; //loosing ref to last trans } else { //add class in chain for prev transaction, reuse current transcontext object objTransContext.trans = upperTransaction; objTransContext.nextObjInTrans = prevTransClassContext.firstObjInTrans; prevTransClassContext.firstObjInTrans = obj; if (objTransContext.backupDataIndex != -1 && prevTransClassContext.backupData != null) { //new created stays new created and if class was created in prev transaction-> there is no backup to objTransContext.backupDataIndex = dbClass.moveBackupData(obj, objTransContext.backupDataIndex, classTransContext.backupData, prevTransClassContext.backupData); } } } } // transContext for activeTrans will not be used any more, here we loose link to it if (classTransContext.backupData != null) { classTransContext.backupData.clear(); } dbClass.transContext = prevTransClassContext; prevTransClassContext.state = prevTransClassContext.state == DbConstants.STATE_CLASS_NEW ? DbConstants.STATE_CLASS_NEW : classTransContext.state;// class is now a part of upper transaction } } upperTransaction.rootIndex = activeTrans.rootIndex; } /** deallocates all object resources * object could not be restored back after this method call * @param obj */ private void detachObject(final DbObject obj) { obj.dbClass.data.deallocateDataIndex(obj.dataIndex); pagesToDeallocate.add(obj.pageNums); obj.globalState = DbConstants.STATE_OBJ_DETACHED; objects[obj.indexId] = null; if (rootIndex == obj.indexId) { rootIndex = -1; } freeIndexIds.add(obj.indexId); obj.indexId = -1; obj.versionId = -1; obj.dbClass = null; obj.transContext = null; } public void rollbackTransaction() { checkDbState(); checkTrans(); // here we should: // process all new, new_deleted -> deallocate resources // restore data from backupData // set state to the state before trans final DbTransactionContext upperTransaction = activeTrans.upperLevelTrans; DbClassImpl nextClass; // force delete all new classes for (DbClassImpl dbClass = activeTrans.firstClassInTrans; dbClass != null; dbClass = nextClass) { if (dbClass.transContext.state == DbConstants.STATE_CLASS_NEW) { removeClass((DbRuntimeClass) dbClass); } nextClass = dbClass.transContext.nextClassInTrans; } // ok there is no new classes now, only deleted for (DbClassImpl dbClass = activeTrans.firstClassInTrans; dbClass != null; dbClass = nextClass) { if (Log.isDebugEnabled()) { Log.debug("Rollback:" + dbClass.getClassName()); } final DbClassTransContext classTransContext = dbClass.transContext; nextClass = classTransContext.nextClassInTrans; if (classTransContext.state == DbConstants.STATE_CLASS_DELETED) { addClassInMaps((DbRuntimeClass) dbClass); } DbObject nextObject; for (DbObject obj = classTransContext.firstObjInTrans; obj != null; obj = nextObject) { final DbObjectTransContext objTransContext = obj.transContext; nextObject = objTransContext.nextObjInTrans; if (objTransContext.backupDataIndex == -1) {//(NEW or NEW_DELETED) detachObject(obj); } else { if (obj.transContext.state == DbConstants.STATE_OBJ_DELETED) { dbClass.addToExtent(obj); } dbClass.restoreObject(obj); // restore from backup obj.transContext = obj.transContext.prevTransContext; } } dbClass.transContext = classTransContext.prevTransContext; } activeTrans = upperTransaction; Log.debug("rollback done!"); } /** 1) called during new obj registration (NEW) * 2) during set method on object wich is not in current transaction(NOT NEW) * 3) not called on obj.delete -> obj states become NEW_DELETED and nothing to do here */ private void addInTransaction(final DbObject obj) { final boolean newObject = obj.globalState == DbConstants.STATE_OBJ_NEW; PAssert.that(obj.transContext == null || obj.transContext.trans != activeTrans); final DbObjectTransContext objTransContext = new DbObjectTransContext(activeTrans); objTransContext.prevTransContext = obj.transContext; objTransContext.state = newObject ? DbConstants.STATE_OBJ_NEW : DbConstants.STATE_OBJ_DIRTY; final DbClassImpl dbClass = obj.dbClass; DbClassTransContext classTransContext = dbClass.transContext; if (classTransContext == null || classTransContext.trans != activeTrans) { // first object of this class in trans addInTransaction(dbClass, false); classTransContext = dbClass.transContext; } // here we need to backupData all obj data into classTransContext.data // we will not create backup if object was just created objTransContext.backupDataIndex = newObject && obj.transContext == null ? -1 : dbClass.backupObject(obj); obj.transContext = objTransContext; objTransContext.nextObjInTrans = classTransContext.firstObjInTrans; classTransContext.firstObjInTrans = obj; } private void addInTransaction(final DbClassImpl dbClass, final boolean newClass) { final DbClassTransContext classContext = new DbClassTransContext(activeTrans); classContext.backupData = newClass ? null : dbClass.createClassData(); classContext.nextClassInTrans = activeTrans.firstClassInTrans; classContext.prevTransContext = dbClass.transContext; dbClass.transContext = classContext; activeTrans.firstClassInTrans = dbClass; } /** * same as execute(trans, null); */ public Object execute(DbTransaction trans) throws Exception { return execute(trans, null); } /** * Executes transaction. * @param params passed to DbTransaction.execute() method * @return result of the DbTransaction execute method} * @throws Exception if it was thrown in DbTransaction.execute method or if * write error on flush occurs */ public Object execute(DbTransaction trans, Object params[]) throws Exception { boolean ok = false; beginTransaction(); try { final Object result = trans.execute(params); ok = true; return result; } finally { if (ok) { try { commitTransaction(); } catch (Exception e) { Log.error("Exception during commit! ", e); throw e; } } else { try { rollbackTransaction(); } catch (Exception e) { Log.error("Exception during rollback!", e); // prev exception will be thrown here. } } } } /** * @return database file name */ public String getDbFileName() { return pageMapper.getFileName(); } /** * @return true if database was closed */ public boolean isClosed() { return !active; } /** all active transactions will be rolled back*/ public void forceClose() { checkDbState(); try { while (activeTrans != null) { rollbackTransaction(); } } finally { activeTrans = null; close(); } } }