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

import net.sourceforge.pain.util.*;

import javax.xml.parsers.*;
import java.io.*;
import java.util.*;
import java.nio.channels.*;
import java.text.*;

/**
 * PAiN Db is a not thread safe semi-object oriented main memory and very buggy database.<br>
 * It's used by <a href="http://pain.sf.net">PAiN Mud Codebase</a> 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.23";

	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 long currentVersionId;
	private final DbObjectMapper objectMapper;
	private DbPageMapper pageMapper;
	private 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 {
		try {
//		System.out.println("Mem1:"+(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
//		long time = System.currentTimeMillis();
			pageMapper = new DbPageMapper(fileName);
//		System.out.println("Mapper Time:"+(System.currentTimeMillis() - time));

			objectMapper = new DbObjectMapper(this, pageMapper);
			meta = new DbRuntimeMetaClass(this);

//		System.out.println("Mem2:"+(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));		time = System.currentTimeMillis();
//		time = System.currentTimeMillis();
			readDB();
//		System.out.println("readDB Time:"+(System.currentTimeMillis() - time));
//		System.out.println("Mem3:"+(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));

			active = true;
		} catch (Exception e) {
			objects = null;
			pageMapper = null;
			meta = null;
			throw e;
		}
	}


	/**
	 * 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();
		if (Log.isDebugEnabled()) {
			Log.debug("PAINDB:flush without disk flush time:" + (System.currentTimeMillis() - time));
		}
		pageMapper.flush();
	}


	/**
	 * @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();
				activeTrans.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("PAINDB:New Database Created" + getDbFileName());
		} 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("PAINDB:Reading db");

			// 1) read all classes
			final int numberOfPages = pageMapper.getNumberOfPages();
			for (int pageNo = 1; pageNo < numberOfPages; pageNo++) {
				if (pageMapper.startup_isPageUsed(pageNo)) { //was already used
					continue;
				}
				final byte[] data = pageMapper.startup_readPage(pageNo);
				if (DbObjectMapper.isClassSchemaStartPage(data)) {
					final DbRuntimeClass dbClass = objectMapper.readClassSchema(pageNo);
					addClassInMaps(dbClass);
					pageMapper.startup_markPageAsUsed(dbClass.getPageNums());
					objects[dbClass.image.indexId] = dbClass.image;
				}
			}
			// 2) read all objects
			for (int pageNo = 0; pageNo < numberOfPages; pageNo++) {
				if (pageMapper.startup_isPageUsed(pageNo)) { //was already used
					continue;
				}
				final byte[] data = pageMapper.startup_readPage(pageNo);
				if (DbObjectMapper.isObjectStartPage(data)) {
					final DbObject obj = objectMapper.readObject(pageNo); // 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)
			_startup_initReferences();

			// 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();
	}

	private void _startup_initReferences() {
		for (Iterator it = dbClassByClass.values().iterator(); it.hasNext();) {
			final DbClassImpl dbClass = (DbClassImpl) it.next();
			dbClass.onDbLoaded();
		}
	}

	/**
	 * 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, null);
				}
				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
	 * @param oid - used for import (we should assign predefined oid to class
	 * @return
	 * @throws RuntimeException
	 */
	synchronized DbRuntimeClass registerNewClass(final Class clazz, final DbClassSchema schema, DbOid oid) throws RuntimeException {
		final DbClassImage image;
		if (oid != null) {
			try {
				image = (DbClassImage) reflectDbObject(meta, oid, DbConstants.ZERO_INT_ARRAY);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		} else {
			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()) {
			extendObjectsIndex(objects.length + 1, true);
		}
		final int indexId = freeIndexIds.removeLast();
		maxUsedIndexId = maxUsedIndexId < indexId ? indexId : maxUsedIndexId;
		return new DbOid(indexId, currentVersionId);
	}

	void extendObjectsIndex(int newCapacity, boolean increaseFreeIndexes) {
		final int oldObjLen = objects.length;
		final int newSize = Math.max(oldObjLen + oldObjLen / 10, newCapacity);
		final DbObject[] newObjects = new DbObject[newSize];
		System.arraycopy(objects, 0, newObjects, 0, oldObjLen);
		if (increaseFreeIndexes) {
			for (int i = newObjects.length; --i >= oldObjLen;) {
				freeIndexIds.add(i);
			}
		}
		objects = newObjects;
	}

	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/import method. Creates new Objects of specified class and assigns
	 * specified pageNums and oid to object. Adds objects to database object index
	 * and allocates data index for object.
	 * @param dbClass
	 * @param oid
	 * @param pageNums
	 * @return ready to use object with uninitialized field values.
	 * @throws Exception
	 */
	DbObject reflectDbObject(final DbClassImpl dbClass, final DbOid oid, final int[] pageNums) throws Exception {
		final DbObject obj;
		obj = (DbObject) dbClass.getReadConstructor().newInstance(constructParams);
		obj.dbClass = dbClass;
		obj.dataIndex = dbClass.data.allocateDataIndex();
		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;
				assert(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);
		pageMapper.deallocatePages(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("PAINDB: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("PAINDB: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;
		assert(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() {
//		checkDbState();
		return pageMapper.getFileName();
	}

	/**
	 * @return true if database was closed
	 */
	public boolean isClosed() {
		return !active;
	}

	/** forces database to close. All active transactions will be rolled back*/
	public void forceClose() {
		checkDbState();
		try {
			while (activeTrans != null) {
				rollbackTransaction();
			}
		} finally {
			activeTrans = null;
			close();
		}
	}

	/**
	 * Creates database backup file<br>
	 * <code>withFlush</code> should be <code>false</code> if this method is called inside of<br>
	 * transaction (database image created with last flush will be used)
	 * @param backupFileName file name for backup
	 * @param withFlush - if true database flush precede to backup
	 */
	public void backupTo(String backupFileName, boolean withFlush) throws IOException {
		File backup = _prepareForBackup(withFlush, backupFileName);

		// primitive backup: here we make just a copy of file
		final RandomAccessFile file = pageMapper.getFile();
		final FileOutputStream backupStream = new FileOutputStream(backup);
		try {
			final FileChannel toChannel = backupStream.getChannel();
			final FileChannel fromChanel = file.getChannel();
			fromChanel.position(0);
			toChannel.transferFrom(fromChanel, 0, fromChanel.size());
		} finally {
			backupStream.close();
		}
	}

	private File _prepareForBackup(boolean flush, String fileName) throws IOException {
		checkDbState();
		if (activeTrans != null && flush) {
			throw new RuntimeException("active transaction found, cannot flush data!");
		}
		if (flush) {
			_flush();
		}
		File backup = new File(fileName);
		if (backup.exists()) {
			throw new RuntimeException("File Already exists:" + backup.getAbsolutePath());
		}
		final File parentFile = backup.getParentFile();
		if (parentFile != null) {     // parent == null if we use default folder
			if (!parentFile.exists()) {
				if (!parentFile.mkdirs()) {
					throw new RuntimeException("Can't create backup file parent dirs: " + backup.getAbsolutePath());
				}
			} else if (!parentFile.isDirectory()) {
				throw new RuntimeException("illegal destination dir:" + parentFile.getAbsolutePath());
			}
		}
		return backup;
	}

	/**
	 * Exports all database data to XML file.<br>
	 * encodes with BASE64 binary fields values<br>
	 * keeps objects identities
	 * @param fileName = XML file name to export
	 */
	public void exportToXml(String fileName) throws IOException {
		File file = _prepareForBackup(true, fileName);
		BufferedWriter writer = new BufferedWriter(new FileWriter(file), 100 * 1000);
		writer.write("<?xml version=\"1.0\"?>\n\n");
		String rootOid = null;
		if (rootIndex > 0) {
			rootOid = DbOid.toString(rootIndex, objects[rootIndex].versionId);
		}
		writer.write("<paindb version=\"" + DB_VERSION + "\" backupDate=\"" + DateFormat.getDateTimeInstance().format(new Date()) + "\" rootOid=\"" + rootOid + "\" maxIndex=\"" + _getMaxUsedIndex() + "\" nItems=\"" + getNumberOfObjectsInDb() + "\">\n");
		writer.write("<metadata>\n");
		for (Iterator it = dbClassByName.values().iterator(); it.hasNext();) {
			DbClassImpl dbClass = (DbClassImpl) it.next();
			writer.write("\t<class name=\"" + dbClass.getClassName() + "\" id=\"" + dbClass.getOid() + "\">\n");
			int nFields = dbClass.getNumberOfFields();
			for (int i = 0; i < nFields; i++) {
				writer.write("\t\t<field name=\"" + dbClass.getFieldName(i) + "\" type=\"" + dbClass.getFieldType(i) + "\"/>\n");
			}
			writer.write("\t</class>\n");
		}
		writer.write("</metadata>\n");
		writer.write("<objects>\n");
		for (Iterator it = dbClassByName.values().iterator(); it.hasNext();) {
			DbClassImpl dbClass = (DbClassImpl) it.next();
			String fieldNames[] = dbClass.getFieldNames();
			byte fieldTypes[] = dbClass.getFieldTypes();
			String className = dbClass.getClassName();
			for (DbObject obj = dbClass.firstInExtent; obj != null; obj = obj.next) {
				writer.write("\t<object class=\"" + className + "\"  id=\"" + DbOid.toString(obj.indexId, obj.versionId) + "\">\n");
				final int len = fieldNames.length;
				for (int i = 0; i < len; i++) {
					String value = objectMapper.getXMLValue(fieldTypes, i, dbClass, obj);
					writer.write("\t\t<field name=\"" + fieldNames[i] + "\" value=\"" + value + "\" />\n");
				}
				writer.write("\t</object>\n");
			}
		}
		writer.write("</objects>\n");
		writer.write("</paindb>");
		writer.close();
	}

	/**
	 * Creates new database instance, fills it with data from given XML file.<br>
	 * all objects keeps identity.
	 * @param xmlFileName - XML file with database image to import
	 * @param resultDbFileName - name if result database file.
	 * @return new database instance  
	 */
	public static PainDB importFromXml(String xmlFileName, String resultDbFileName) throws Exception {
		File f = new File(xmlFileName);
		if (!f.exists()) {
			throw new FileNotFoundException(f.getAbsolutePath());
		}
		if (f.isDirectory()) {
			throw new RuntimeException("specified file is directory:" + f.getAbsolutePath());
		}
		boolean ok = false;
		PainDB db = new PainDB(resultDbFileName);
		try {
			SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
			final FileInputStream is = new FileInputStream(f);
			try {
//				long time = System.currentTimeMillis();
				final DbObjectMapper.XMLImportDataHandler xmlImportDataHandler = db.objectMapper.createXmlImportDataHandler();
				parser.parse(new BufferedInputStream(is, 100 * 1000), xmlImportDataHandler);
//				System.out.println("time:" + (System.currentTimeMillis() - time));
				db._startup_initReferences();
				if (!"null".equals(xmlImportDataHandler.rootOid)) {
					db.rootIndex = new DbOid(xmlImportDataHandler.rootOid).indexId;
				}
				db.maxUsedIndexId = xmlImportDataHandler.maxUsedIndexId;
				db.freeIndexIds.clear();
				db.freeIndexIds.ensureCapacity(db.objects.length - xmlImportDataHandler.nItems + 1);
				DbObject[] objs = db.objects;
				DbIntBuffer dirty = db.dirty;
				for (int i = objs.length; --i >= 0;) {
					final DbObject obj = objs[i];
					if (obj == null) {
						db.freeIndexIds.add(i);
					} else {
						obj.globalState = DbConstants.STATE_OBJ_DIRTY;
						dirty.add(obj.indexId);
					}
				}
				if (xmlImportDataHandler.nItems != db.getNumberOfObjectsInDb()) {
					throw new RuntimeException("import checksum error!");
				}
//				System.out.println("Root:" + xmlImportDataHandler.rootOid);
			} finally {
				is.close();
			}
			ok = true;
		} finally {
			if (!ok) {
				db.close();
				new File(resultDbFileName).delete();
			}
		}
		return db;
	}

	DbRuntimeClass getDbClassByClassName(String className) {
		return (DbRuntimeClass) dbClassByName.get(className);
	}

	private int _getMaxUsedIndex() {
		DbObject[] objs = objects;
		for (int i = objs.length; --i >= 0;) {
			if (objs[i] != null) {
				return i;
			}
		}
		return 0;
	}

}