/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.orm.jpa;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceException;
import javax.persistence.Query;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
 * Helper class that allows for writing JPA data access code in the same style
 * as with Spring's well-known JdoTemplate and HibernateTemplate classes.
 * Automatically converts PersistenceExceptions into Spring DataAccessExceptions,
 * following the {@code org.springframework.dao} exception hierarchy.
 *
 * <p>The central method is of this template is "execute", supporting JPA access code
 * implementing the {@link JpaCallback} interface. It provides JPA EntityManager
 * handling such that neither the JpaCallback implementation nor the calling code
 * needs to explicitly care about retrieving/closing EntityManagers, or handling
 * JPA lifecycle exceptions.
 *
 * <p>Can be used within a service implementation via direct instantiation with
 * a EntityManagerFactory reference, or get prepared in an application context
 * and given to services as bean reference. Note: The EntityManagerFactory should
 * always be configured as bean in the application context, in the first case
 * given to the service directly, in the second case to the prepared template.
 *
 * <p><b>NOTE: JpaTemplate mainly exists as a sibling of JdoTemplate and
 * HibernateTemplate, offering the same style for people used to it. For newly
 * started projects, consider adopting the standard JPA style of coding data
 * access objects instead, based on a "shared EntityManager" reference injected
 * via a Spring bean definition or the JPA PersistenceContext annotation.</b>
 * (Using Spring's SharedEntityManagerBean / PersistenceAnnotationBeanPostProcessor,
 * or using a direct JNDI lookup for an EntityManager on a Java EE 5 server.)
 *
 * <p>JpaTemplate can be considered as direct alternative to working with the
 * native JPA EntityManager API (through a shared EntityManager reference,
 * as outlined above). The major advantage is its automatic conversion to
 * DataAccessExceptions; the major disadvantage is that it introduces
 * another thin layer on top of the native JPA API. Note that exception
 * translation can also be achieved through AOP advice; check out
 * {@link org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor}.
 *
 * <p>{@link LocalContainerEntityManagerFactoryBean} is the preferred way of
 * obtaining a reference to an EntityManagerFactory, at least outside of a full
 * Java EE 5 environment. The Spring application context will manage its lifecycle,
 * initializing and shutting down the factory as part of the application.
 * Within a Java EE 5 environment, you will typically work with a server-managed
 * EntityManagerFactory that is exposed via JNDI, obtained through Spring's
 * {@link org.springframework.jndi.JndiObjectFactoryBean}.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see #setEntityManagerFactory
 * @see JpaCallback
 * @see javax.persistence.EntityManager
 * @see LocalEntityManagerFactoryBean
 * @see LocalContainerEntityManagerFactoryBean
 * @see JpaTransactionManager
 * @see org.springframework.transaction.jta.JtaTransactionManager
 * @see org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
 * @see org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor
 * @deprecated as of Spring 3.1, in favor of native EntityManager usage
 * (typically obtained through {@code @PersistenceContext})
 * Note that this class did not get upgraded to JPA 2.0 and never will.
 */
@Deprecated
public class JpaTemplate extends JpaAccessor implements JpaOperations {

	private boolean exposeNativeEntityManager = false;


	/**
	 * Create a new JpaTemplate instance.
	 */
	public JpaTemplate() {
	}

	/**
	 * Create a new JpaTemplate instance.
	 * @param emf EntityManagerFactory to create EntityManagers
	 */
	public JpaTemplate(EntityManagerFactory emf) {
		setEntityManagerFactory(emf);
		afterPropertiesSet();
	}

	/**
	 * Create a new JpaTemplate instance.
	 * @param em EntityManager to use
	 */
	public JpaTemplate(EntityManager em) {
		setEntityManager(em);
		afterPropertiesSet();
	}


	/**
	 * Set whether to expose the native JPA EntityManager to JpaCallback
	 * code. Default is "false": a EntityManager proxy will be returned,
	 * suppressing {@code close} calls and automatically applying transaction
	 * timeouts (if any).
	 * <p>As there is often a need to cast to a provider-specific EntityManager
	 * class in DAOs that use the JPA 1.0 API, for JPA 2.0 previews and other
	 * provider-specific functionality, the exposed proxy implements all interfaces
	 * implemented by the original EntityManager. If this is not sufficient,
	 * turn this flag to "true".
	 * @see JpaCallback
	 * @see javax.persistence.EntityManager
	 */
	public void setExposeNativeEntityManager(boolean exposeNativeEntityManager) {
		this.exposeNativeEntityManager = exposeNativeEntityManager;
	}

	/**
	 * Return whether to expose the native JPA EntityManager to JpaCallback
	 * code, or rather an EntityManager proxy.
	 */
	public boolean isExposeNativeEntityManager() {
		return this.exposeNativeEntityManager;
	}


	public <T> T execute(JpaCallback<T> action) throws DataAccessException {
		return execute(action, isExposeNativeEntityManager());
	}

	public List executeFind(JpaCallback<?> action) throws DataAccessException {
		Object result = execute(action, isExposeNativeEntityManager());
		if (!(result instanceof List)) {
			throw new InvalidDataAccessApiUsageException(
					"Result object returned from JpaCallback isn't a List: [" + result + "]");
		}
		return (List) result;
	}

	/**
	 * Execute the action specified by the given action object within a
	 * EntityManager.
	 * @param action callback object that specifies the JPA action
	 * @param exposeNativeEntityManager whether to expose the native
	 * JPA entity manager to callback code
	 * @return a result object returned by the action, or {@code null}
	 * @throws org.springframework.dao.DataAccessException in case of JPA errors
	 */
	public <T> T execute(JpaCallback<T> action, boolean exposeNativeEntityManager) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		EntityManager em = getEntityManager();
		boolean isNewEm = false;
		if (em == null) {
			em = getTransactionalEntityManager();
			if (em == null) {
				logger.debug("Creating new EntityManager for JpaTemplate execution");
				em = createEntityManager();
				isNewEm = true;
			}
		}

		try {
			EntityManager emToExpose = (exposeNativeEntityManager ? em : createEntityManagerProxy(em));
			T result = action.doInJpa(emToExpose);
			flushIfNecessary(em, !isNewEm);
			return result;
		}
		catch (RuntimeException ex) {
			throw translateIfNecessary(ex);
		}
		finally {
			if (isNewEm) {
				logger.debug("Closing new EntityManager after JPA template execution");
				EntityManagerFactoryUtils.closeEntityManager(em);
			}
		}
	}

	/**
	 * Create a close-suppressing proxy for the given JPA EntityManager.
	 * The proxy also prepares returned JPA Query objects.
	 * @param em the JPA EntityManager to create a proxy for
	 * @return the EntityManager proxy, implementing all interfaces
	 * implemented by the passed-in EntityManager object (that is,
	 * also implementing all provider-specific extension interfaces)
	 * @see javax.persistence.EntityManager#close
	 */
	protected EntityManager createEntityManagerProxy(EntityManager em) {
		Class[] ifcs = null;
		EntityManagerFactory emf = getEntityManagerFactory();
		if (emf instanceof EntityManagerFactoryInfo) {
			Class entityManagerInterface = ((EntityManagerFactoryInfo) emf).getEntityManagerInterface();
			if (entityManagerInterface != null) {
				ifcs = new Class[] {entityManagerInterface};
			}
		}
		if (ifcs == null) {
			ifcs = ClassUtils.getAllInterfacesForClass(em.getClass());
		}
		return (EntityManager) Proxy.newProxyInstance(
				em.getClass().getClassLoader(), ifcs, new CloseSuppressingInvocationHandler(em));
	}


	//-------------------------------------------------------------------------
	// Convenience methods for load, save, delete
	//-------------------------------------------------------------------------

	public <T> T find(final Class<T> entityClass, final Object id) throws DataAccessException {
		return execute(new JpaCallback<T>() {
			public T doInJpa(EntityManager em) throws PersistenceException {
				return em.find(entityClass, id);
			}
		}, true);
	}

	public <T> T getReference(final Class<T> entityClass, final Object id) throws DataAccessException {
		return execute(new JpaCallback<T>() {
			public T doInJpa(EntityManager em) throws PersistenceException {
				return em.getReference(entityClass, id);
			}
		}, true);
	}

	public boolean contains(final Object entity) throws DataAccessException {
		return execute(new JpaCallback<Boolean>() {
			public Boolean doInJpa(EntityManager em) throws PersistenceException {
				return em.contains(entity);
			}
		}, true);
	}

	public void refresh(final Object entity) throws DataAccessException {
		execute(new JpaCallback<Object>() {
			public Object doInJpa(EntityManager em) throws PersistenceException {
				em.refresh(entity);
				return null;
			}
		}, true);
	}

	public void persist(final Object entity) throws DataAccessException {
		execute(new JpaCallback<Object>() {
			public Object doInJpa(EntityManager em) throws PersistenceException {
				em.persist(entity);
				return null;
			}
		}, true);
	}

	public <T> T merge(final T entity) throws DataAccessException {
		return execute(new JpaCallback<T>() {
			public T doInJpa(EntityManager em) throws PersistenceException {
				return em.merge(entity);
			}
		}, true);
	}

	public void remove(final Object entity) throws DataAccessException {
		execute(new JpaCallback<Object>() {
			public Object doInJpa(EntityManager em) throws PersistenceException {
				em.remove(entity);
				return null;
			}
		}, true);
	}

	public void flush() throws DataAccessException {
		execute(new JpaCallback<Object>() {
			public Object doInJpa(EntityManager em) throws PersistenceException {
				em.flush();
				return null;
			}
		}, true);
	}


	//-------------------------------------------------------------------------
	// Convenience finder methods
	//-------------------------------------------------------------------------

	public List find(String queryString) throws DataAccessException {
		return find(queryString, (Object[]) null);
	}

	public List find(final String queryString, final Object... values) throws DataAccessException {
		return execute(new JpaCallback<List>() {
			public List doInJpa(EntityManager em) throws PersistenceException {
				Query queryObject = em.createQuery(queryString);
				prepareQuery(queryObject);
				if (values != null) {
					for (int i = 0; i < values.length; i++) {
						queryObject.setParameter(i + 1, values[i]);
					}
				}
				return queryObject.getResultList();
			}
		});
	}

	public List findByNamedParams(final String queryString, final Map<String, ?> params) throws DataAccessException {
		return execute(new JpaCallback<List>() {
			public List doInJpa(EntityManager em) throws PersistenceException {
				Query queryObject = em.createQuery(queryString);
				prepareQuery(queryObject);
				if (params != null) {
					for (Map.Entry<String, ?> entry : params.entrySet()) {
						queryObject.setParameter(entry.getKey(), entry.getValue());
					}
				}
				return queryObject.getResultList();
			}
		});
	}

	public List findByNamedQuery(String queryName) throws DataAccessException {
		return findByNamedQuery(queryName, (Object[]) null);
	}

	public List findByNamedQuery(final String queryName, final Object... values) throws DataAccessException {
		return execute(new JpaCallback<List>() {
			public List doInJpa(EntityManager em) throws PersistenceException {
				Query queryObject = em.createNamedQuery(queryName);
				prepareQuery(queryObject);
				if (values != null) {
					for (int i = 0; i < values.length; i++) {
						queryObject.setParameter(i + 1, values[i]);
					}
				}
				return queryObject.getResultList();
			}
		});
	}

	public List findByNamedQueryAndNamedParams(final String queryName, final Map<String, ?> params)
			throws DataAccessException {

		return execute(new JpaCallback<List>() {
			public List doInJpa(EntityManager em) throws PersistenceException {
				Query queryObject = em.createNamedQuery(queryName);
				prepareQuery(queryObject);
				if (params != null) {
					for (Map.Entry<String, ?> entry : params.entrySet()) {
						queryObject.setParameter(entry.getKey(), entry.getValue());
					}
				}
				return queryObject.getResultList();
			}
		});
	}


	/**
	 * Prepare the given JPA query object. To be used within a JpaCallback.
	 * <p>Applies a transaction timeout, if any. If you don't use such timeouts,
	 * the call is a no-op.
	 * <p>In general, prefer a proxied EntityManager instead, which will
	 * automatically apply the transaction timeout (through the use of a special
	 * EntityManager proxy). You need to set the "exposeNativeEntityManager"
	 * property to "false" to activate this. Note that you won't be able to cast
	 * to a provider-specific JPA EntityManager class anymore then.
	 * @param query the JPA query object
	 * @see JpaCallback#doInJpa
	 * @see EntityManagerFactoryUtils#applyTransactionTimeout
	 * @see #setExposeNativeEntityManager
	 */
	public void prepareQuery(Query query) {
		EntityManagerFactory emf = getEntityManagerFactory();
		if (emf != null) {
			EntityManagerFactoryUtils.applyTransactionTimeout(query, getEntityManagerFactory());
		}
	}


	/**
	 * Invocation handler that suppresses close calls on JPA EntityManagers.
	 * Also prepares returned Query objects.
	 * @see javax.persistence.EntityManager#close()
	 */
	private class CloseSuppressingInvocationHandler implements InvocationHandler {

		private final EntityManager target;

		public CloseSuppressingInvocationHandler(EntityManager target) {
			this.target = target;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			// Invocation on EntityManager interface (or provider-specific extension) coming in...

			if (method.getName().equals("equals")) {
				// Only consider equal when proxies are identical.
				return (proxy == args[0]);
			}
			else if (method.getName().equals("hashCode")) {
				// Use hashCode of EntityManager proxy.
				return System.identityHashCode(proxy);
			}
			else if (method.getName().equals("close")) {
				// Handle close method: suppress, not valid.
				return null;
			}

			// Invoke method on target EntityManager.
			try {
				Object retVal = method.invoke(this.target, args);
				// If return value is a JPA Query object, apply transaction timeout.
				if (retVal instanceof Query) {
					prepareQuery(((Query) retVal));
				}
				return retVal;
			}
			catch (InvocationTargetException ex) {
				throw ex.getTargetException();
			}
		}
	}

}
