//$Id: AbstractSaveEventListener.java,v 1.29 2005/08/25 05:20:16 oneovthafew Exp $
package org.hibernate.event.def;

import java.io.Serializable;
import java.util.Map;

import net.sf.cglib.transform.impl.InterceptFieldEnabled;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.action.EntityIdentityInsertAction;
import org.hibernate.action.EntityInsertAction;
import org.hibernate.classic.Lifecycle;
import org.hibernate.classic.Validatable;
import org.hibernate.engine.Cascade;
import org.hibernate.engine.CascadingAction;
import org.hibernate.engine.EntityEntry;
import org.hibernate.engine.EntityKey;
import org.hibernate.engine.ForeignKeys;
import org.hibernate.engine.Nullability;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.Status;
import org.hibernate.engine.Versioning;
import org.hibernate.event.EventSource;
import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.id.IdentifierGeneratorFactory;
import org.hibernate.intercept.FieldInterceptor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.type.Type;
import org.hibernate.type.TypeFactory;

/**
 * A convenience bas class for listeners responding to save events.
 *
 * @author Steve Ebersole.
 */
public abstract class AbstractSaveEventListener extends AbstractReassociateEventListener {

	protected static final int PERSISTENT = 0;
	protected static final int TRANSIENT = 1;
	protected static final int DETACHED = 2;
	protected static final int DELETED = 3;

	private static final Log log = LogFactory.getLog(AbstractSaveEventListener.class);

	/**
	 * Prepares the save call using the given requested id.
	 * @param entity The entity to be saved.
	 * @param requestedId The id to which to associate the entity.
	 * @param source The session which is the source of this save event.
	 * @return The id used to save the entity.
	 * @throws HibernateException
	 */
	protected Serializable saveWithRequestedId(
			Object entity,
			Serializable requestedId,
			String entityName,
			Object anything,
			EventSource source) 
	throws HibernateException {
		return performSave(
				entity,
				requestedId,
				source.getEntityPersister(entityName, entity),
				false,
				anything,
				source
			);
	}

	/**
	 * Prepares the save call using a newly generated id.
	 * @param entity The entity to be saved
	 * @param source The session which is the source of this save event.
	 * @return The id used to save the entity
	 * @throws HibernateException
	 */
	protected Serializable saveWithGeneratedId(
			Object entity,
			String entityName,
			Object anything,
			EventSource source) 
	throws HibernateException {
		
		EntityPersister persister = source.getEntityPersister(entityName, entity);

		Serializable generatedId = persister.getIdentifierGenerator()
			.generate( source, entity );
		
		if ( generatedId == null ) {
			throw new IdentifierGenerationException( "null id generated for:" + entity.getClass() );
		}
		else if ( generatedId == IdentifierGeneratorFactory.SHORT_CIRCUIT_INDICATOR ) {
			return source.getIdentifier( entity );
		}
		else if ( generatedId == IdentifierGeneratorFactory.POST_INSERT_INDICATOR ) {
			return performSave(entity, null, persister, true, anything, source);
		}
		else {
			
			if ( log.isDebugEnabled() ) {
				log.debug(
						"generated identifier: " + 
						persister.getIdentifierType().toLoggableString( generatedId, source.getFactory() ) + 
						", using strategy: " + 
						persister.getIdentifierGenerator().getClass().getName() //TODO: define toString()s for generators
					);
			}
			
			return performSave(entity, generatedId, persister, false, anything, source);
		}
	}

	/**
	 * Ppepares the save call by checking the session caches for a pre-existing
	 * entity and performing any lifecycle callbacks.
	 * @param entity The entity to be saved.
	 * @param id The id by which to save the entity.
	 * @param persister The entity's persister instance.
	 * @param useIdentityColumn Is an identity column in use?
	 * @param source The session from which the event originated.
	 * @return The id used to save the entity.
	 * @throws HibernateException
	 */
	protected Serializable performSave(
			Object entity,
			Serializable id,
			EntityPersister persister,
			boolean useIdentityColumn,
			Object anything,
			EventSource source) 
	throws HibernateException {

		if ( log.isTraceEnabled() ) {
			log.trace( 
					"saving " + 
					MessageHelper.infoString( persister, id, source.getFactory() ) 
				);
		}
		
		EntityKey key;
		if ( !useIdentityColumn ) {
			key = new EntityKey( id, persister, source.getEntityMode() );
			Object old = source.getPersistenceContext().getEntity(key);
			if (old != null) {
				if ( source.getPersistenceContext().getEntry(old).getStatus() == Status.DELETED ) {
					source.forceFlush( source.getPersistenceContext().getEntry(old) );
				}
				else {
					throw new NonUniqueObjectException( id, persister.getEntityName() );
				}
			}
			persister.setIdentifier(entity, id, source.getEntityMode());
		}
		else {
			key = null;
		}
		
		if ( invokeSaveLifecycle(entity, persister, source) ) {
			return id; //EARLY EXIT
		}

		return performSaveOrReplicate(
				entity, 
				key, 
				persister, 
				useIdentityColumn, 
				anything, 
				source
			);
	}
	
	protected boolean invokeSaveLifecycle(Object entity, EntityPersister persister, EventSource source) {
		// Sub-insertions should occur before containing insertion so
		// Try to do the callback now
		if ( persister.implementsLifecycle( source.getEntityMode() ) ) {
			log.debug( "calling onSave()" );
			if ( ( (Lifecycle) entity ).onSave(source) ) {
				log.debug( "insertion vetoed by onSave()" );
				return true;
			}
		}
		return false;
	}

	protected void validate(Object entity, EntityPersister persister, EventSource source) {
		if ( persister.implementsValidatable( source.getEntityMode() ) ) {
            ( ( Validatable ) entity ).validate();
        }
	}

	/**
	 * Performs all the actual work needed to save an entity (well to get the save moved to
	 * the execution queue).
	 * @param entity The entity to be saved
	 * @param key The id to be used for saving the entity (or null, in the case of identity columns)
	 * @param persister The entity's persister instance.
	 * @param useIdentityColumn Should an identity column be used for id generation?
	 * @param source The session which is the source of the current event.
	 * @return The id used to save the entity.
	 * @throws HibernateException
	 */
	protected Serializable performSaveOrReplicate(
			Object entity,
			EntityKey key,
			EntityPersister persister,
			boolean useIdentityColumn,
			Object anything,
			EventSource source) 
	throws HibernateException {
		
		validate( entity, persister, source );

		Serializable id = key==null ? null : key.getIdentifier();

		if (useIdentityColumn) {
			log.trace("executing insertions");
			source.getActionQueue().executeInserts();
		}

		// Put a placeholder in entries, so we don't recurse back and try to save() the
		// same object again. QUESTION: should this be done before onSave() is called?
		// likewise, should it be done before onUpdate()?
		source.getPersistenceContext().addEntry(
				entity,
				Status.SAVING,
				null,
				null,
				id,
				null,
				LockMode.WRITE,
				useIdentityColumn,
				persister, 
				false, 
				false
			);

		cascadeBeforeSave(source, persister, entity, anything);

		Object[] values = persister.getPropertyValuesToInsert(entity, getMergeMap(anything), source);
		Type[] types = persister.getPropertyTypes();

		boolean substitute = substituteValuesIfNecessary(entity, id, values, persister, source);

		if ( persister.hasCollections() ) {
			substitute = substitute || visitCollectionsBeforeSave(id, values, types, source);
		}

		if (substitute) persister.setPropertyValues( entity, values, source.getEntityMode() );

		TypeFactory.deepCopy( 
				values, 
				types, 
				persister.getPropertyUpdateability(), 
				values, 
				source
			);
		
		new ForeignKeys.Nullifier(entity, false, useIdentityColumn, source)
				.nullifyTransientReferences(values, types);
		new Nullability(source).checkNullability( values, persister, false );
		
		if (useIdentityColumn) {
			EntityIdentityInsertAction insert = new EntityIdentityInsertAction(values, entity, persister, source);
			source.getActionQueue().execute(insert);
			id = insert.getGeneratedId();
			persister.setIdentifier( entity, id, source.getEntityMode() );
			key = new EntityKey( id, persister, source.getEntityMode() );
			source.getPersistenceContext().checkUniqueness(key, entity);
			//source.getBatcher().executeBatch(); //found another way to ensure that all batched joined inserts have been executed
		}

		Object version = Versioning.getVersion(values, persister);
		source.getPersistenceContext().addEntity(
				entity,
				Status.MANAGED,
				values,
				key,
				version,
				LockMode.WRITE,
				useIdentityColumn,
				persister,
				isVersionIncrementDisabled(), 
				false
			);
		//source.getPersistenceContext().removeNonExist( new EntityKey( id, persister, source.getEntityMode() ) );

		if ( !useIdentityColumn ) {
			source.getActionQueue().addAction( 
					new EntityInsertAction(id, values, entity, version, persister, source) 
				);
		}

		cascadeAfterSave(source, persister, entity, anything);

		markInterceptorDirty( entity, persister, source );

		return id;
	}

	private void markInterceptorDirty(Object entity, EntityPersister persister, EventSource source) {
		if ( entity instanceof InterceptFieldEnabled ) {
			FieldInterceptor fieldInterceptor = FieldInterceptor.initFieldInterceptor( 
					entity, 
					persister.getEntityName(), 
					source, 
					null 
				);
			fieldInterceptor.dirty();
		}
	}
	
	protected Map getMergeMap(Object anything) {
		return null;
	}
	
	/**
	 * After the save, will te version number be incremented
	 * if the instance is modified?
	 */
	protected boolean isVersionIncrementDisabled() {
		return false;
	}
	
	protected boolean visitCollectionsBeforeSave(Serializable id, Object[] values, Type[] types, EventSource source) {
		WrapVisitor visitor = new WrapVisitor(source);
		// substitutes into values by side-effect
		visitor.processEntityPropertyValues(values, types);
		return visitor.isSubstitutionRequired();
	}
	
	/**
	 * Perform any property value substitution that is necessary
	 * (interceptor callback, version initialization...)
	 */
	protected boolean substituteValuesIfNecessary(
			Object entity, 
			Serializable id, 
			Object[] values, 
			EntityPersister persister,
			SessionImplementor source
	) {
		boolean substitute = source.getInterceptor().onSave(
				entity,
				id,
				values,
				persister.getPropertyNames(),
				persister.getPropertyTypes()
			);

		//keep the existing version number in the case of replicate!
		if ( persister.isVersioned() ) {
			substitute = Versioning.seedVersion(
					values,
					persister.getVersionProperty(),
					persister.getVersionType(),
			        source
			) || substitute;
		}
		return substitute;
	}

	/**
	 * Handles the calls needed to perform pre-save cascades for the given entity.
	 * @param source The session from whcih the save event originated.
	 * @param persister The entity's persister instance.
	 * @param entity The entity to be saved.
	 * @throws HibernateException
	 */
	protected void cascadeBeforeSave(
			EventSource source,
			EntityPersister persister,
			Object entity,
			Object anything)
	throws HibernateException {

		// cascade-save to many-to-one BEFORE the parent is saved
		source.getPersistenceContext().incrementCascadeLevel();
		try {
			new Cascade( getCascadeAction(), Cascade.BEFORE_INSERT_AFTER_DELETE, source )
					.cascade(persister, entity,	anything);
		}
		finally {
			source.getPersistenceContext().decrementCascadeLevel();
		}
	}

	/**
	 * Handles to calls needed to perform post-save cascades.
	 * @param source The session from which the event originated.
	 * @param persister The entity's persister instance.
	 * @param entity The entity beng saved.
	 * @throws HibernateException
	 */
	protected void cascadeAfterSave(
			EventSource source,
	        EntityPersister persister,
	        Object entity,
			Object anything)
	throws HibernateException {

		// cascade-save to collections AFTER the collection owner was saved
		source.getPersistenceContext().incrementCascadeLevel();
		try {
			new Cascade( getCascadeAction(), Cascade.AFTER_INSERT_BEFORE_DELETE, source )
					.cascade(persister, entity,	anything);
		}
		finally {
			source.getPersistenceContext().decrementCascadeLevel();
		}
	}
	
	protected abstract CascadingAction getCascadeAction();
	
	/**
	 * Determine whether the entity is persistent, detached, or transient
	 */
	protected int getEntityState(
			Object entity, 
			String entityName, 
			EntityEntry entry, //pass this as an argument only to avoid double looking
			SessionImplementor source
	) {
				
		if ( entry!=null ) { // the object is persistent
			
			//the entity is associated with the session, so check its status
			if ( entry.getStatus() != Status.DELETED ) {
				// do nothing for persistent instances
				if ( log.isTraceEnabled() ) {
					log.trace( 
							"persistent instance of: " + 
							getLoggableName(entityName, entity) 
						);
				}
				return PERSISTENT;
			}
			else { 
				//ie. e.status==DELETED
				if ( log.isTraceEnabled() ) {
					log.trace( 
							"deleted instance of: " + 
							getLoggableName(entityName, entity) 
						);
				}
				return DELETED;
			}
			
		}
		else { // the object is transient or detached
			
			//the entity is not associated with the session, so
			//try interceptor and unsaved-value
			
			if ( ForeignKeys.isTransient( entityName, entity, getAssumedUnsaved(), source ) ) {
				if ( log.isTraceEnabled() ) {
					log.trace( 
							"transient instance of: " + 
							getLoggableName(entityName, entity) 
						);
				}
				return TRANSIENT;
			}
			else {
				if ( log.isTraceEnabled() ) {
					log.trace( 
							"detached instance of: " + 
							getLoggableName(entityName, entity) 
						);
				}
				return DETACHED;
			}

		}
	}
	
	protected String getLoggableName(String entityName, Object entity) {
		return entityName==null ? entity.getClass().getName() : entityName;
	}
	
	protected Boolean getAssumedUnsaved() {
		return null;
	}

}
