/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/

// Package
package gov.va.med.fw.persistent.hibernate;

//Java Classes
import gov.va.med.fw.model.AbstractEntity;
import gov.va.med.fw.model.AbstractKeyedEntity;
import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.persistent.DAO;
import gov.va.med.fw.persistent.DAOException;
import gov.va.med.fw.service.ServiceException;

import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.Query;

import org.apache.commons.lang.Validate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.jpa.EntityManagerFactoryUtils;
import org.springframework.orm.jpa.support.JpaDaoSupport;

/**
 * An abstract base class for all DAO classes that use hibernate framework to
 * persist data
 * 
 * @author Vu Le
 * @version 1.0
 */
public class AbstractDAOImpl extends JpaDaoSupport // HibernateDaoSupport
		implements DAO, BeanNameAware, ApplicationContextAware {

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

	/**
	 * An instance of name
	 */
	private String name = null;

	/**
	 * An instance of context
	 */
	private ApplicationContext context = null;

	/**
	 * A default constructor
	 */
	public AbstractDAOImpl() {
		super();
	}

	/**
	 * Provides concrete implementation using Hibernate to return a object by
	 * its id from a database
	 */
	@SuppressWarnings("unchecked")
	public <T> EntityKey<T> insertObject(T obj) throws DAOException {
		EntityKey<T> key = null;
		try {
			getJpaTemplate().persist(obj);
			if (obj instanceof AbstractKeyedEntity)
				key = ((AbstractKeyedEntity) obj).getEntityKey();
			else {
				// TODO key = new BaseEntityKey(id, obj.getClass());
			}

		} catch (DataAccessException e) {
			throw new DAOException("exception loading '" + obj.getClass().getName(), e);
		}
		return key;
	}

	/**
	 * Provides concrete implementation using Hibernate to return a object by
	 * its id from a database
	 */
	/*
	 * public Object getObject(Object obj, String id) throws DAOException {
	 * 
	 * Object value = null; try { value =
	 * getHibernateTemplate().get(obj.getClass(), id); } catch
	 * (DataAccessException e) { throw new DAOException("exception loading '" +
	 * id.getClass().getName() + "' with id '" + id + "'", e); } return value; }
	 */

	/**
	 * Generic method to return a persistent instance of a given entity class
	 * with the given identifier, or null if not found
	 * 
	 * @param obj
	 *            An entity to query
	 * @param id
	 *            An entity's id
	 * @return a populated object (or null if id doesn't exist)
	 * @throws DAOException
	 *             Thrown if failed to query an object
	 */
	public <T> T getByKey(EntityKey<T> key) throws DAOException {

		if (key.getEntityClass() == null)
			throw new IllegalArgumentException(
					"Must specify a non-null entity class in the EntityKey");

		T value = null;
		try {
			value = getJpaTemplate().find(key.getEntityClass(), key.getKeyValue());
		} catch (DataAccessException e) {
			throw new DAOException("exception loading '" + key.getEntityClass().getName()
					+ "' with key '" + key + "'", e);
		}
		return value;
	}

	/**
	 * Saves an object to a database using Hibernate technology. It locks the
	 * lock object to increment the version number.
	 * 
	 * @see gov.va.med.fw.persistent.DAO#saveObject(java.lang.Object,
	 *      java.lang.Object)
	 */
	public Object saveObject(AbstractEntity obj, AbstractKeyedEntity lock) throws DAOException {
		if (lock != null && lock.getId() != null) {
			EntityManager em = EntityManagerFactoryUtils
					.getTransactionalEntityManager(getJpaTemplate().getEntityManagerFactory());
			if (em == null) {
				logger.warn("EntityManage is null, creating new one.");
				em = getJpaTemplate().getEntityManagerFactory().createEntityManager();
			}
			if (em != null) {
				em.lock(lock, LockModeType.WRITE);
			}
		}
		return saveObject(obj);
	}

	/**
	 * Saves an object to a database using Hibernate technology
	 * 
	 * @see gov.va.med.fw.persistent.DAO#saveObject(java.lang.Object)
	 */
	public Object saveObject(Object obj) throws DAOException {
		try {
			getJpaTemplate().merge(obj);
			getJpaTemplate().flush();
			return obj;
		} catch (DataAccessException e) {
			if (logger.isErrorEnabled()) {
				logger.error("Error during save: " + e.getMessage());
			}

			throw new DAOException("Failed to persist an entity " + obj, e);
		}
	}

	/**
	 * Remove an object from a database using Hibernate technology
	 * 
	 * @see gov.va.med.fw.persistent.DAO#removeObject(java.lang.Object,
	 *      java.lang.String)
	 */
	public void removeObject(EntityKey<?> id) throws DAOException {
		Object value = getByKey(id);
		if (value != null) {
			try {
				getJpaTemplate().remove(value);
			} catch (DataAccessException e) {
				throw new DAOException("Failed to remove an entity with id: " + id, e);
			}
		}
	}

	/**
	 * Generic method to execute a simple query
	 * 
	 * @param query
	 *            A named query to execute. Implemented as the key for a named
	 *            query.
	 * @throws DAOException
	 *             Thrown if failed to remove an object
	 */
	public List<?> find(String query) {
		List<?> list = null;
		if (query != null)
			try {
				list = getJpaTemplate().findByNamedQuery(query);
			} catch (DataAccessException e) {
				// TODO
			}
		return list;
	}

	/**
	 * Sets a component name to identify this component in a container
	 * 
	 * @param name
	 *            A component name
	 */
	public void setBeanName(String name) {
		this.name = name;
	}

	/**
	 * Returns a name identifying this component in a container
	 * 
	 * @return A bean's name.
	 */
	public String getBeanName() {
		return name;
	}

	/**
	 * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
	 */
	public void setApplicationContext(ApplicationContext applicationContext) {
		this.context = applicationContext;
	}

	/**
	 * Provides a convenient method to obtain a bean from a spring context.
	 * 
	 * @param name
	 *            A bean name to look up in a context
	 * @return A bean defined in the context
	 * @throws BeansException
	 *             If failed to retrieve a bean
	 */
	protected Object getComponent(String name) throws ServiceException {
		try {
			return this.context != null ? this.context.getBean(name) : null;
		} catch (BeansException e) {
			throw new ServiceException("Failed to obtain a service " + name, e);
		}
	}

	/**
	 * Provides a convenient method to obtain a bean from a spring context for
	 * the specific type.
	 * 
	 * @param name
	 *            A bean's name defined in spring context
	 * @param type
	 *            A bean's class type
	 * @return A bean defined in the context
	 * @throws BeansException
	 *             If failed to retrieve a bean
	 */
	protected Object getComponent(String name, Class<?> type) throws ServiceException {
		try {
			return this.context != null ? this.context.getBean(name, type) : null;
		} catch (BeansException e) {
			throw new ServiceException("Failed to obtain a service " + name + " type " + type, e);
		}
	}

	/**
	 * @see org.springframework.dao.support.DaoSupport#initDao()
	 */
	protected void initDao() throws Exception {
		super.initDao();
		Validate.notNull(this.context, "An application context must not be null");
	}
	
	public static void setQueryParameters(Query query, Map<String, Object> parameters) {
		if (query == null || parameters == null)
			return;

		org.hibernate.ejb.QueryImpl ejbQuery = (org.hibernate.ejb.QueryImpl) query;
		org.hibernate.Query hibernateQuery = ejbQuery.getHibernateQuery();
		String[] namedParameters = hibernateQuery.getNamedParameters();

		/*
		 * set only the parameters that are required for the query, even if we
		 * pass in more
		 */
		if (namedParameters != null) {
			for (String parName : namedParameters) {
				/*
				 * ...and only set the params that we pass in, don't call
				 * setParam on any param we didn't pass
				 */
				if (parameters.containsKey(parName))
					query.setParameter(parName, parameters.get(parName));
			}
		}
	}
}