/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 *
 */
package org.hibernate.engine;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.map.ReferenceMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.AssertionFailure;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.MappingException;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.PersistentObjectException;
import org.hibernate.TransientObjectException;
import org.hibernate.engine.loading.LoadContexts;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.tuple.ElementWrapper;
import org.hibernate.util.IdentityMap;
import org.hibernate.util.MarkerObject;

/**
 * A <tt>PersistenceContext</tt> represents the state of persistent "stuff" which
 * Hibernate is tracking.  This includes persistent entities, collections,
 * as well as proxies generated.
 * </p>
 * There is meant to be a one-to-one correspondence between a SessionImpl and
 * a PersistentContext.  The SessionImpl uses the PersistentContext to track
 * the current state of its context.  Event-listeners then use the
 * PersistentContext to drive their processing.
 *
 * @author Steve Ebersole
 */
public class StatefulPersistenceContext implements PersistenceContext {

	public static final Object NO_ROW = new MarkerObject( "NO_ROW" );

	private static final Logger log = LoggerFactory.getLogger( StatefulPersistenceContext.class );
	private static final Logger PROXY_WARN_LOG = LoggerFactory.getLogger( StatefulPersistenceContext.class.getName() + ".ProxyWarnLog" );
	private static final int INIT_COLL_SIZE = 8;

	private SessionImplementor session;
	
	// Loaded entity instances, by EntityKey
	private Map entitiesByKey;

	// Loaded entity instances, by EntityUniqueKey
	private Map entitiesByUniqueKey;
	
	// Identity map of EntityEntry instances, by the entity instance
	private Map entityEntries;
	
	// Entity proxies, by EntityKey
	private Map proxiesByKey;
	
	// Snapshots of current database state for entities
	// that have *not* been loaded
	private Map entitySnapshotsByKey;
	
	// Identity map of array holder ArrayHolder instances, by the array instance
	private Map arrayHolders;
	
	// Identity map of CollectionEntry instances, by the collection wrapper
	private Map collectionEntries;
	
	// Collection wrappers, by the CollectionKey
	private Map collectionsByKey; //key=CollectionKey, value=PersistentCollection
	
	// Set of EntityKeys of deleted objects
	private HashSet nullifiableEntityKeys;
	
	// properties that we have tried to load, and not found in the database
	private HashSet nullAssociations;
	
	// A list of collection wrappers that were instantiating during result set
	// processing, that we will need to initialize at the end of the query
	private List nonlazyCollections;
	
	// A container for collections we load up when the owning entity is not
	// yet loaded ... for now, this is purely transient!
	private Map unownedCollections;
	
	// Parent entities cache by their child for cascading
	// May be empty or not contains all relation 
	private Map parentsByChild;
	
	private int cascading = 0;
	private int loadCounter = 0;
	private boolean flushing = false;
	
	private boolean defaultReadOnly = false;
	private boolean hasNonReadOnlyEntities = false;

	private LoadContexts loadContexts;
	private BatchFetchQueue batchFetchQueue;



	/**
	 * Constructs a PersistentContext, bound to the given session.
	 *
	 * @param session The session "owning" this context.
	 */
	public StatefulPersistenceContext(SessionImplementor session) {
		this.session = session;

		entitiesByKey = new HashMap( INIT_COLL_SIZE );
		entitiesByUniqueKey = new HashMap( INIT_COLL_SIZE );
		proxiesByKey = new ReferenceMap( ReferenceMap.HARD, ReferenceMap.WEAK );
		entitySnapshotsByKey = new HashMap( INIT_COLL_SIZE );

		entityEntries = IdentityMap.instantiateSequenced( INIT_COLL_SIZE );
		collectionEntries = IdentityMap.instantiateSequenced( INIT_COLL_SIZE );
		collectionsByKey = new HashMap( INIT_COLL_SIZE );
		arrayHolders = IdentityMap.instantiate( INIT_COLL_SIZE );
		parentsByChild = IdentityMap.instantiateSequenced( INIT_COLL_SIZE );
		
		nullifiableEntityKeys = new HashSet();

		initTransientState();
	}

	private void initTransientState() {
		nullAssociations = new HashSet( INIT_COLL_SIZE );
		nonlazyCollections = new ArrayList( INIT_COLL_SIZE );
	}

	public boolean isStateless() {
		return false;
	}
	
	public SessionImplementor getSession() {
		return session;
	}

	public LoadContexts getLoadContexts() {
		if ( loadContexts == null ) {
			loadContexts = new LoadContexts( this );
		}
		return loadContexts;
	}

	public void addUnownedCollection(CollectionKey key, PersistentCollection collection) {
		if (unownedCollections==null) {
			unownedCollections = new HashMap(8);
		}
		unownedCollections.put(key, collection);
	}
	
	public PersistentCollection useUnownedCollection(CollectionKey key) {
		if (unownedCollections==null) {
			return null;
		}
		else {
			return (PersistentCollection) unownedCollections.remove(key);
		}
	}
	
	/**
	 * Get the <tt>BatchFetchQueue</tt>, instantiating one if
	 * necessary.
	 */
	public BatchFetchQueue getBatchFetchQueue() {
		if (batchFetchQueue==null) {
			batchFetchQueue = new BatchFetchQueue(this);
		}
		return batchFetchQueue;
	}

	public void clear() {
		Iterator itr = proxiesByKey.values().iterator();
		while ( itr.hasNext() ) {
			final LazyInitializer li = ( ( HibernateProxy ) itr.next() ).getHibernateLazyInitializer();
			li.unsetSession();
		}
		Map.Entry[] collectionEntryArray = IdentityMap.concurrentEntries( collectionEntries );
		for ( int i = 0; i < collectionEntryArray.length; i++ ) {
			( ( PersistentCollection ) collectionEntryArray[i].getKey() ).unsetSession( getSession() );
		}
		arrayHolders.clear();
		entitiesByKey.clear();
		entitiesByUniqueKey.clear();
		entityEntries.clear();
		parentsByChild.clear();
		entitySnapshotsByKey.clear();
		collectionsByKey.clear();
		collectionEntries.clear();
		if ( unownedCollections != null ) {
			unownedCollections.clear();
		}
		proxiesByKey.clear();
		nullifiableEntityKeys.clear();
		if ( batchFetchQueue != null ) {
			batchFetchQueue.clear();
		}
		// defaultReadOnly is unaffected by clear()
		hasNonReadOnlyEntities = false;
		if ( loadContexts != null ) {
			loadContexts.cleanup();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isDefaultReadOnly() {
		return defaultReadOnly;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setDefaultReadOnly(boolean defaultReadOnly) {
		this.defaultReadOnly = defaultReadOnly;
	}

	public boolean hasNonReadOnlyEntities() {
		return hasNonReadOnlyEntities;
	}
	
	public void setEntryStatus(EntityEntry entry, Status status) {
		entry.setStatus(status);
		setHasNonReadOnlyEnties(status);
	}
	
	private void setHasNonReadOnlyEnties(Status status) {
		if ( status==Status.DELETED || status==Status.MANAGED || status==Status.SAVING ) {
			hasNonReadOnlyEntities = true;
		}
	}

	public void afterTransactionCompletion() {
		cleanUpInsertedKeysAfterTransaction();
		// Downgrade locks
		Iterator iter = entityEntries.values().iterator();
		while ( iter.hasNext() ) {
			( (EntityEntry) iter.next() ).setLockMode(LockMode.NONE);
		}
	}

	/**
	 * Get the current state of the entity as known to the underlying
	 * database, or null if there is no corresponding row 
	 */
	public Object[] getDatabaseSnapshot(Serializable id, EntityPersister persister)
	throws HibernateException {
		EntityKey key = new EntityKey( id, persister, session.getEntityMode() );
		Object cached = entitySnapshotsByKey.get(key);
		if (cached!=null) {
			return cached==NO_ROW ? null : (Object[]) cached;
		}
		else {
			Object[] snapshot = persister.getDatabaseSnapshot( id, session );
			entitySnapshotsByKey.put( key, snapshot==null ? NO_ROW : snapshot );
			return snapshot;
		}
	}

	public Object[] getNaturalIdSnapshot(Serializable id, EntityPersister persister)
	throws HibernateException {
		if ( !persister.hasNaturalIdentifier() ) {
			return null;
		}

		// if the natural-id is marked as non-mutable, it is not retrieved during a
		// normal database-snapshot operation...
		int[] props = persister.getNaturalIdentifierProperties();
		boolean[] updateable = persister.getPropertyUpdateability();
		boolean allNatualIdPropsAreUpdateable = true;
		for ( int i = 0; i < props.length; i++ ) {
			if ( !updateable[ props[i] ] ) {
				allNatualIdPropsAreUpdateable = false;
				break;
			}
		}

		if ( allNatualIdPropsAreUpdateable ) {
			// do this when all the properties are updateable since there is
			// a certain likelihood that the information will already be
			// snapshot-cached.
			Object[] entitySnapshot = getDatabaseSnapshot( id, persister );
			if ( entitySnapshot == NO_ROW ) {
				return null;
			}
			Object[] naturalIdSnapshot = new Object[ props.length ];
			for ( int i = 0; i < props.length; i++ ) {
				naturalIdSnapshot[i] = entitySnapshot[ props[i] ];
			}
			return naturalIdSnapshot;
		}
		else {
			return persister.getNaturalIdentifierSnapshot( id, session );
		}
	}

	/**
	 * Retrieve the cached database snapshot for the requested entity key.
	 * <p/>
	 * This differs from {@link #getDatabaseSnapshot} is two important respects:<ol>
	 * <li>no snapshot is obtained from the database if not already cached</li>
	 * <li>an entry of {@link #NO_ROW} here is interpretet as an exception</li>
	 * </ol>
	 * @param key The entity key for which to retrieve the cached snapshot
	 * @return The cached snapshot
	 * @throws IllegalStateException if the cached snapshot was == {@link #NO_ROW}.
	 */
	public Object[] getCachedDatabaseSnapshot(EntityKey key) {
		Object snapshot = entitySnapshotsByKey.get( key );
		if ( snapshot == NO_ROW ) {
			throw new IllegalStateException( "persistence context reported no row snapshot for " + MessageHelper.infoString( key.getEntityName(), key.getIdentifier() ) );
		}
		return ( Object[] ) snapshot;
	}

	/*public void removeDatabaseSnapshot(EntityKey key) {
		entitySnapshotsByKey.remove(key);
	}*/

	public void addEntity(EntityKey key, Object entity) {
		entitiesByKey.put(key, entity);
		getBatchFetchQueue().removeBatchLoadableEntityKey(key);
	}

	/**
	 * Get the entity instance associated with the given 
	 * <tt>EntityKey</tt>
	 */
	public Object getEntity(EntityKey key) {
		return entitiesByKey.get(key);
	}

	public boolean containsEntity(EntityKey key) {
		return entitiesByKey.containsKey(key);
	}

	/**
	 * Remove an entity from the session cache, also clear
	 * up other state associated with the entity, all except
	 * for the <tt>EntityEntry</tt>
	 */
	public Object removeEntity(EntityKey key) {
		Object entity = entitiesByKey.remove(key);
		Iterator iter = entitiesByUniqueKey.values().iterator();
		while ( iter.hasNext() ) {
			if ( iter.next()==entity ) iter.remove();
		}
		// Clear all parent cache
		parentsByChild.clear();
		entitySnapshotsByKey.remove(key);
		nullifiableEntityKeys.remove(key);
		getBatchFetchQueue().removeBatchLoadableEntityKey(key);
		getBatchFetchQueue().removeSubselect(key);
		return entity;
	}

	/**
	 * Get an entity cached by unique key
	 */
	public Object getEntity(EntityUniqueKey euk) {
		return entitiesByUniqueKey.get(euk);
	}

	/**
	 * Add an entity to the cache by unique key
	 */
	public void addEntity(EntityUniqueKey euk, Object entity) {
		entitiesByUniqueKey.put(euk, entity);
	}

	/**
	 * Retreive the EntityEntry representation of the given entity.
	 *
	 * @param entity The entity for which to locate the EntityEntry.
	 * @return The EntityEntry for the given entity.
	 */
	public EntityEntry getEntry(Object entity) {
		return (EntityEntry) entityEntries.get(entity);
	}

	/**
	 * Remove an entity entry from the session cache
	 */
	public EntityEntry removeEntry(Object entity) {
		return (EntityEntry) entityEntries.remove(entity);
	}

	/**
	 * Is there an EntityEntry for this instance?
	 */
	public boolean isEntryFor(Object entity) {
		return entityEntries.containsKey(entity);
	}

	/**
	 * Get the collection entry for a persistent collection
	 */
	public CollectionEntry getCollectionEntry(PersistentCollection coll) {
		return (CollectionEntry) collectionEntries.get(coll);
	}

	/**
	 * Adds an entity to the internal caches.
	 */
	public EntityEntry addEntity(
			final Object entity,
			final Status status,
			final Object[] loadedState,
			final EntityKey entityKey,
			final Object version,
			final LockMode lockMode,
			final boolean existsInDatabase,
			final EntityPersister persister,
			final boolean disableVersionIncrement, 
			boolean lazyPropertiesAreUnfetched
	) {
		
		addEntity( entityKey, entity );
		
		return addEntry(
				entity,
				status,
				loadedState,
				null,
				entityKey.getIdentifier(),
				version,
				lockMode,
				existsInDatabase,
				persister,
				disableVersionIncrement, 
				lazyPropertiesAreUnfetched
			);
	}


	/**
	 * Generates an appropriate EntityEntry instance and adds it 
	 * to the event source's internal caches.
	 */
	public EntityEntry addEntry(
			final Object entity,
			final Status status,
			final Object[] loadedState,
			final Object rowId,
			final Serializable id,
			final Object version,
			final LockMode lockMode,
			final boolean existsInDatabase,
			final EntityPersister persister,
			final boolean disableVersionIncrement, 
			boolean lazyPropertiesAreUnfetched) {
		
		EntityEntry e = new EntityEntry(
				status,
				loadedState,
				rowId,
				id,
				version,
				lockMode,
				existsInDatabase,
				persister,
				session.getEntityMode(),
				disableVersionIncrement,
				lazyPropertiesAreUnfetched
			);
		entityEntries.put(entity, e);
		
		setHasNonReadOnlyEnties(status);
		return e;
	}

	public boolean containsCollection(PersistentCollection collection) {
		return collectionEntries.containsKey(collection);
	}

	public boolean containsProxy(Object entity) {
		return proxiesByKey.containsValue( entity );
	}
	
	/**
	 * Takes the given object and, if it represents a proxy, reassociates it with this event source.
	 *
	 * @param value The possible proxy to be reassociated.
	 * @return Whether the passed value represented an actual proxy which got initialized.
	 * @throws MappingException
	 */
	public boolean reassociateIfUninitializedProxy(Object value) throws MappingException {
		if ( value instanceof ElementWrapper ) {
			value = ( (ElementWrapper) value ).getElement();
		}
		
		if ( !Hibernate.isInitialized(value) ) {
			HibernateProxy proxy = (HibernateProxy) value;
			LazyInitializer li = proxy.getHibernateLazyInitializer();
			reassociateProxy(li, proxy);
			return true;
		}
		else {
			return false;
		}
	}

	/**
	 * If a deleted entity instance is re-saved, and it has a proxy, we need to
	 * reset the identifier of the proxy 
	 */
	public void reassociateProxy(Object value, Serializable id) throws MappingException {
		if ( value instanceof ElementWrapper ) {
			value = ( (ElementWrapper) value ).getElement();
		}
		
		if ( value instanceof HibernateProxy ) {
			if ( log.isDebugEnabled() ) log.debug("setting proxy identifier: " + id);
			HibernateProxy proxy = (HibernateProxy) value;
			LazyInitializer li = proxy.getHibernateLazyInitializer();
			li.setIdentifier(id);
			reassociateProxy(li, proxy);
		}
	}

	/**
	 * Associate a proxy that was instantiated by another session with this session
	 *
	 * @param li The proxy initializer.
	 * @param proxy The proxy to reassociate.
	 */
	private void reassociateProxy(LazyInitializer li, HibernateProxy proxy) {
		if ( li.getSession() != this.getSession() ) {
			EntityPersister persister = session.getFactory().getEntityPersister( li.getEntityName() );
			EntityKey key = new EntityKey( li.getIdentifier(), persister, session.getEntityMode() );
		  	// any earlier proxy takes precedence
			if ( !proxiesByKey.containsKey( key ) ) {
				proxiesByKey.put( key, proxy );
			}
			proxy.getHibernateLazyInitializer().setSession( session );
		}
	}

	/**
	 * Get the entity instance underlying the given proxy, throwing
	 * an exception if the proxy is uninitialized. If the given object
	 * is not a proxy, simply return the argument.
	 */
	public Object unproxy(Object maybeProxy) throws HibernateException {
		if ( maybeProxy instanceof ElementWrapper ) {
			maybeProxy = ( (ElementWrapper) maybeProxy ).getElement();
		}
		
		if ( maybeProxy instanceof HibernateProxy ) {
			HibernateProxy proxy = (HibernateProxy) maybeProxy;
			LazyInitializer li = proxy.getHibernateLazyInitializer();
			if ( li.isUninitialized() ) {
				throw new PersistentObjectException(
						"object was an uninitialized proxy for " +
						li.getEntityName()
				);
			}
			return li.getImplementation(); //unwrap the object
		}
		else {
			return maybeProxy;
		}
	}

	/**
	 * Possibly unproxy the given reference and reassociate it with the current session.
	 *
	 * @param maybeProxy The reference to be unproxied if it currently represents a proxy.
	 * @return The unproxied instance.
	 * @throws HibernateException
	 */
	public Object unproxyAndReassociate(Object maybeProxy) throws HibernateException {
		if ( maybeProxy instanceof ElementWrapper ) {
			maybeProxy = ( (ElementWrapper) maybeProxy ).getElement();
		}
		
		if ( maybeProxy instanceof HibernateProxy ) {
			HibernateProxy proxy = (HibernateProxy) maybeProxy;
			LazyInitializer li = proxy.getHibernateLazyInitializer();
			reassociateProxy(li, proxy);
			return li.getImplementation(); //initialize + unwrap the object
		}
		else {
			return maybeProxy;
		}
	}

	/**
	 * Attempts to check whether the given key represents an entity already loaded within the
	 * current session.
	 * @param object The entity reference against which to perform the uniqueness check.
	 * @throws HibernateException
	 */
	public void checkUniqueness(EntityKey key, Object object) throws HibernateException {
		Object entity = getEntity(key);
		if ( entity == object ) {
			throw new AssertionFailure( "object already associated, but no entry was found" );
		}
		if ( entity != null ) {
			throw new NonUniqueObjectException( key.getIdentifier(), key.getEntityName() );
		}
	}

	/**
	 * If the existing proxy is insufficiently "narrow" (derived), instantiate a new proxy
	 * and overwrite the registration of the old one. This breaks == and occurs only for
	 * "class" proxies rather than "interface" proxies. Also init the proxy to point to
	 * the given target implementation if necessary.
	 *
	 * @param proxy The proxy instance to be narrowed.
	 * @param persister The persister for the proxied entity.
	 * @param key The internal cache key for the proxied entity.
	 * @param object (optional) the actual proxied entity instance.
	 * @return An appropriately narrowed instance.
	 * @throws HibernateException
	 */
	public Object narrowProxy(Object proxy, EntityPersister persister, EntityKey key, Object object)
	throws HibernateException {
		
		boolean alreadyNarrow = persister.getConcreteProxyClass( session.getEntityMode() )
				.isAssignableFrom( proxy.getClass() );
		
		if ( !alreadyNarrow ) {
			if ( PROXY_WARN_LOG.isWarnEnabled() ) {
				PROXY_WARN_LOG.warn(
						"Narrowing proxy to " +
						persister.getConcreteProxyClass( session.getEntityMode() ) +
						" - this operation breaks =="
				);
			}

			if ( object != null ) {
				proxiesByKey.remove(key);
				return object; //return the proxied object
			}
			else {
				proxy = persister.createProxy( key.getIdentifier(), session );
				Object proxyOrig = proxiesByKey.put(key, proxy); //overwrite old proxy
				if ( proxyOrig != null ) {
					if ( ! ( proxyOrig instanceof HibernateProxy ) ) {
						throw new AssertionFailure(
								"proxy not of type HibernateProxy; it is " + proxyOrig.getClass()
						);
					}
					// set the read-only/modifiable mode in the new proxy to what it was in the original proxy
					boolean readOnlyOrig = ( ( HibernateProxy ) proxyOrig ).getHibernateLazyInitializer().isReadOnly();
					( ( HibernateProxy ) proxy ).getHibernateLazyInitializer().setReadOnly( readOnlyOrig );
				}
				return proxy;
			}			
		}
		else {
			
			if ( object != null ) {
				LazyInitializer li = ( (HibernateProxy) proxy ).getHibernateLazyInitializer();
				li.setImplementation(object);
			}
			
			return proxy;
			
		}
		
	}

	/**
	 * Return the existing proxy associated with the given <tt>EntityKey</tt>, or the
	 * third argument (the entity associated with the key) if no proxy exists. Init
	 * the proxy to the target implementation, if necessary.
	 */
	public Object proxyFor(EntityPersister persister, EntityKey key, Object impl) 
	throws HibernateException {
		if ( !persister.hasProxy() ) return impl;
		Object proxy = proxiesByKey.get(key);
		if ( proxy != null ) {
			return narrowProxy(proxy, persister, key, impl);
		}
		else {
			return impl;
		}
	}

	/**
	 * Return the existing proxy associated with the given <tt>EntityKey</tt>, or the
	 * argument (the entity associated with the key) if no proxy exists.
	 * (slower than the form above)
	 */
	public Object proxyFor(Object impl) throws HibernateException {
		EntityEntry e = getEntry(impl);
		return proxyFor( e.getPersister(), e.getEntityKey(), impl );
	}

	/**
	 * Get the entity that owns this persistent collection
	 */
	public Object getCollectionOwner(Serializable key, CollectionPersister collectionPersister) throws MappingException {
		return getEntity( new EntityKey( key, collectionPersister.getOwnerEntityPersister(), session.getEntityMode() ) );
	}

	/**
	 * Get the entity that owned this persistent collection when it was loaded
	 *
	 * @param collection The persistent collection
	 * @return the owner, if its entity ID is available from the collection's loaded key
	 * and the owner entity is in the persistence context; otherwise, returns null
	 */
	public Object getLoadedCollectionOwnerOrNull(PersistentCollection collection) {
		CollectionEntry ce = getCollectionEntry( collection );
		if ( ce.getLoadedPersister() == null ) {
			return null; // early exit...
		}
		Object loadedOwner = null;
		// TODO: an alternative is to check if the owner has changed; if it hasn't then
		// return collection.getOwner()
		Serializable entityId = getLoadedCollectionOwnerIdOrNull( ce );
		if ( entityId != null ) {
			loadedOwner = getCollectionOwner( entityId, ce.getLoadedPersister() );
		}
		return loadedOwner;
	}

	/**
	 * Get the ID for the entity that owned this persistent collection when it was loaded
	 *
	 * @param collection The persistent collection
	 * @return the owner ID if available from the collection's loaded key; otherwise, returns null
	 */
	public Serializable getLoadedCollectionOwnerIdOrNull(PersistentCollection collection) {
		return getLoadedCollectionOwnerIdOrNull( getCollectionEntry( collection ) );
	}

	/**
	 * Get the ID for the entity that owned this persistent collection when it was loaded
	 *
	 * @param ce The collection entry
	 * @return the owner ID if available from the collection's loaded key; otherwise, returns null
	 */
	private Serializable getLoadedCollectionOwnerIdOrNull(CollectionEntry ce) {
		if ( ce == null || ce.getLoadedKey() == null || ce.getLoadedPersister() == null ) {
			return null;
		}
		// TODO: an alternative is to check if the owner has changed; if it hasn't then
		// get the ID from collection.getOwner()
		return ce.getLoadedPersister().getCollectionType().getIdOfOwnerOrNull( ce.getLoadedKey(), session );
	}

	/**
	 * add a collection we just loaded up (still needs initializing)
	 */
	public void addUninitializedCollection(CollectionPersister persister, PersistentCollection collection, Serializable id) {
		CollectionEntry ce = new CollectionEntry(collection, persister, id, flushing);
		addCollection(collection, ce, id);
	}

	/**
	 * add a detached uninitialized collection
	 */
	public void addUninitializedDetachedCollection(CollectionPersister persister, PersistentCollection collection) {
		CollectionEntry ce = new CollectionEntry( persister, collection.getKey() );
		addCollection( collection, ce, collection.getKey() );
	}

	/**
	 * Add a new collection (ie. a newly created one, just instantiated by the
	 * application, with no database state or snapshot)
	 * @param collection The collection to be associated with the persistence context
	 */
	public void addNewCollection(CollectionPersister persister, PersistentCollection collection)
	throws HibernateException {
		addCollection(collection, persister);
	}

	/**
	 * Add an collection to the cache, with a given collection entry.
	 *
	 * @param coll The collection for which we are adding an entry.
	 * @param entry The entry representing the collection.
	 * @param key The key of the collection's entry.
	 */
	private void addCollection(PersistentCollection coll, CollectionEntry entry, Serializable key) {
		collectionEntries.put( coll, entry );
		CollectionKey collectionKey = new CollectionKey( entry.getLoadedPersister(), key, session.getEntityMode() );
		PersistentCollection old = ( PersistentCollection ) collectionsByKey.put( collectionKey, coll );
		if ( old != null ) {
			if ( old == coll ) {
				throw new AssertionFailure("bug adding collection twice");
			}
			// or should it actually throw an exception?
			old.unsetSession( session );
			collectionEntries.remove( old );
			// watch out for a case where old is still referenced
			// somewhere in the object graph! (which is a user error)
		}
	}

	/**
	 * Add a collection to the cache, creating a new collection entry for it
	 *
	 * @param collection The collection for which we are adding an entry.
	 * @param persister The collection persister
	 */
	private void addCollection(PersistentCollection collection, CollectionPersister persister) {
		CollectionEntry ce = new CollectionEntry( persister, collection );
		collectionEntries.put( collection, ce );
	}

	/**
	 * add an (initialized) collection that was created by another session and passed
	 * into update() (ie. one with a snapshot and existing state on the database)
	 */
	public void addInitializedDetachedCollection(CollectionPersister collectionPersister, PersistentCollection collection) 
	throws HibernateException {
		if ( collection.isUnreferenced() ) {
			//treat it just like a new collection
			addCollection( collection, collectionPersister );
		}
		else {
			CollectionEntry ce = new CollectionEntry( collection, session.getFactory() );
			addCollection( collection, ce, collection.getKey() );
		}
	}

	/**
	 * add a collection we just pulled out of the cache (does not need initializing)
	 */
	public CollectionEntry addInitializedCollection(CollectionPersister persister, PersistentCollection collection, Serializable id)
	throws HibernateException {
		CollectionEntry ce = new CollectionEntry(collection, persister, id, flushing);
		ce.postInitialize(collection);
		addCollection(collection, ce, id);
		return ce;
	}
	
	/**
	 * Get the collection instance associated with the <tt>CollectionKey</tt>
	 */
	public PersistentCollection getCollection(CollectionKey collectionKey) {
		return (PersistentCollection) collectionsByKey.get(collectionKey);
	}
	
	/**
	 * Register a collection for non-lazy loading at the end of the
	 * two-phase load
	 */
	public void addNonLazyCollection(PersistentCollection collection) {
		nonlazyCollections.add(collection);
	}

	/**
	 * Force initialization of all non-lazy collections encountered during
	 * the current two-phase load (actually, this is a no-op, unless this
	 * is the "outermost" load)
	 */
	public void initializeNonLazyCollections() throws HibernateException {
		if ( loadCounter == 0 ) {
			log.debug( "initializing non-lazy collections" );
			//do this work only at the very highest level of the load
			loadCounter++; //don't let this method be called recursively
			try {
				int size;
				while ( ( size = nonlazyCollections.size() ) > 0 ) {
					//note that each iteration of the loop may add new elements
					( (PersistentCollection) nonlazyCollections.remove( size - 1 ) ).forceInitialization();
				}
			}
			finally {
				loadCounter--;
				clearNullProperties();
			}
		}
	}


	/**
	 * Get the <tt>PersistentCollection</tt> object for an array
	 */
	public PersistentCollection getCollectionHolder(Object array) {
		return (PersistentCollection) arrayHolders.get(array);
	}

	/**
	 * Register a <tt>PersistentCollection</tt> object for an array.
	 * Associates a holder with an array - MUST be called after loading 
	 * array, since the array instance is not created until endLoad().
	 */
	public void addCollectionHolder(PersistentCollection holder) {
		//TODO:refactor + make this method private
		arrayHolders.put( holder.getValue(), holder );
	}

	public PersistentCollection removeCollectionHolder(Object array) {
		return (PersistentCollection) arrayHolders.remove(array);
	}

	/**
	 * Get the snapshot of the pre-flush collection state
	 */
	public Serializable getSnapshot(PersistentCollection coll) {
		return getCollectionEntry(coll).getSnapshot();
	}

	/**
	 * Get the collection entry for a collection passed to filter,
	 * which might be a collection wrapper, an array, or an unwrapped
	 * collection. Return null if there is no entry.
	 */
	public CollectionEntry getCollectionEntryOrNull(Object collection) {
		PersistentCollection coll;
		if ( collection instanceof PersistentCollection ) {
			coll = (PersistentCollection) collection;
			//if (collection==null) throw new TransientObjectException("Collection was not yet persistent");
		}
		else {
			coll = getCollectionHolder(collection);
			if ( coll == null ) {
				//it might be an unwrapped collection reference!
				//try to find a wrapper (slowish)
				Iterator wrappers = IdentityMap.keyIterator(collectionEntries);
				while ( wrappers.hasNext() ) {
					PersistentCollection pc = (PersistentCollection) wrappers.next();
					if ( pc.isWrapper(collection) ) {
						coll = pc;
						break;
					}
				}
			}
		}

		return (coll == null) ? null : getCollectionEntry(coll);
	}

	/**
	 * Get an existing proxy by key
	 */
	public Object getProxy(EntityKey key) {
		return proxiesByKey.get(key);
	}

	/**
	 * Add a proxy to the session cache
	 */
	public void addProxy(EntityKey key, Object proxy) {
		proxiesByKey.put(key, proxy);
	}

	/**
	 * Remove a proxy from the session cache.
	 * <p/>
	 * Additionally, ensure that any load optimization references
	 * such as batch or subselect loading get cleaned up as well.
	 *
	 * @param key The key of the entity proxy to be removed
	 * @return The proxy reference.
	 */
	public Object removeProxy(EntityKey key) {
		if ( batchFetchQueue != null ) {
			batchFetchQueue.removeBatchLoadableEntityKey( key );
			batchFetchQueue.removeSubselect( key );
		}
		return proxiesByKey.remove( key );
	}

	/**
	 * Record the fact that an entity does not exist in the database
	 * 
	 * @param key the primary key of the entity
	 */
	/*public void addNonExistantEntityKey(EntityKey key) {
		nonExistantEntityKeys.add(key);
	}*/

	/**
	 * Record the fact that an entity does not exist in the database
	 * 
	 * @param key a unique key of the entity
	 */
	/*public void addNonExistantEntityUniqueKey(EntityUniqueKey key) {
		nonExistentEntityUniqueKeys.add(key);
	}*/

	/*public void removeNonExist(EntityKey key) {
		nonExistantEntityKeys.remove(key);
	}*/

	/** 
	 * Retrieve the set of EntityKeys representing nullifiable references
	 */
	public HashSet getNullifiableEntityKeys() {
		return nullifiableEntityKeys;
	}

	public Map getEntitiesByKey() {
		return entitiesByKey;
	}

	public Map getProxiesByKey() {
		return proxiesByKey;
	}

	public Map getEntityEntries() {
		return entityEntries;
	}

	public Map getCollectionEntries() {
		return collectionEntries;
	}

	public Map getCollectionsByKey() {
		return collectionsByKey;
	}

	/**
	 * Do we already know that the entity does not exist in the
	 * database?
	 */
	/*public boolean isNonExistant(EntityKey key) {
		return nonExistantEntityKeys.contains(key);
	}*/

	/**
	 * Do we already know that the entity does not exist in the
	 * database?
	 */
	/*public boolean isNonExistant(EntityUniqueKey key) {
		return nonExistentEntityUniqueKeys.contains(key);
	}*/

	public int getCascadeLevel() {
		return cascading;
	}

	public int incrementCascadeLevel() {
		return ++cascading;
	}

	public int decrementCascadeLevel() {
		return --cascading;
	}

	public boolean isFlushing() {
		return flushing;
	}

	public void setFlushing(boolean flushing) {
		this.flushing = flushing;
	}

	/**
	 * Call this before begining a two-phase load
	 */
	public void beforeLoad() {
		loadCounter++;
	}

	/**
	 * Call this after finishing a two-phase load
	 */
	public void afterLoad() {
		loadCounter--;
	}

	public boolean isLoadFinished() {
		return loadCounter == 0;
	}
	/**
	 * Returns a string representation of the object.
	 *
	 * @return a string representation of the object.
	 */
	public String toString() {
		return new StringBuffer()
				.append("PersistenceContext[entityKeys=")
				.append(entitiesByKey.keySet())
				.append(",collectionKeys=")
				.append(collectionsByKey.keySet())
				.append("]")
				.toString();
	}

	/**
	 * Search <tt>this</tt> persistence context for an associated entity instance which is considered the "owner" of
	 * the given <tt>childEntity</tt>, and return that owner's id value.  This is performed in the scenario of a
	 * uni-directional, non-inverse one-to-many collection (which means that the collection elements do not maintain
	 * a direct reference to the owner).
	 * <p/>
	 * As such, the processing here is basically to loop over every entity currently associated with this persistence
	 * context and for those of the correct entity (sub) type to extract its collection role property value and see
	 * if the child is contained within that collection.  If so, we have found the owner; if not, we go on.
	 * <p/>
	 * Also need to account for <tt>mergeMap</tt> which acts as a local copy cache managed for the duration of a merge
	 * operation.  It represents a map of the detached entity instances pointing to the corresponding managed instance.
	 *
	 * @param entityName The entity name for the entity type which would own the child
	 * @param propertyName The name of the property on the owning entity type which would name this child association.
	 * @param childEntity The child entity instance for which to locate the owner instance id.
	 * @param mergeMap A map of non-persistent instances from an on-going merge operation (possibly null).
	 *
	 * @return The id of the entityName instance which is said to own the child; null if an appropriate owner not
	 * located.
	 */
	public Serializable getOwnerId(String entityName, String propertyName, Object childEntity, Map mergeMap) {
		final String collectionRole = entityName + '.' + propertyName;
		final EntityPersister persister = session.getFactory().getEntityPersister( entityName );
		final CollectionPersister collectionPersister = session.getFactory().getCollectionPersister( collectionRole );

		// try cache lookup first
		Object parent = parentsByChild.get( childEntity );
		if ( parent != null ) {
			final EntityEntry entityEntry = ( EntityEntry ) entityEntries.get( parent );
			//there maybe more than one parent, filter by type
			if ( 	persister.isSubclassEntityName(entityEntry.getEntityName() )
					&& isFoundInParent( propertyName, childEntity, persister, collectionPersister, parent ) ) {
				return getEntry( parent ).getId();
			}
			else {
				parentsByChild.remove( childEntity ); // remove wrong entry
			}
		}

		//not found in case, proceed
		// iterate all the entities currently associated with the persistence context.
		Iterator entities = IdentityMap.entries(entityEntries).iterator();
		while ( entities.hasNext() ) {
			final Map.Entry me = ( Map.Entry ) entities.next();
			final EntityEntry entityEntry = ( EntityEntry ) me.getValue();
			// does this entity entry pertain to the entity persister in which we are interested (owner)?
			if ( persister.isSubclassEntityName( entityEntry.getEntityName() ) ) {
				final Object entityEntryInstance = me.getKey();

				//check if the managed object is the parent
				boolean found = isFoundInParent(
						propertyName,
						childEntity,
						persister,
						collectionPersister,
						entityEntryInstance
				);

				if ( !found && mergeMap != null ) {
					//check if the detached object being merged is the parent
					Object unmergedInstance = mergeMap.get( entityEntryInstance );
					Object unmergedChild = mergeMap.get( childEntity );
					if ( unmergedInstance != null && unmergedChild != null ) {
						found = isFoundInParent(
								propertyName,
								unmergedChild,
								persister,
								collectionPersister,
								unmergedInstance
						);
					}
				}

				if ( found ) {
					return entityEntry.getId();
				}

			}
		}

		// if we get here, it is possible that we have a proxy 'in the way' of the merge map resolution...
		// 		NOTE: decided to put this here rather than in the above loop as I was nervous about the performance
		//		of the loop-in-loop especially considering this is far more likely the 'edge case'
		if ( mergeMap != null ) {
			Iterator mergeMapItr = mergeMap.entrySet().iterator();
			while ( mergeMapItr.hasNext() ) {
				final Map.Entry mergeMapEntry = ( Map.Entry ) mergeMapItr.next();
				if ( mergeMapEntry.getKey() instanceof HibernateProxy ) {
					final HibernateProxy proxy = ( HibernateProxy ) mergeMapEntry.getKey();
					if ( persister.isSubclassEntityName( proxy.getHibernateLazyInitializer().getEntityName() ) ) {
						boolean found = isFoundInParent(
								propertyName,
								childEntity,
								persister,
								collectionPersister,
								mergeMap.get( proxy )
						);
						if ( !found ) {
							found = isFoundInParent(
									propertyName,
									mergeMap.get( childEntity ),
									persister,
									collectionPersister,
									mergeMap.get( proxy )
							);
						}
						if ( found ) {
							return proxy.getHibernateLazyInitializer().getIdentifier();
						}
					}
				}
			}
		}

		return null;
	}

	private boolean isFoundInParent(
			String property,
			Object childEntity,
			EntityPersister persister,
			CollectionPersister collectionPersister,
			Object potentialParent) {
		Object collection = persister.getPropertyValue(
				potentialParent,
				property,
				session.getEntityMode()
		);
		return collection != null
				&& Hibernate.isInitialized( collection )
				&& collectionPersister.getCollectionType().contains( collection, childEntity, session );
	}

	/**
	 * Search the persistence context for an index of the child object,
	 * given a collection role
	 */
	public Object getIndexInOwner(String entity, String property, Object childEntity, Map mergeMap) {

		EntityPersister persister = session.getFactory()
				.getEntityPersister(entity);
		CollectionPersister cp = session.getFactory()
				.getCollectionPersister(entity + '.' + property);
		
	    // try cache lookup first
	    Object parent = parentsByChild.get(childEntity);
		if (parent != null) {
			final EntityEntry entityEntry = (EntityEntry) entityEntries.get(parent);
			//there maybe more than one parent, filter by type
			if ( persister.isSubclassEntityName( entityEntry.getEntityName() ) ) {
				Object index = getIndexInParent(property, childEntity, persister, cp, parent);

				if (index==null && mergeMap!=null) {
					Object unmergedInstance = mergeMap.get(parent);
					Object unmergedChild = mergeMap.get(childEntity);
					if ( unmergedInstance!=null && unmergedChild!=null ) {
						index = getIndexInParent(property, unmergedChild, persister, cp, unmergedInstance);
					}
				}
				if (index!=null) {
					return index;
				}
			}
			else {
				parentsByChild.remove(childEntity); // remove wrong entry
			}
		}

		//Not found in cache, proceed
		Iterator entities = IdentityMap.entries(entityEntries).iterator();
		while ( entities.hasNext() ) {
			Map.Entry me = (Map.Entry) entities.next();
			EntityEntry ee = (EntityEntry) me.getValue();
			if ( persister.isSubclassEntityName( ee.getEntityName() ) ) {
				Object instance = me.getKey();
				
				Object index = getIndexInParent(property, childEntity, persister, cp, instance);
				
				if (index==null && mergeMap!=null) {
					Object unmergedInstance = mergeMap.get(instance);
					Object unmergedChild = mergeMap.get(childEntity);
					if ( unmergedInstance!=null && unmergedChild!=null ) {
						index = getIndexInParent(property, unmergedChild, persister, cp, unmergedInstance);
					}
				}
				
				if (index!=null) return index;
			}
		}
		return null;
	}
	
	private Object getIndexInParent(
			String property, 
			Object childEntity, 
			EntityPersister persister, 
			CollectionPersister collectionPersister,
			Object potentialParent
	){	
		Object collection = persister.getPropertyValue( potentialParent, property, session.getEntityMode() );
		if ( collection!=null && Hibernate.isInitialized(collection) ) {
			return collectionPersister.getCollectionType().indexOf(collection, childEntity);
		}
		else {
			return null;
		}
	}
	
	/**
	 * Record the fact that the association belonging to the keyed
	 * entity is null.
	 */
	public void addNullProperty(EntityKey ownerKey, String propertyName) {
		nullAssociations.add( new AssociationKey(ownerKey, propertyName) );
	}
	
	/**
	 * Is the association property belonging to the keyed entity null?
	 */
	public boolean isPropertyNull(EntityKey ownerKey, String propertyName) {
		return nullAssociations.contains( new AssociationKey(ownerKey, propertyName) );
	}
	
	private void clearNullProperties() {
		nullAssociations.clear();
	}

	public boolean isReadOnly(Object entityOrProxy) {
		if ( entityOrProxy == null ) {
			throw new AssertionFailure( "object must be non-null." );
		}
		boolean isReadOnly;
		if ( entityOrProxy instanceof HibernateProxy ) {
			isReadOnly = ( ( HibernateProxy ) entityOrProxy ).getHibernateLazyInitializer().isReadOnly();
		}
		else {
			EntityEntry ee =  getEntry( entityOrProxy );
			if ( ee == null ) {
				throw new TransientObjectException("Instance was not associated with this persistence context" );
			}
			isReadOnly = ee.isReadOnly();
		}
		return isReadOnly;
	}

	public void setReadOnly(Object object, boolean readOnly) {
		if ( object == null ) {
			throw new AssertionFailure( "object must be non-null." );
		}
		if ( isReadOnly( object ) == readOnly ) {
			return;
		}
		if ( object instanceof HibernateProxy ) {
			HibernateProxy proxy = ( HibernateProxy ) object;
			setProxyReadOnly( proxy, readOnly );
			if ( Hibernate.isInitialized( proxy ) ) {
				setEntityReadOnly(
						proxy.getHibernateLazyInitializer().getImplementation(),
						readOnly
				);
			}
		}
		else {
			setEntityReadOnly( object, readOnly );
			// PersistenceContext.proxyFor( entity ) returns entity if there is no proxy for that entity
			// so need to check the return value to be sure it is really a proxy
			Object maybeProxy = getSession().getPersistenceContext().proxyFor( object );
			if ( maybeProxy instanceof HibernateProxy ) {
				setProxyReadOnly( ( HibernateProxy ) maybeProxy, readOnly );
			}
		}
	}

	private void setProxyReadOnly(HibernateProxy proxy, boolean readOnly) {
		if ( proxy.getHibernateLazyInitializer().getSession() != getSession() ) {
			throw new AssertionFailure(
					"Attempt to set a proxy to read-only that is associated with a different session" );
		}
		proxy.getHibernateLazyInitializer().setReadOnly( readOnly );
	}

	private void setEntityReadOnly(Object entity, boolean readOnly) {
		EntityEntry entry = getEntry(entity);
		if (entry == null) {
			throw new TransientObjectException("Instance was not associated with this persistence context" );
		}
		entry.setReadOnly(readOnly, entity );
		hasNonReadOnlyEntities = hasNonReadOnlyEntities || ! readOnly;
	}

	public void replaceDelayedEntityIdentityInsertKeys(EntityKey oldKey, Serializable generatedId) {
		Object entity = entitiesByKey.remove( oldKey );
		EntityEntry oldEntry = ( EntityEntry ) entityEntries.remove( entity );
		parentsByChild.clear();

		EntityKey newKey = new EntityKey( generatedId, oldEntry.getPersister(), getSession().getEntityMode() );
		addEntity( newKey, entity );
		addEntry(
				entity,
		        oldEntry.getStatus(),
		        oldEntry.getLoadedState(),
		        oldEntry.getRowId(),
		        generatedId,
		        oldEntry.getVersion(),
		        oldEntry.getLockMode(),
		        oldEntry.isExistsInDatabase(),
		        oldEntry.getPersister(),
		        oldEntry.isBeingReplicated(),
		        oldEntry.isLoadedWithLazyPropertiesUnfetched()
		);
	}

	/**
	 * Used by the owning session to explicitly control serialization of the
	 * persistence context.
	 *
	 * @param oos The stream to which the persistence context should get written
	 * @throws IOException serialization errors.
	 */
	public void serialize(ObjectOutputStream oos) throws IOException {
		log.trace( "serializing persistent-context" );

		oos.writeBoolean( defaultReadOnly );
		oos.writeBoolean( hasNonReadOnlyEntities );

		oos.writeInt( entitiesByKey.size() );
		log.trace( "starting serialization of [" + entitiesByKey.size() + "] entitiesByKey entries" );
		Iterator itr = entitiesByKey.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			( ( EntityKey ) entry.getKey() ).serialize( oos );
			oos.writeObject( entry.getValue() );
		}

		oos.writeInt( entitiesByUniqueKey.size() );
		log.trace( "starting serialization of [" + entitiesByUniqueKey.size() + "] entitiesByUniqueKey entries" );
		itr = entitiesByUniqueKey.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			( ( EntityUniqueKey ) entry.getKey() ).serialize( oos );
			oos.writeObject( entry.getValue() );
		}

		oos.writeInt( proxiesByKey.size() );
		log.trace( "starting serialization of [" + proxiesByKey.size() + "] proxiesByKey entries" );
		itr = proxiesByKey.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			( ( EntityKey ) entry.getKey() ).serialize( oos );
			oos.writeObject( entry.getValue() );
		}

		oos.writeInt( entitySnapshotsByKey.size() );
		log.trace( "starting serialization of [" + entitySnapshotsByKey.size() + "] entitySnapshotsByKey entries" );
		itr = entitySnapshotsByKey.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			( ( EntityKey ) entry.getKey() ).serialize( oos );
			oos.writeObject( entry.getValue() );
		}

		oos.writeInt( entityEntries.size() );
		log.trace( "starting serialization of [" + entityEntries.size() + "] entityEntries entries" );
		itr = entityEntries.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			oos.writeObject( entry.getKey() );
			( ( EntityEntry ) entry.getValue() ).serialize( oos );
		}

		oos.writeInt( collectionsByKey.size() );
		log.trace( "starting serialization of [" + collectionsByKey.size() + "] collectionsByKey entries" );
		itr = collectionsByKey.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			( ( CollectionKey ) entry.getKey() ).serialize( oos );
			oos.writeObject( entry.getValue() );
		}

		oos.writeInt( collectionEntries.size() );
		log.trace( "starting serialization of [" + collectionEntries.size() + "] collectionEntries entries" );
		itr = collectionEntries.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			oos.writeObject( entry.getKey() );
			( ( CollectionEntry ) entry.getValue() ).serialize( oos );
		}

		oos.writeInt( arrayHolders.size() );
		log.trace( "starting serialization of [" + arrayHolders.size() + "] arrayHolders entries" );
		itr = arrayHolders.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			oos.writeObject( entry.getKey() );
			oos.writeObject( entry.getValue() );
		}

		oos.writeInt( nullifiableEntityKeys.size() );
		log.trace( "starting serialization of [" + nullifiableEntityKeys.size() + "] nullifiableEntityKeys entries" );
		itr = nullifiableEntityKeys.iterator();
		while ( itr.hasNext() ) {
			EntityKey entry = ( EntityKey ) itr.next();
			entry.serialize( oos );
		}
	}

	public static StatefulPersistenceContext deserialize(
			ObjectInputStream ois,
	        SessionImplementor session) throws IOException, ClassNotFoundException {
		log.trace( "deserializing persistent-context" );
		StatefulPersistenceContext rtn = new StatefulPersistenceContext( session );

		// during deserialization, we need to reconnect all proxies and
		// collections to this session, as well as the EntityEntry and
		// CollectionEntry instances; these associations are transient
		// because serialization is used for different things.

		try {
			rtn.defaultReadOnly = ois.readBoolean();
			// todo : we can actually just determine this from the incoming EntityEntry-s
			rtn.hasNonReadOnlyEntities = ois.readBoolean();

			int count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] entitiesByKey entries" );
			rtn.entitiesByKey = new HashMap( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				rtn.entitiesByKey.put( EntityKey.deserialize( ois, session ), ois.readObject() );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] entitiesByUniqueKey entries" );
			rtn.entitiesByUniqueKey = new HashMap( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				rtn.entitiesByUniqueKey.put( EntityUniqueKey.deserialize( ois, session ), ois.readObject() );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] proxiesByKey entries" );
			rtn.proxiesByKey = new ReferenceMap( ReferenceMap.HARD, ReferenceMap.WEAK, count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count, .75f );
			for ( int i = 0; i < count; i++ ) {
				EntityKey ek = EntityKey.deserialize( ois, session );
				Object proxy = ois.readObject();
				if ( proxy instanceof HibernateProxy ) {
					( ( HibernateProxy ) proxy ).getHibernateLazyInitializer().setSession( session );
					rtn.proxiesByKey.put( ek, proxy );
				}
				else {
					log.trace( "encountered prunded proxy" );
				}
				// otherwise, the proxy was pruned during the serialization process
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] entitySnapshotsByKey entries" );
			rtn.entitySnapshotsByKey = new HashMap( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				rtn.entitySnapshotsByKey.put( EntityKey.deserialize( ois, session ), ois.readObject() );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] entityEntries entries" );
			rtn.entityEntries = IdentityMap.instantiateSequenced( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				Object entity = ois.readObject();
				EntityEntry entry = EntityEntry.deserialize( ois, session );
				rtn.entityEntries.put( entity, entry );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] collectionsByKey entries" );
			rtn.collectionsByKey = new HashMap( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				rtn.collectionsByKey.put( CollectionKey.deserialize( ois, session ), ois.readObject() );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] collectionEntries entries" );
			rtn.collectionEntries = IdentityMap.instantiateSequenced( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				final PersistentCollection pc = ( PersistentCollection ) ois.readObject();
				final CollectionEntry ce = CollectionEntry.deserialize( ois, session );
				pc.setCurrentSession( session );
				rtn.collectionEntries.put( pc, ce );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] arrayHolders entries" );
			rtn.arrayHolders = IdentityMap.instantiate( count < INIT_COLL_SIZE ? INIT_COLL_SIZE : count );
			for ( int i = 0; i < count; i++ ) {
				rtn.arrayHolders.put( ois.readObject(), ois.readObject() );
			}

			count = ois.readInt();
			log.trace( "staring deserialization of [" + count + "] nullifiableEntityKeys entries" );
			rtn.nullifiableEntityKeys = new HashSet();
			for ( int i = 0; i < count; i++ ) {
				rtn.nullifiableEntityKeys.add( EntityKey.deserialize( ois, session ) );
			}

		}
		catch ( HibernateException he ) {
			throw new InvalidObjectException( he.getMessage() );
		}

		return rtn;
	}

	/**
	 * @see org.hibernate.engine.PersistenceContext#addChildParent(java.lang.Object, java.lang.Object)
	 */
	public void addChildParent(Object child, Object parent) {
		parentsByChild.put(child, parent);
	}
	
	/**
	 * @see org.hibernate.engine.PersistenceContext#removeChildParent(java.lang.Object)
	 */
	public void removeChildParent(Object child) {
	   parentsByChild.remove(child);
	}


	private HashMap<String,List<Serializable>> insertedKeysMap;

	/**
	 * {@inheritDoc}
	 */
	public void registerInsertedKey(EntityPersister persister, Serializable id) {
		// we only are about regsitering these if the persister defines caching
		if ( persister.hasCache() ) {
			if ( insertedKeysMap == null ) {
				insertedKeysMap = new HashMap<String, List<Serializable>>();
			}
			final String rootEntityName = persister.getRootEntityName();
			List<Serializable> insertedEntityIds = insertedKeysMap.get( rootEntityName );
			if ( insertedEntityIds == null ) {
				insertedEntityIds = new ArrayList<Serializable>();
				insertedKeysMap.put( rootEntityName, insertedEntityIds );
			}
			insertedEntityIds.add( id );
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean wasInsertedDuringTransaction(EntityPersister persister, Serializable id) {
		// again, we only really care if the entity is cached
		if ( persister.hasCache() ) {
			if ( insertedKeysMap != null ) {
				List<Serializable> insertedEntityIds = insertedKeysMap.get( persister.getRootEntityName() );
				if ( insertedEntityIds != null ) {
					return insertedEntityIds.contains( id );
				}
			}
		}
		return false;
	}

	private void cleanUpInsertedKeysAfterTransaction() {
		if ( insertedKeysMap != null ) {
			insertedKeysMap.clear();
		}
	}

}
