Java tutorial
/* * Copyright 2009 Grepo Committers. * * 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.codehaus.grepo.query.jpa.repository; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Map; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.FlushModeType; import org.codehaus.grepo.core.validator.GenericValidationUtils; import org.codehaus.grepo.query.commons.annotation.GenericQuery; import org.codehaus.grepo.query.commons.aop.QueryMethodParameterInfo; import org.codehaus.grepo.query.commons.executor.QueryExecutor; import org.codehaus.grepo.query.commons.generator.GeneratorUtils; import org.codehaus.grepo.query.commons.repository.GenericQueryRepositorySupport; import org.codehaus.grepo.query.jpa.annotation.JpaFlushMode; import org.codehaus.grepo.query.jpa.annotation.JpaQueryOptions; import org.codehaus.grepo.query.jpa.context.JpaQueryExecutionContext; import org.codehaus.grepo.query.jpa.context.JpaQueryExecutionContextImpl; import org.codehaus.grepo.query.jpa.executor.JpaQueryExecutor; import org.codehaus.grepo.query.jpa.generator.JpaQueryGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.support.DataAccessUtils; import org.springframework.orm.jpa.DefaultJpaDialect; import org.springframework.orm.jpa.EntityManagerFactoryInfo; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.JpaDialect; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** * @author dguggi * @param <T> The main entity type. */ public class DefaultJpaRepository<T> extends GenericQueryRepositorySupport<T> implements JpaRepository<T>, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(DefaultJpaRepository.class); /** Flag to indicate whether or not the native entity manager should be exposed. */ private boolean exposeNativeEntityManager = true; /** The entity manager factory. */ private EntityManagerFactory entityManagerFactory; /** A map of jpa properties. */ private Map<String, Object> jpaPropertyMap; /** The jpa dialect. */ private JpaDialect jpaDialect = new DefaultJpaDialect(); /** Flag to indicate whether or not exceptions should be translated. */ private boolean translateExceptions = false; /** The jpa flush mode to set. */ private JpaFlushMode flushMode = JpaFlushMode.UNDEFINED; /** A map of default query hints applied to jpa queries. */ private Map<String, Object> defaultQueryHints; public DefaultJpaRepository() { super(); } public DefaultJpaRepository(Class<T> entityType) { super(entityType); } /** * {@inheritDoc} */ public Object executeGenericQuery(QueryMethodParameterInfo qmpi, GenericQuery genericQuery) throws Exception { createStatisticsEntry(qmpi); try { Object result = executeQuery(qmpi, genericQuery); result = convertResult(result, qmpi, genericQuery); validateResult(result, qmpi, genericQuery); return result; } finally { completeStatisticsEntry(qmpi.getStatisticsEntry()); } } /** * {@inheritDoc} */ public void afterPropertiesSet() throws Exception { EntityManagerFactory emf = getEntityManagerFactory(); if (emf instanceof EntityManagerFactoryInfo) { JpaDialect dialect = ((EntityManagerFactoryInfo) emf).getJpaDialect(); if (dialect != null) { setJpaDialect(dialect); } } } /** * @param qmpi The query method parameter info. * @param genericQuery The annotation. * @return Returns the result of query execution. */ @SuppressWarnings("unchecked") protected Object executeQuery(final QueryMethodParameterInfo qmpi, final GenericQuery genericQuery) { Class<? extends QueryExecutor<?>> clazz = (Class<? extends QueryExecutor<?>>) getQueryExecutorFindingStrategy() .findExecutor(genericQuery.queryExecutor(), qmpi); final JpaQueryExecutor executor = (JpaQueryExecutor) getQueryExecutorFactory().createExecutor(clazz); GeneratorUtils.validateQueryGenerator(genericQuery.queryGenerator(), JpaQueryGenerator.class); JpaCallbackCreator callback = new JpaCallbackCreator() { @Override protected Object doExecute(JpaQueryExecutionContext context) { Object result = executor.execute(qmpi, context); logger.debug("Query result is '{}'", result); return result; } }; return executeCallback(callback.create(qmpi, isExposeNativeEntityManager()), executor.isReadOnlyOperation()); } /** * @return Returns {@code true} if the entity manager was newly created. */ protected CurrentEntityManagerHolder getCurrentEntityManager() { boolean isNewEm = false; EntityManager em = getTransactionalEntityManager(); if (em == null) { logger.debug("Creating new EntityManager for generic repository execution"); em = createEntityManager(); isNewEm = true; } return new CurrentEntityManagerHolder(isNewEm, em); } /** * @param emHolder The current entity manager info. */ protected void closeNewEntityManager(CurrentEntityManagerHolder emHolder) { if (emHolder.isNewEm()) { logger.debug("Closing new EntityManager after generic repository execution"); EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); } else { // we have an existing entity manager if (emHolder.getPreviousFlushMode() != null) { logger.debug("Setting flushMode back to previous value '{}' after generic repository execution", emHolder.getPreviousFlushMode()); emHolder.getEntityManager().setFlushMode(emHolder.getPreviousFlushMode()); } } } /** * Creates a hibernate query execution context. * * @param emHolder The mandatory entityManagerHolder. * @param doExposeNativeEntityManager Controls whether to expose the native JPA entity manager to callback code. * @return Returns the newly created {@link HibernateQueryExecutionContext}. */ protected JpaQueryExecutionContext createQueryExecutionContext(CurrentEntityManagerHolder emHolder, boolean doExposeNativeEntityManager) { JpaQueryExecutionContextImpl context = new JpaQueryExecutionContextImpl(); context.setApplicationContext(getApplicationContext()); context.setMaxResults(getMaxResults()); context.setQueryNamingStrategy(getQueryNamingStrategy()); if (doExposeNativeEntityManager) { context.setEntityManager(emHolder.getEntityManager()); } else { context.setEntityManager(createEntityManagerProxy(emHolder.getEntityManager())); } return context; } /** * Create a close-suppressing proxy for the given JPA EntityManager. * * @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). */ 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)); } /** * @param result The result to convert. * @param qmpi The query method parameter info. * @param genericQuery The annotation. * @return Returns the possibly converted result. */ protected Object convertResult(Object result, QueryMethodParameterInfo qmpi, GenericQuery genericQuery) { if (getResultConversionService() == null) { logger.debug("No result conversion is performed, because no resultConversionService is configured"); return result; } else { return getResultConversionService().convert(qmpi, genericQuery.resultConverter(), result); } } /** * @param result The result. * @param qmpi The query method parameter info. * @param genericQuery The annotation. * @throws Exception in case of errors. */ protected void validateResult(Object result, QueryMethodParameterInfo qmpi, GenericQuery genericQuery) throws Exception { GenericValidationUtils.validateResult(qmpi, genericQuery.resultValidator(), result); } /** * @return Returns the transaction manager. * @throws IllegalStateException in case factory is null */ protected EntityManager getTransactionalEntityManager() throws IllegalStateException { Assert.state(getEntityManagerFactory() != null, "No EntityManagerFactory specified"); return EntityManagerFactoryUtils.getTransactionalEntityManager(getEntityManagerFactory(), getJpaPropertyMap()); } /** * @return Returns the entity manager. * @throws IllegalStateException in case factory is null */ protected EntityManager createEntityManager() throws IllegalStateException { Assert.state(getEntityManagerFactory() != null, "No EntityManagerFactory specified"); return (hasJpaProperties() ? getEntityManagerFactory().createEntityManager(getJpaPropertyMap()) : getEntityManagerFactory().createEntityManager()); } /** * Convert the given runtime exception to an appropriate exception from the {@code org.springframework.dao} * hierarchy if necessary, or return the exception itself if it is not persistence related. * * @param ex runtime exception that occured, which may or may not be JPA-related * @return the corresponding DataAccessException instance if wrapping should occur, otherwise the raw exception */ protected RuntimeException translateIfNecessary(RuntimeException ex) { if (isTranslateExceptions()) { return DataAccessUtils.translateIfNecessary(ex, getJpaDialect()); } else { return ex; } } /** * Apply the flush mode that's been specified. * * @param emHolder The current entity manager holder. * @param queryOptions the query options. */ protected void applyFlushMode(CurrentEntityManagerHolder emHolder, JpaQueryOptions queryOptions) { JpaFlushMode flushModeToUse = getFlushMode(queryOptions); FlushModeType flushModeToSet = null; FlushModeType previousFlushMode = null; if (flushModeToUse != null && flushModeToUse.value() != null) { if (emHolder.isNewEm()) { flushModeToSet = flushModeToUse.value(); } else { // we have an existing entity manager... previousFlushMode = emHolder.getEntityManager().getFlushMode(); if (previousFlushMode != flushModeToUse.value()) { flushModeToSet = flushModeToUse.value(); } } } if (flushModeToSet != null) { logger.debug("Setting flushMode to '{}' for generic repository execution", flushModeToSet); emHolder.getEntityManager().setFlushMode(flushModeToSet); } if (previousFlushMode != null) { emHolder.setPreviousFlushMode(previousFlushMode); } } /** * Flush the given the Hibernate session if necessary. * * @param emHolder The session holder. * @param queryOptions the query options. */ protected void flushIfNecessary(CurrentEntityManagerHolder emHolder, JpaQueryOptions queryOptions) { JpaFlushMode flushModeToUse = getFlushMode(queryOptions); if (flushModeToUse != null && flushModeToUse == JpaFlushMode.EAGER) { logger.debug("Eagerly flushing Jpa entity manager"); emHolder.getEntityManager().flush(); } } /** * {@inheritDoc} */ @Override public GrepoQueryJpaConfiguration getConfiguration() { return (GrepoQueryJpaConfiguration) super.getConfiguration(); } public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { this.entityManagerFactory = entityManagerFactory; } public EntityManagerFactory getEntityManagerFactory() { return entityManagerFactory; } public Map<String, Object> getJpaPropertyMap() { return jpaPropertyMap; } public void setJpaPropertyMap(Map<String, Object> jpaPropertyMap) { this.jpaPropertyMap = jpaPropertyMap; } /** * @return Returns {@code true} if jpa properties available. */ public boolean hasJpaProperties() { return !CollectionUtils.isEmpty(jpaPropertyMap); } public Map<String, Object> getDefaultQueryHints() { return defaultQueryHints; } public void setDefaultQueryHints(Map<String, Object> defaultQueryHints) { this.defaultQueryHints = defaultQueryHints; } public void setJpaDialect(JpaDialect jpaDialect) { this.jpaDialect = (jpaDialect == null ? new DefaultJpaDialect() : jpaDialect); } public JpaDialect getJpaDialect() { return this.jpaDialect; } public boolean isExposeNativeEntityManager() { return exposeNativeEntityManager; } public void setExposeNativeEntityManager(boolean exposeNativeEntityManager) { this.exposeNativeEntityManager = exposeNativeEntityManager; } public boolean isTranslateExceptions() { return translateExceptions; } public void setTranslateExceptions(boolean translateExceptions) { this.translateExceptions = translateExceptions; } public JpaFlushMode getFlushMode() { return flushMode; } /** * @param queryOptions The query options. * @return Returns the flush mode. */ public JpaFlushMode getFlushMode(JpaQueryOptions queryOptions) { JpaFlushMode flushModeToUse = getFlushMode(); if (queryOptions != null && queryOptions.flushMode() != JpaFlushMode.UNDEFINED) { flushModeToUse = queryOptions.flushMode(); } return flushModeToUse; } public void setFlushMode(JpaFlushMode flushMode) { this.flushMode = flushMode; } /** * @author dguggi */ protected class CurrentEntityManagerHolder { /** The is new flag. */ private boolean newEm; /** The entity manager. */ private EntityManager entityManager; /** The previous flush mode. */ private FlushModeType previousFlushMode; /** * @param isNew The flag. * @param entityManager The entity manager. */ public CurrentEntityManagerHolder(boolean isNew, EntityManager entityManager) { super(); this.newEm = isNew; this.entityManager = entityManager; } public boolean isNewEm() { return newEm; } public void setNewEm(boolean isNew) { this.newEm = isNew; } public EntityManager getEntityManager() { return entityManager; } public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } public FlushModeType getPreviousFlushMode() { return previousFlushMode; } public void setPreviousFlushMode(FlushModeType previousFlushMode) { this.previousFlushMode = previousFlushMode; } } /** * @author dguggi */ protected abstract class JpaCallbackCreator { /** * Creates a new transaction callback. * * @param qmpi The query method parameter info. Note that this parameter is null for methods which are not * annotated with {@code GenericQuery}. * @param doExposeNativeEntityManager Controls whether to expose the native JPA entity manager to callback code. * @return Returns the call back. */ public TransactionCallback<Object> create(final QueryMethodParameterInfo qmpi, final boolean doExposeNativeEntityManager) { return new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus status) { CurrentEntityManagerHolder emHolder = getCurrentEntityManager(); JpaQueryOptions queryOptions = null; if (qmpi != null) { queryOptions = qmpi.getMethodAnnotation(JpaQueryOptions.class); } try { applyFlushMode(emHolder, queryOptions); Object result = doExecute( createQueryExecutionContext(emHolder, doExposeNativeEntityManager)); flushIfNecessary(emHolder, queryOptions); return result; } catch (RuntimeException e) { throw translateIfNecessary(e); } finally { closeNewEntityManager(emHolder); } } }; } /** * Stuff to be executed in transaction. * * @param context The hibernate query execution context. * @return Returns the result or {@code null}. */ protected abstract Object doExecute(JpaQueryExecutionContext context); } /** * Invocation handler that suppresses close calls on JPA EntityManagers. * * @author dguggi */ private class CloseSuppressingInvocationHandler implements InvocationHandler { /** The target entity manager. */ private final EntityManager target; /** * @param target The target to set. */ public CloseSuppressingInvocationHandler(EntityManager target) { this.target = target; } /** * {@inheritDoc} */ 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 { return method.invoke(this.target, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } }