/*******************************************************************************
 * Copyright © 2005 VHA. All rights reserved
 ******************************************************************************/
package gov.va.med.fw.persistent.hibernate;

// Java Classes
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.Validate;
import org.hibernate.CallbackException;
import org.hibernate.type.Type;

import gov.va.med.fw.model.AbstractVersionedEntity;
import gov.va.med.fw.model.UserPrincipalImpl;
import gov.va.med.fw.security.Principal;
import gov.va.med.fw.security.SecurityContextHelper;
import gov.va.med.fw.service.transaction.TransactionTimestampManager;

/**
 * A Hibernate Interceptor to perform general interception during lifecycle
 * joinpoints. Currently implemented to do the following:<br>
 * <ul>
 * <li> Populate the "user" audit fields (eg, createdBy, modifiedBy) of an
 * Application for AbstractVersionedEntity entities.
 * <li> Synchronize all flushes in the same Transaction to use the same
 * Timestamp for the "date" audit fields (eg, createdOn, modifiedOn)
 * </ul>
 * 
 * @author Gregory Bohmer
 */
public class EntityInterceptor extends PersistentInterceptorAdapter 
	implements Serializable {

	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = 8642808474447403684L;

	/**
	 * An instance of CLASS_NAME
	 */
	//private static final String CLASS_NAME = EntityInterceptor.class.getName();
	
	/**
	 * An instance of TRANSACTION_TIMESTAMP
	 */
	//private static final String TRANSACTION_TIMESTAMP = CLASS_NAME + ".ts";

	/**
	 * An instance of TRANSACTION_KEY
	 */
	//private static final String TRANSACTION_KEY = CLASS_NAME + ".tx";

	/**
	 * An instance of TIMESTAMP_NO_TX
	 */
	//private static final String TIMESTAMP_NO_TX = CLASS_NAME + ".noTxTs";

	/**
	 * An instance of PROPERTY_NAMES_CREATED_BY
	 */
	private static final Map PROPERTY_NAMES_CREATED_BY = new HashMap();

	/**
	 * An instance of PROPERTY_NAMES_MODIFIED_BY
	 */
	private static final Map PROPERTY_NAMES_MODIFIED_BY = new HashMap();

	/**
	 * An instance of PROPERTY_NAMES_CREATED_ON
	 */
	private static final Map PROPERTY_NAMES_CREATED_ON = new HashMap();

	/**
	 * An instance of PROPERTY_NAMES_MODIFIED_ON
	 */
	private static final Map PROPERTY_NAMES_MODIFIED_ON = new HashMap();

	// support for the following columns is provided
	static {
		PROPERTY_NAMES_CREATED_BY.put("createdBy", null);
		PROPERTY_NAMES_MODIFIED_BY.put("modifiedBy", null);
		PROPERTY_NAMES_CREATED_ON.put("created", null);
		PROPERTY_NAMES_CREATED_ON.put("createdOn", null);
		PROPERTY_NAMES_MODIFIED_ON.put("modified", null);
		PROPERTY_NAMES_MODIFIED_ON.put("modifiedOn", null);
	}

	/**
	 * An instance of defaultAuditName
	 */
	private String defaultAuditName;
	
	private TransactionTimestampManager timestampManager;


	/**
	 * Default Constructor
	 */
	public EntityInterceptor() {
		super();
	}


	/**
	 * @return Returns the defaultAuditName.
	 */
	public String getDefaultAuditName() {
		return defaultAuditName;
	}

	/**
	 * @param defaultAuditName
	 *           The defaultAuditName to set.
	 */
	public void setDefaultAuditName(String defaultAuditName) {
		this.defaultAuditName = defaultAuditName;
	}

	/**
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	public void afterPropertiesSet() {
		Validate.notNull( timestampManager);
	}


	/**
	 * @see org.hibernate.Interceptor#onFlushDirty(java.lang.Object,
	 *      java.io.Serializable, java.lang.Object[], java.lang.Object[],
	 *      java.lang.String[], org.hibernate.type.Type[])
	 */
	public boolean onFlushDirty(Object entity, Serializable id,
			Object[] currentState, Object[] previousState, String[] propertyNames,
			Type[] types) throws CallbackException {
		boolean wasStateModified = false;

		if (entity instanceof AbstractVersionedEntity) {
			for (int index = 0; index < propertyNames.length; index++) {
				if (PROPERTY_NAMES_MODIFIED_BY.containsKey(propertyNames[index])) {
					currentState[index] = getCurrentPrincipal();
					wasStateModified = true;
				}
				else if (PROPERTY_NAMES_MODIFIED_ON
						.containsKey(propertyNames[index])) {
					currentState[index] = getCurrentTimestamp();
					wasStateModified = true;
				}
			}
		}
		
		return wasStateModified;
	}

	/**
	 * @see org.hibernate.Interceptor#onSave(java.lang.Object,
	 *      java.io.Serializable, java.lang.Object[], java.lang.String[],
	 *      org.hibernate.type.Type[])
	 */
	public boolean onSave(Object entity, Serializable id, Object[] state,
			String[] propertyNames, Type[] types) throws CallbackException {
		boolean wasStateModified = false;

		if (entity instanceof AbstractVersionedEntity) {
			Timestamp ts = getCurrentTimestamp();
			for (int index = 0; index < propertyNames.length; index++) {
				if (PROPERTY_NAMES_CREATED_BY.containsKey(propertyNames[index])
						|| PROPERTY_NAMES_MODIFIED_BY
								.containsKey(propertyNames[index])) {
					state[index] = getCurrentPrincipal();
					wasStateModified = true;
				}
				else if (PROPERTY_NAMES_CREATED_ON
						.containsKey(propertyNames[index])
						|| PROPERTY_NAMES_MODIFIED_ON
								.containsKey(propertyNames[index])) {
					state[index] = ts;
					wasStateModified = true;
				}
			}
		}
		
		return wasStateModified;
	}


	/**
	 * @see org.hibernate.Interceptor#onDelete(java.lang.Object, java.io.Serializable, java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
	 */
	public void onDelete(Object entity, Serializable id, Object[] state,
			String[] propertyNames, Type[] types) throws CallbackException {
	
		if (entity instanceof AbstractVersionedEntity) {
			for (int index = 0; index < propertyNames.length; index++) {
				if (PROPERTY_NAMES_MODIFIED_BY.containsKey(propertyNames[index])) {
					state[index] = getCurrentPrincipal();
				}
				else if (PROPERTY_NAMES_MODIFIED_ON
						.containsKey(propertyNames[index])) {
					state[index] = getCurrentTimestamp();
				}
			}
		}
	
	}

	/**
	 * @see org.hibernate.Interceptor#onPrepareStatement(java.lang.String)
	 */
	public String onPrepareStatement(String arg0) {
		return arg0;
	}


	/** 
	 * Get the loaded Timestamp from Thread-bound storage
	 * @return
	 */
	private Timestamp getCurrentTimestamp() {
		return timestampManager.getTransactionTimestamp();
	}


	/** Get the logged in Principal that is bound to the Thread 
	 * @return
	 */
	private Principal getCurrentPrincipal() {
		try {
			return SecurityContextHelper.getCurrentUser();
		}
		catch (Exception e) {
			// Had trouble getting the principal, attempt to use default
			// Principal.
			if (logger.isErrorEnabled()) {
				logger.error( "Exception while trying to get the principal from the context.", e );
			}
			return new UserPrincipalImpl(getDefaultAuditName());
		}
	}


	/**
	 * @return Returns the timestampManager.
	 */
	public TransactionTimestampManager getTimestampManager() {
		return timestampManager;
	}


	/**
	 * @param timestampManager The timestampManager to set.
	 */
	public void setTimestampManager(TransactionTimestampManager timestampManager) {
		this.timestampManager = timestampManager;
	}
}