/
area/
classes/net/sourceforge/pain/console/
classes/net/sourceforge/pain/logic/
classes/net/sourceforge/pain/logic/event/
classes/net/sourceforge/pain/logic/fn/util/
classes/net/sourceforge/pain/plugin/
classes/net/sourceforge/pain/plugin/reset/
classes/net/sourceforge/pain/plugin/shutdown/
classes/net/sourceforge/pain/plugin/social/
classes/net/sourceforge/pain/util/
classest/net/sourceforge/pain/db/data/
doc/
doc/paindb/resources/
src/net/sourceforge/pain/console/
src/net/sourceforge/pain/console/telnet/
src/net/sourceforge/pain/logic/
src/net/sourceforge/pain/logic/event/
src/net/sourceforge/pain/logic/fn/util/
src/net/sourceforge/pain/plugin/
src/net/sourceforge/pain/plugin/command/
src/net/sourceforge/pain/plugin/reset/
src/net/sourceforge/pain/plugin/shutdown/
src/net/sourceforge/pain/plugin/social/
src/net/sourceforge/pain/util/
tests/net/sourceforge/pain/db/data/
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();
		}
	}
}