diff --git a/org.springframework.orm/build.xml b/org.springframework.orm/build.xml new file mode 100644 index 00000000000..ac4c8153a52 --- /dev/null +++ b/org.springframework.orm/build.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.springframework.orm/ivy.xml b/org.springframework.orm/ivy.xml new file mode 100644 index 00000000000..bd163c81858 --- /dev/null +++ b/org.springframework.orm/ivy.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.orm/pom.xml b/org.springframework.orm/pom.xml new file mode 100644 index 00000000000..b4cee14adcb --- /dev/null +++ b/org.springframework.orm/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + org.springframework + org.springframework.core + jar + Spring Core Abstractions and Utilities + 3.0.0.M1 + + + com.springsource.repository.bundles.external + SpringSource Enterprise Bundle Repository - External Bundle Releases + http://repository.springsource.com/maven/bundles/external + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + + + org.apache.commons + com.springsource.org.apache.commons.logging + 1.1.1 + + + org.apache.log4j + com.springsource.org.apache.log4j + 1.2.15 + true + + + org.apache.commons + com.springsource.org.apache.commons.collections + 3.2.0 + true + + + org.aspectj + com.springsource.org.aspectj.weaver + 1.6.2.RELEASE + true + + + org.objectweb.asm + com.springsource.org.objectweb.asm.commons + 2.2.3 + true + + + \ No newline at end of file diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java new file mode 100644 index 00000000000..4cd1dc8da7b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2006 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; + +import org.springframework.dao.OptimisticLockingFailureException; + +/** + * Exception thrown on an optimistic locking violation for a mapped object. + * Provides information about the persistent class and the identifier. + * + * @author Juergen Hoeller + * @since 13.10.2003 + */ +public class ObjectOptimisticLockingFailureException extends OptimisticLockingFailureException { + + private Object persistentClass; + + private Object identifier; + + + /** + * Create a general ObjectOptimisticLockingFailureException with the given message, + * without any information on the affected object. + * @param msg the detail message + * @param cause the source exception + */ + public ObjectOptimisticLockingFailureException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Create a new ObjectOptimisticLockingFailureException for the given object, + * with the default "optimistic locking failed" message. + * @param persistentClass the persistent class + * @param identifier the ID of the object for which the locking failed + */ + public ObjectOptimisticLockingFailureException(Class persistentClass, Object identifier) { + this(persistentClass, identifier, null); + } + + /** + * Create a new ObjectOptimisticLockingFailureException for the given object, + * with the default "optimistic locking failed" message. + * @param persistentClass the persistent class + * @param identifier the ID of the object for which the locking failed + * @param cause the source exception + */ + public ObjectOptimisticLockingFailureException( + Class persistentClass, Object identifier, Throwable cause) { + + this(persistentClass, identifier, + "Object of class [" + persistentClass.getName() + "] with identifier [" + identifier + + "]: optimistic locking failed", cause); + } + + /** + * Create a new ObjectOptimisticLockingFailureException for the given object, + * with the given explicit message. + * @param persistentClass the persistent class + * @param identifier the ID of the object for which the locking failed + * @param msg the detail message + * @param cause the source exception + */ + public ObjectOptimisticLockingFailureException( + Class persistentClass, Object identifier, String msg, Throwable cause) { + + super(msg, cause); + this.persistentClass = persistentClass; + this.identifier = identifier; + } + + /** + * Create a new ObjectOptimisticLockingFailureException for the given object, + * with the default "optimistic locking failed" message. + * @param persistentClassName the name of the persistent class + * @param identifier the ID of the object for which the locking failed + */ + public ObjectOptimisticLockingFailureException(String persistentClassName, Object identifier) { + this(persistentClassName, identifier, null); + } + + /** + * Create a new ObjectOptimisticLockingFailureException for the given object, + * with the default "optimistic locking failed" message. + * @param persistentClassName the name of the persistent class + * @param identifier the ID of the object for which the locking failed + * @param cause the source exception + */ + public ObjectOptimisticLockingFailureException( + String persistentClassName, Object identifier, Throwable cause) { + + this(persistentClassName, identifier, + "Object of class [" + persistentClassName + "] with identifier [" + identifier + + "]: optimistic locking failed", cause); + } + + /** + * Create a new ObjectOptimisticLockingFailureException for the given object, + * with the given explicit message. + * @param persistentClassName the name of the persistent class + * @param identifier the ID of the object for which the locking failed + * @param msg the detail message + * @param cause the source exception + */ + public ObjectOptimisticLockingFailureException( + String persistentClassName, Object identifier, String msg, Throwable cause) { + + super(msg, cause); + this.persistentClass = persistentClassName; + this.identifier = identifier; + } + + + /** + * Return the persistent class of the object for which the locking failed. + * If no Class was specified, this method returns null. + */ + public Class getPersistentClass() { + return (this.persistentClass instanceof Class ? (Class) this.persistentClass : null); + } + + /** + * Return the name of the persistent class of the object for which the locking failed. + * Will work for both Class objects and String names. + */ + public String getPersistentClassName() { + if (this.persistentClass instanceof Class) { + return ((Class) this.persistentClass).getName(); + } + return (this.persistentClass != null ? this.persistentClass.toString() : null); + } + + /** + * Return the identifier of the object for which the locking failed. + */ + public Object getIdentifier() { + return identifier; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java new file mode 100644 index 00000000000..b9151890fc2 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2006 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; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * Exception thrown if a mapped object could not be retrieved via its identifier. + * Provides information about the persistent class and the identifier. + * + * @author Juergen Hoeller + * @since 13.10.2003 + */ +public class ObjectRetrievalFailureException extends DataRetrievalFailureException { + + private Object persistentClass; + + private Object identifier; + + + /** + * Create a general ObjectRetrievalFailureException with the given message, + * without any information on the affected object. + * @param msg the detail message + * @param cause the source exception + */ + public ObjectRetrievalFailureException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Create a new ObjectRetrievalFailureException for the given object, + * with the default "not found" message. + * @param persistentClass the persistent class + * @param identifier the ID of the object that should have been retrieved + */ + public ObjectRetrievalFailureException(Class persistentClass, Object identifier) { + this(persistentClass, identifier, + "Object of class [" + persistentClass.getName() + "] with identifier [" + identifier + "]: not found", + null); + } + + /** + * Create a new ObjectRetrievalFailureException for the given object, + * with the given explicit message and exception. + * @param persistentClass the persistent class + * @param identifier the ID of the object that should have been retrieved + * @param msg the detail message + * @param cause the source exception + */ + public ObjectRetrievalFailureException( + Class persistentClass, Object identifier, String msg, Throwable cause) { + + super(msg, cause); + this.persistentClass = persistentClass; + this.identifier = identifier; + } + + /** + * Create a new ObjectRetrievalFailureException for the given object, + * with the default "not found" message. + * @param persistentClassName the name of the persistent class + * @param identifier the ID of the object that should have been retrieved + */ + public ObjectRetrievalFailureException(String persistentClassName, Object identifier) { + this(persistentClassName, identifier, + "Object of class [" + persistentClassName + "] with identifier [" + identifier + "]: not found", + null); + } + + /** + * Create a new ObjectRetrievalFailureException for the given object, + * with the given explicit message and exception. + * @param persistentClassName the name of the persistent class + * @param identifier the ID of the object that should have been retrieved + * @param msg the detail message + * @param cause the source exception + */ + public ObjectRetrievalFailureException( + String persistentClassName, Object identifier, String msg, Throwable cause) { + + super(msg, cause); + this.persistentClass = persistentClassName; + this.identifier = identifier; + } + + + /** + * Return the persistent class of the object that was not found. + * If no Class was specified, this method returns null. + */ + public Class getPersistentClass() { + return (this.persistentClass instanceof Class ? (Class) this.persistentClass : null); + } + + /** + * Return the name of the persistent class of the object that was not found. + * Will work for both Class objects and String names. + */ + public String getPersistentClassName() { + if (this.persistentClass instanceof Class) { + return ((Class) this.persistentClass).getName(); + } + return (this.persistentClass != null ? this.persistentClass.toString() : null); + } + + /** + * Return the identifier of the object that was not found. + */ + public Object getIdentifier() { + return identifier; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/AbstractSessionFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/AbstractSessionFactoryBean.java new file mode 100644 index 00000000000..8e5acc85dae --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/AbstractSessionFactoryBean.java @@ -0,0 +1,336 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.SessionFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * Abstract {@link org.springframework.beans.factory.FactoryBean} that creates + * a Hibernate {@link org.hibernate.SessionFactory} within a Spring application + * context, providing general infrastructure not related to Hibernate's + * specific configuration API. + * + *

This class implements the + * {@link org.springframework.dao.support.PersistenceExceptionTranslator} + * interface, as autodetected by Spring's + * {@link org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor}, + * for AOP-based translation of native exceptions to Spring DataAccessExceptions. + * Hence, the presence of e.g. LocalSessionFactoryBean automatically enables + * a PersistenceExceptionTranslationPostProcessor to translate Hibernate exceptions. + * + *

This class mainly serves as common base class for {@link LocalSessionFactoryBean}. + * For details on typical SessionFactory setup, see the LocalSessionFactoryBean javadoc. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setExposeTransactionAwareSessionFactory + * @see org.hibernate.SessionFactory#getCurrentSession() + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + */ +public abstract class AbstractSessionFactoryBean + implements FactoryBean, InitializingBean, DisposableBean, PersistenceExceptionTranslator { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private DataSource dataSource; + + private boolean useTransactionAwareDataSource = false; + + private boolean exposeTransactionAwareSessionFactory = true; + + private SQLExceptionTranslator jdbcExceptionTranslator; + + private SessionFactory sessionFactory; + + + /** + * Set the DataSource to be used by the SessionFactory. + * If set, this will override corresponding settings in Hibernate properties. + *

If this is set, the Hibernate settings should not define + * a connection provider to avoid meaningless double configuration. + *

If using HibernateTransactionManager as transaction strategy, consider + * proxying your target DataSource with a LazyConnectionDataSourceProxy. + * This defers fetching of an actual JDBC Connection until the first JDBC + * Statement gets executed, even within JDBC transactions (as performed by + * HibernateTransactionManager). Such lazy fetching is particularly beneficial + * for read-only operations, in particular if the chances of resolving the + * result in the second-level cache are high. + *

As JTA and transactional JNDI DataSources already provide lazy enlistment + * of JDBC Connections, LazyConnectionDataSourceProxy does not add value with + * JTA (i.e. Spring's JtaTransactionManager) as transaction strategy. + * @see #setUseTransactionAwareDataSource + * @see HibernateTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Return the DataSource to be used by the SessionFactory. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set whether to use a transaction-aware DataSource for the SessionFactory, + * i.e. whether to automatically wrap the passed-in DataSource with Spring's + * TransactionAwareDataSourceProxy. + *

Default is "false": LocalSessionFactoryBean is usually used with Spring's + * HibernateTransactionManager or JtaTransactionManager, both of which work nicely + * on a plain JDBC DataSource. Hibernate Sessions and their JDBC Connections are + * fully managed by the Hibernate/JTA transaction infrastructure in such a scenario. + *

If you switch this flag to "true", Spring's Hibernate access will be able to + * participate in JDBC-based transactions managed outside of Hibernate + * (for example, by Spring's DataSourceTransactionManager). This can be convenient + * if you need a different local transaction strategy for another O/R mapping tool, + * for example, but still want Hibernate access to join into those transactions. + *

A further benefit of this option is that plain Sessions opened directly + * via the SessionFactory, outside of Spring's Hibernate support, will still + * participate in active Spring-managed transactions. However, consider using + * Hibernate's getCurrentSession() method instead (see javadoc of + * "exposeTransactionAwareSessionFactory" property). + *

WARNING: When using a transaction-aware JDBC DataSource in combination + * with OpenSessionInViewFilter/Interceptor, whether participating in JTA or + * external JDBC-based transactions, it is strongly recommended to set Hibernate's + * Connection release mode to "after_transaction" or "after_statement", which + * guarantees proper Connection handling in such a scenario. In contrast to that, + * HibernateTransactionManager generally requires release mode "on_close". + *

Note: If you want to use Hibernate's Connection release mode "after_statement" + * with a DataSource specified on this LocalSessionFactoryBean (for example, a + * JTA-aware DataSource fetched from JNDI), switch this setting to "true". + * Otherwise, the ConnectionProvider used underneath will vote against aggressive + * release and thus silently switch to release mode "after_transaction". + * @see #setDataSource + * @see #setExposeTransactionAwareSessionFactory + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + * @see HibernateTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) { + this.useTransactionAwareDataSource = useTransactionAwareDataSource; + } + + /** + * Return whether to use a transaction-aware DataSource for the SessionFactory. + */ + protected boolean isUseTransactionAwareDataSource() { + return this.useTransactionAwareDataSource; + } + + /** + * Set whether to expose a transaction-aware current Session from the + * SessionFactory's getCurrentSession() method, returning the + * Session that's associated with the current Spring-managed transaction, if any. + *

Default is "true", letting data access code work with the plain + * Hibernate SessionFactory and its getCurrentSession() method, + * while still being able to participate in current Spring-managed transactions: + * with any transaction management strategy, either local or JTA / EJB CMT, + * and any transaction synchronization mechanism, either Spring or JTA. + * Furthermore, getCurrentSession() will also seamlessly work with + * a request-scoped Session managed by OpenSessionInViewFilter/Interceptor. + *

Turn this flag off to expose the plain Hibernate SessionFactory with + * Hibernate's default getCurrentSession() behavior, supporting + * plain JTA synchronization only. Alternatively, simply override the + * corresponding Hibernate property "hibernate.current_session_context_class". + * @see SpringSessionContext + * @see org.hibernate.SessionFactory#getCurrentSession() + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see HibernateTransactionManager + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + */ + public void setExposeTransactionAwareSessionFactory(boolean exposeTransactionAwareSessionFactory) { + this.exposeTransactionAwareSessionFactory = exposeTransactionAwareSessionFactory; + } + + /** + * Return whether to expose a transaction-aware proxy for the SessionFactory. + */ + protected boolean isExposeTransactionAwareSessionFactory() { + return this.exposeTransactionAwareSessionFactory; + } + + /** + * Set the JDBC exception translator for the SessionFactory, + * exposed via the PersistenceExceptionTranslator interface. + *

Applied to any SQLException root cause of a Hibernate JDBCException, + * overriding Hibernate's default SQLException translation (which is + * based on Hibernate's Dialect for a specific target database). + * @param jdbcExceptionTranslator the exception translator + * @see java.sql.SQLException + * @see org.hibernate.JDBCException + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + * @see org.springframework.dao.support.PersistenceExceptionTranslator + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + + /** + * Build and expose the SessionFactory. + * @see #buildSessionFactory() + * @see #wrapSessionFactoryIfNecessary + */ + public void afterPropertiesSet() throws Exception { + SessionFactory rawSf = buildSessionFactory(); + this.sessionFactory = wrapSessionFactoryIfNecessary(rawSf); + afterSessionFactoryCreation(); + } + + /** + * Wrap the given SessionFactory with a proxy, if demanded. + *

The default implementation simply returns the given SessionFactory as-is. + * Subclasses may override this to implement transaction awareness through + * a SessionFactory proxy, for example. + * @param rawSf the raw SessionFactory as built by {@link #buildSessionFactory()} + * @return the SessionFactory reference to expose + * @see #buildSessionFactory() + */ + protected SessionFactory wrapSessionFactoryIfNecessary(SessionFactory rawSf) { + return rawSf; + } + + /** + * Return the exposed SessionFactory. + * Will throw an exception if not initialized yet. + * @return the SessionFactory (never null) + * @throws IllegalStateException if the SessionFactory has not been initialized yet + */ + protected final SessionFactory getSessionFactory() { + if (this.sessionFactory == null) { + throw new IllegalStateException("SessionFactory not initialized yet"); + } + return this.sessionFactory; + } + + /** + * Close the SessionFactory on bean factory shutdown. + */ + public void destroy() throws HibernateException { + logger.info("Closing Hibernate SessionFactory"); + try { + beforeSessionFactoryDestruction(); + } + finally { + this.sessionFactory.close(); + } + } + + + /** + * Return the singleton SessionFactory. + */ + public Object getObject() { + return this.sessionFactory; + } + + public Class getObjectType() { + return (this.sessionFactory != null) ? this.sessionFactory.getClass() : SessionFactory.class; + } + + public boolean isSingleton() { + return true; + } + + + /** + * Implementation of the PersistenceExceptionTranslator interface, + * as autodetected by Spring's PersistenceExceptionTranslationPostProcessor. + *

Converts the exception if it is a HibernateException; + * else returns null to indicate an unknown exception. + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * @see #convertHibernateAccessException + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof HibernateException) { + return convertHibernateAccessException((HibernateException) ex); + } + return null; + } + + /** + * Convert the given HibernateException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * Hibernate JDBCException, else rely on Hibernate's default translation. + * @param ex HibernateException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertHibernateAccessException + * @see #setJdbcExceptionTranslator + */ + protected DataAccessException convertHibernateAccessException(HibernateException ex) { + if (this.jdbcExceptionTranslator != null && ex instanceof JDBCException) { + JDBCException jdbcEx = (JDBCException) ex; + return this.jdbcExceptionTranslator.translate( + "Hibernate operation: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); + } + return SessionFactoryUtils.convertHibernateAccessException(ex); + } + + + /** + * Build the underlying Hibernate SessionFactory. + * @return the raw SessionFactory (potentially to be wrapped with a + * transaction-aware proxy before it is exposed to the application) + * @throws Exception in case of initialization failure + */ + protected abstract SessionFactory buildSessionFactory() throws Exception; + + /** + * Hook that allows post-processing after the SessionFactory has been + * successfully created. The SessionFactory is already available through + * getSessionFactory() at this point. + *

This implementation is empty. + * @throws Exception in case of initialization failure + * @see #getSessionFactory() + */ + protected void afterSessionFactoryCreation() throws Exception { + } + + /** + * Hook that allows shutdown processing before the SessionFactory + * will be closed. The SessionFactory is still available through + * getSessionFactory() at this point. + *

This implementation is empty. + * @see #getSessionFactory() + */ + protected void beforeSessionFactoryDestruction() { + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/FilterDefinitionFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/FilterDefinitionFactoryBean.java new file mode 100644 index 00000000000..c63c3ac4d93 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/FilterDefinitionFactoryBean.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.hibernate.engine.FilterDefinition; +import org.hibernate.type.TypeFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * Convenient FactoryBean for defining Hibernate FilterDefinitions. + * Exposes a corresponding Hibernate FilterDefinition object. + * + *

Typically defined as an inner bean within a LocalSessionFactoryBean + * definition, as the list element for the "filterDefinitions" bean property. + * For example: + * + *

+ * <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
+ *   ...
+ *   <property name="filterDefinitions">
+ *     <list>
+ *       <bean class="org.springframework.orm.hibernate3.FilterDefinitionFactoryBean">
+ *         <property name="filterName" value="myFilter"/>
+ *         <property name="parameterTypes">
+ *           <props>
+ *             <prop key="myParam">string</prop>
+ *             <prop key="myOtherParam">long</prop>
+ *           </props>
+ *         </property>
+ *       </bean>
+ *     </list>
+ *   </property>
+ *   ...
+ * </bean>
+ * + * Alternatively, specify a bean id (or name) attribute for the inner bean, + * instead of the "filterName" property. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.hibernate.engine.FilterDefinition + * @see LocalSessionFactoryBean#setFilterDefinitions + */ +public class FilterDefinitionFactoryBean implements FactoryBean, BeanNameAware, InitializingBean { + + private String filterName; + + private Map parameterTypeMap = new HashMap(); + + private String defaultFilterCondition; + + private FilterDefinition filterDefinition; + + + /** + * Set the name of the filter. + */ + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + /** + * Set the parameter types for the filter, + * with parameter names as keys and type names as values. + * @see org.hibernate.type.TypeFactory#heuristicType(String) + */ + public void setParameterTypes(Properties parameterTypes) { + if (parameterTypes != null) { + this.parameterTypeMap = new HashMap(parameterTypes.size()); + for (Enumeration names = parameterTypes.propertyNames(); names.hasMoreElements();) { + String paramName = (String) names.nextElement(); + String typeName = parameterTypes.getProperty(paramName); + this.parameterTypeMap.put(paramName, TypeFactory.heuristicType(typeName)); + } + } + else { + this.parameterTypeMap = new HashMap(); + } + } + + /** + * Specify a default filter condition for the filter, if any. + */ + public void setDefaultFilterCondition(String defaultFilterCondition) { + this.defaultFilterCondition = defaultFilterCondition; + } + + /** + * If no explicit filter name has been specified, the bean name of + * the FilterDefinitionFactoryBean will be used. + * @see #setFilterName + */ + public void setBeanName(String name) { + if (this.filterName == null) { + this.filterName = name; + } + } + + public void afterPropertiesSet() { + this.filterDefinition = + new FilterDefinition(this.filterName, this.defaultFilterCondition, this.parameterTypeMap); + } + + + public Object getObject() { + return this.filterDefinition; + } + + public Class getObjectType() { + return FilterDefinition.class; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateAccessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateAccessor.java new file mode 100644 index 00000000000..45d2228abbe --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateAccessor.java @@ -0,0 +1,489 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +import java.sql.SQLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Interceptor; +import org.hibernate.JDBCException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.exception.GenericJDBCException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Constants; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * Base class for {@link HibernateTemplate} and {@link HibernateInterceptor}, + * defining common properties such as SessionFactory and flushing behavior. + * + *

Not intended to be used directly. + * See {@link HibernateTemplate} and {@link HibernateInterceptor}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see HibernateTemplate + * @see HibernateInterceptor + * @see #setFlushMode + */ +public abstract class HibernateAccessor implements InitializingBean, BeanFactoryAware { + + /** + * Never flush is a good strategy for read-only units of work. + * Hibernate will not track and look for changes in this case, + * avoiding any overhead of modification detection. + *

In case of an existing Session, FLUSH_NEVER will turn the flush mode + * to NEVER for the scope of the current operation, resetting the previous + * flush mode afterwards. + * @see #setFlushMode + */ + public static final int FLUSH_NEVER = 0; + + /** + * Automatic flushing is the default mode for a Hibernate Session. + * A session will get flushed on transaction commit, and on certain find + * operations that might involve already modified instances, but not + * after each unit of work like with eager flushing. + *

In case of an existing Session, FLUSH_AUTO will participate in the + * existing flush mode, not modifying it for the current operation. + * This in particular means that this setting will not modify an existing + * flush mode NEVER, in contrast to FLUSH_EAGER. + * @see #setFlushMode + */ + public static final int FLUSH_AUTO = 1; + + /** + * Eager flushing leads to immediate synchronization with the database, + * even if in a transaction. This causes inconsistencies to show up and throw + * a respective exception immediately, and JDBC access code that participates + * in the same transaction will see the changes as the database is already + * aware of them then. But the drawbacks are: + *

+ *

In case of an existing Session, FLUSH_EAGER will turn the flush mode + * to AUTO for the scope of the current operation and issue a flush at the + * end, resetting the previous flush mode afterwards. + * @see #setFlushMode + */ + public static final int FLUSH_EAGER = 2; + + /** + * Flushing at commit only is intended for units of work where no + * intermediate flushing is desired, not even for find operations + * that might involve already modified instances. + *

In case of an existing Session, FLUSH_COMMIT will turn the flush mode + * to COMMIT for the scope of the current operation, resetting the previous + * flush mode afterwards. The only exception is an existing flush mode + * NEVER, which will not be modified through this setting. + * @see #setFlushMode + */ + public static final int FLUSH_COMMIT = 3; + + /** + * Flushing before every query statement is rarely necessary. + * It is only available for special needs. + *

In case of an existing Session, FLUSH_ALWAYS will turn the flush mode + * to ALWAYS for the scope of the current operation, resetting the previous + * flush mode afterwards. + * @see #setFlushMode + */ + public static final int FLUSH_ALWAYS = 4; + + + /** Constants instance for HibernateAccessor */ + private static final Constants constants = new Constants(HibernateAccessor.class); + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private SessionFactory sessionFactory; + + private Object entityInterceptor; + + private SQLExceptionTranslator jdbcExceptionTranslator; + + private SQLExceptionTranslator defaultJdbcExceptionTranslator; + + private int flushMode = FLUSH_AUTO; + + private String[] filterNames; + + /** + * Just needed for entityInterceptorBeanName. + * @see #setEntityInterceptorBeanName + */ + private BeanFactory beanFactory; + + + /** + * Set the Hibernate SessionFactory that should be used to create + * Hibernate Sessions. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the Hibernate SessionFactory that should be used to create + * Hibernate Sessions. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + /** + * Set the bean name of a Hibernate entity interceptor that allows to inspect + * and change property values before writing to and reading from the database. + * Will get applied to any new Session created by this transaction manager. + *

Requires the bean factory to be known, to be able to resolve the bean + * name to an interceptor instance on session creation. Typically used for + * prototype interceptors, i.e. a new interceptor instance per session. + *

Can also be used for shared interceptor instances, but it is recommended + * to set the interceptor reference directly in such a scenario. + * @param entityInterceptorBeanName the name of the entity interceptor in + * the bean factory + * @see #setBeanFactory + * @see #setEntityInterceptor + */ + public void setEntityInterceptorBeanName(String entityInterceptorBeanName) { + this.entityInterceptor = entityInterceptorBeanName; + } + + /** + * Set a Hibernate entity interceptor that allows to inspect and change + * property values before writing to and reading from the database. + * Will get applied to any new Session created by this object. + *

Such an interceptor can either be set at the SessionFactory level, + * i.e. on LocalSessionFactoryBean, or at the Session level, i.e. on + * HibernateTemplate, HibernateInterceptor, and HibernateTransactionManager. + * It's preferable to set it on LocalSessionFactoryBean or HibernateTransactionManager + * to avoid repeated configuration and guarantee consistent behavior in transactions. + * @see #setEntityInterceptorBeanName + * @see LocalSessionFactoryBean#setEntityInterceptor + * @see HibernateTransactionManager#setEntityInterceptor + */ + public void setEntityInterceptor(Interceptor entityInterceptor) { + this.entityInterceptor = entityInterceptor; + } + + /** + * Return the current Hibernate entity interceptor, or null if none. + * Resolves an entity interceptor bean name via the bean factory, + * if necessary. + * @throws IllegalStateException if bean name specified but no bean factory set + * @throws org.springframework.beans.BeansException if bean name resolution via the bean factory failed + * @see #setEntityInterceptor + * @see #setEntityInterceptorBeanName + * @see #setBeanFactory + */ + public Interceptor getEntityInterceptor() throws IllegalStateException, BeansException { + if (this.entityInterceptor instanceof String) { + if (this.beanFactory == null) { + throw new IllegalStateException("Cannot get entity interceptor via bean name if no bean factory set"); + } + return (Interceptor) this.beanFactory.getBean((String) this.entityInterceptor, Interceptor.class); + } + return (Interceptor) this.entityInterceptor; + } + + /** + * Set the JDBC exception translator for this instance. + *

Applied to any SQLException root cause of a Hibernate JDBCException, + * overriding Hibernate's default SQLException translation (which is + * based on Hibernate's Dialect for a specific target database). + * @param jdbcExceptionTranslator the exception translator + * @see java.sql.SQLException + * @see org.hibernate.JDBCException + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + /** + * Return the JDBC exception translator for this instance, if any. + */ + public SQLExceptionTranslator getJdbcExceptionTranslator() { + return this.jdbcExceptionTranslator; + } + + /** + * Set the flush behavior by the name of the respective constant + * in this class, e.g. "FLUSH_AUTO". Default is "FLUSH_AUTO". + * @param constantName name of the constant + * @see #setFlushMode + * @see #FLUSH_AUTO + */ + public void setFlushModeName(String constantName) { + setFlushMode(constants.asNumber(constantName).intValue()); + } + + /** + * Set the flush behavior to one of the constants in this class. + * Default is FLUSH_AUTO. + * @see #setFlushModeName + * @see #FLUSH_AUTO + */ + public void setFlushMode(int flushMode) { + this.flushMode = flushMode; + } + + /** + * Return if a flush should be forced after executing the callback code. + */ + public int getFlushMode() { + return this.flushMode; + } + + /** + * Set the name of a Hibernate filter to be activated for all + * Sessions that this accessor works with. + *

This filter will be enabled at the beginning of each operation + * and correspondingly disabled at the end of the operation. + * This will work for newly opened Sessions as well as for existing + * Sessions (for example, within a transaction). + * @see #enableFilters(org.hibernate.Session) + * @see org.hibernate.Session#enableFilter(String) + * @see LocalSessionFactoryBean#setFilterDefinitions + */ + public void setFilterName(String filter) { + this.filterNames = new String[] {filter}; + } + + /** + * Set one or more names of Hibernate filters to be activated for all + * Sessions that this accessor works with. + *

Each of those filters will be enabled at the beginning of each + * operation and correspondingly disabled at the end of the operation. + * This will work for newly opened Sessions as well as for existing + * Sessions (for example, within a transaction). + * @see #enableFilters(org.hibernate.Session) + * @see org.hibernate.Session#enableFilter(String) + * @see LocalSessionFactoryBean#setFilterDefinitions + */ + public void setFilterNames(String[] filterNames) { + this.filterNames = filterNames; + } + + /** + * Return the names of Hibernate filters to be activated, if any. + */ + public String[] getFilterNames() { + return this.filterNames; + } + + /** + * The bean factory just needs to be known for resolving entity interceptor + * bean names. It does not need to be set for any other mode of operation. + * @see #setEntityInterceptorBeanName + */ + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public void afterPropertiesSet() { + if (getSessionFactory() == null) { + throw new IllegalArgumentException("Property 'sessionFactory' is required"); + } + } + + + /** + * Apply the flush mode that's been specified for this accessor + * to the given Session. + * @param session the current Hibernate Session + * @param existingTransaction if executing within an existing transaction + * @return the previous flush mode to restore after the operation, + * or null if none + * @see #setFlushMode + * @see org.hibernate.Session#setFlushMode + */ + protected FlushMode applyFlushMode(Session session, boolean existingTransaction) { + if (getFlushMode() == FLUSH_NEVER) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getFlushMode(); + if (!previousFlushMode.lessThan(FlushMode.COMMIT)) { + session.setFlushMode(FlushMode.NEVER); + return previousFlushMode; + } + } + else { + session.setFlushMode(FlushMode.NEVER); + } + } + else if (getFlushMode() == FLUSH_EAGER) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getFlushMode(); + if (!previousFlushMode.equals(FlushMode.AUTO)) { + session.setFlushMode(FlushMode.AUTO); + return previousFlushMode; + } + } + else { + // rely on default FlushMode.AUTO + } + } + else if (getFlushMode() == FLUSH_COMMIT) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getFlushMode(); + if (previousFlushMode.equals(FlushMode.AUTO) || previousFlushMode.equals(FlushMode.ALWAYS)) { + session.setFlushMode(FlushMode.COMMIT); + return previousFlushMode; + } + } + else { + session.setFlushMode(FlushMode.COMMIT); + } + } + else if (getFlushMode() == FLUSH_ALWAYS) { + if (existingTransaction) { + FlushMode previousFlushMode = session.getFlushMode(); + if (!previousFlushMode.equals(FlushMode.ALWAYS)) { + session.setFlushMode(FlushMode.ALWAYS); + return previousFlushMode; + } + } + else { + session.setFlushMode(FlushMode.ALWAYS); + } + } + return null; + } + + /** + * Flush the given Hibernate Session if necessary. + * @param session the current Hibernate Session + * @param existingTransaction if executing within an existing transaction + * @throws HibernateException in case of Hibernate flushing errors + */ + protected void flushIfNecessary(Session session, boolean existingTransaction) throws HibernateException { + if (getFlushMode() == FLUSH_EAGER || (!existingTransaction && getFlushMode() != FLUSH_NEVER)) { + logger.debug("Eagerly flushing Hibernate session"); + session.flush(); + } + } + + + /** + * Convert the given HibernateException to an appropriate exception + * from the org.springframework.dao hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * Hibernate JDBCException, else rely on Hibernate's default translation. + * @param ex HibernateException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertHibernateAccessException + * @see #setJdbcExceptionTranslator + */ + public DataAccessException convertHibernateAccessException(HibernateException ex) { + if (getJdbcExceptionTranslator() != null && ex instanceof JDBCException) { + return convertJdbcAccessException((JDBCException) ex, getJdbcExceptionTranslator()); + } + else if (GenericJDBCException.class.equals(ex.getClass())) { + return convertJdbcAccessException((GenericJDBCException) ex, getDefaultJdbcExceptionTranslator()); + } + return SessionFactoryUtils.convertHibernateAccessException(ex); + } + + /** + * Convert the given Hibernate JDBCException to an appropriate exception + * from the org.springframework.dao hierarchy, using the + * given SQLExceptionTranslator. + * @param ex Hibernate JDBCException that occured + * @param translator the SQLExceptionTranslator to use + * @return a corresponding DataAccessException + */ + protected DataAccessException convertJdbcAccessException(JDBCException ex, SQLExceptionTranslator translator) { + return translator.translate("Hibernate operation: " + ex.getMessage(), ex.getSQL(), ex.getSQLException()); + } + + /** + * Convert the given SQLException to an appropriate exception from the + * org.springframework.dao hierarchy. Can be overridden in subclasses. + *

Note that a direct SQLException can just occur when callback code + * performs direct JDBC access via Session.connection(). + * @param ex the SQLException + * @return the corresponding DataAccessException instance + * @see #setJdbcExceptionTranslator + * @see org.hibernate.Session#connection() + */ + protected DataAccessException convertJdbcAccessException(SQLException ex) { + SQLExceptionTranslator translator = getJdbcExceptionTranslator(); + if (translator == null) { + translator = getDefaultJdbcExceptionTranslator(); + } + return translator.translate("Hibernate-related JDBC operation", null, ex); + } + + /** + * Obtain a default SQLExceptionTranslator, lazily creating it if necessary. + *

Creates a default + * {@link org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator} + * for the SessionFactory's underlying DataSource. + */ + protected synchronized SQLExceptionTranslator getDefaultJdbcExceptionTranslator() { + if (this.defaultJdbcExceptionTranslator == null) { + this.defaultJdbcExceptionTranslator = SessionFactoryUtils.newJdbcExceptionTranslator(getSessionFactory()); + } + return this.defaultJdbcExceptionTranslator; + } + + + /** + * Enable the specified filters on the given Session. + * @param session the current Hibernate Session + * @see #setFilterNames + * @see org.hibernate.Session#enableFilter(String) + */ + protected void enableFilters(Session session) { + String[] filterNames = getFilterNames(); + if (filterNames != null) { + for (int i = 0; i < filterNames.length; i++) { + session.enableFilter(filterNames[i]); + } + } + } + + /** + * Disable the specified filters on the given Session. + * @param session the current Hibernate Session + * @see #setFilterNames + * @see org.hibernate.Session#disableFilter(String) + */ + protected void disableFilters(Session session) { + String[] filterNames = getFilterNames(); + if (filterNames != null) { + for (int i = 0; i < filterNames.length; i++) { + session.disableFilter(filterNames[i]); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateCallback.java new file mode 100644 index 00000000000..38846b7d238 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateCallback.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2006 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.hibernate3; + +import java.sql.SQLException; + +import org.hibernate.HibernateException; +import org.hibernate.Session; + +/** + * Callback interface for Hibernate code. To be used with {@link HibernateTemplate}'s + * execution methods, often as anonymous classes within a method implementation. + * A typical implementation will call Session.load/find/update to perform + * some operations on persistent objects. It can also perform direct JDBC operations + * via Hibernate's Session.connection(), operating on a JDBC Connection. + * + *

Note that Hibernate works on unmodified plain Java objects, performing dirty + * detection via copies made at load time. Returned objects can thus be used outside + * of an active Hibernate Session without any hassle, e.g. for display in a web GUI. + * Reassociating such instances with a new Session, e.g. for updates when coming + * back from the GUI, is straightforward, as the instance has kept its identity. + * You should care to reassociate them as early as possible though, to avoid having + * already loaded a version from the database in the same Session. + * + * @author Juergen Hoeller + * @since 1.2 + * @see HibernateTemplate + * @see HibernateTransactionManager + */ +public interface HibernateCallback { + + /** + * Gets called by HibernateTemplate.execute with an active + * Hibernate Session. Does not need to care about activating + * or closing the Session, or handling transactions. + * + *

If called without a thread-bound Hibernate transaction (initiated + * by HibernateTransactionManager), the code will simply get executed on the + * underlying JDBC connection with its transactional semantics. If Hibernate + * is configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + * + *

Allows for returning a result object created within the callback, + * i.e. a domain object or a collection of domain objects. + * A thrown custom RuntimeException is treated as an application exception: + * It gets propagated to the caller of the template. + * + * @param session active Hibernate session + * @return a result object, or null if none + * @throws HibernateException if thrown by the Hibernate API + * @throws SQLException if thrown by Hibernate-exposed JDBC API + * @see HibernateTemplate#execute + * @see HibernateTemplate#executeFind + */ + Object doInHibernate(Session session) throws HibernateException, SQLException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateInterceptor.java new file mode 100644 index 00000000000..d53bee15402 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateInterceptor.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * This interceptor binds a new Hibernate Session to the thread before a method + * call, closing and removing it afterwards in case of any method outcome. + * If there already is a pre-bound Session (e.g. from HibernateTransactionManager, + * or from a surrounding Hibernate-intercepted method), the interceptor simply + * participates in it. + * + *

Application code must retrieve a Hibernate Session via the + * SessionFactoryUtils.getSession method or - preferably - + * Hibernate's own SessionFactory.getCurrentSession() method, to be + * able to detect a thread-bound Session. Typically, the code will look like as follows: + * + *

+ * public void doSomeDataAccessAction() {
+ *   Session session = this.sessionFactory.getCurrentSession();
+ *   ...
+ *   // No need to close the Session or translate exceptions!
+ * }
+ * + * Note that this interceptor automatically translates HibernateExceptions, + * via delegating to the SessionFactoryUtils.convertHibernateAccessException + * method that converts them to exceptions that are compatible with the + * org.springframework.dao exception hierarchy (like HibernateTemplate does). + * This can be turned off if the raw exceptions are preferred. + * + *

This class can be considered a declarative alternative to HibernateTemplate's + * callback approach. The advantages are: + *

+ * + *

The drawback is the dependency on interceptor configuration. However, note + * that this interceptor is usually not necessary in scenarios where the + * data access code always executes within transactions. A transaction will always + * have a thread-bound Session in the first place, so adding this interceptor to the + * configuration just adds value when fine-tuning Session settings like the flush mode + * - or when relying on exception translation. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.hibernate.SessionFactory#getCurrentSession() + * @see HibernateTransactionManager + * @see HibernateTemplate + */ +public class HibernateInterceptor extends HibernateAccessor implements MethodInterceptor { + + private boolean exceptionConversionEnabled = true; + + + /** + * Set whether to convert any HibernateException raised to a Spring DataAccessException, + * compatible with the org.springframework.dao exception hierarchy. + *

Default is "true". Turn this flag off to let the caller receive raw exceptions + * as-is, without any wrapping. + * @see org.springframework.dao.DataAccessException + */ + public void setExceptionConversionEnabled(boolean exceptionConversionEnabled) { + this.exceptionConversionEnabled = exceptionConversionEnabled; + } + + + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + Session session = getSession(); + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory()); + + boolean existingTransaction = (sessionHolder != null && sessionHolder.containsSession(session)); + if (existingTransaction) { + logger.debug("Found thread-bound Session for HibernateInterceptor"); + } + else { + if (sessionHolder != null) { + sessionHolder.addSession(session); + } + else { + TransactionSynchronizationManager.bindResource(getSessionFactory(), new SessionHolder(session)); + } + } + + FlushMode previousFlushMode = null; + try { + previousFlushMode = applyFlushMode(session, existingTransaction); + enableFilters(session); + Object retVal = methodInvocation.proceed(); + flushIfNecessary(session, existingTransaction); + return retVal; + } + catch (HibernateException ex) { + if (this.exceptionConversionEnabled) { + throw convertHibernateAccessException(ex); + } + else { + throw ex; + } + } + finally { + if (existingTransaction) { + logger.debug("Not closing pre-bound Hibernate Session after HibernateInterceptor"); + disableFilters(session); + if (previousFlushMode != null) { + session.setFlushMode(previousFlushMode); + } + } + else { + SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory()); + if (sessionHolder == null || sessionHolder.doesNotHoldNonDefaultSession()) { + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + } + } + } + + /** + * Return a Session for use by this interceptor. + * @see SessionFactoryUtils#getSession + */ + protected Session getSession() { + return SessionFactoryUtils.getSession( + getSessionFactory(), getEntityInterceptor(), getJdbcExceptionTranslator()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateJdbcException.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateJdbcException.java new file mode 100644 index 00000000000..c6cf41537f0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateJdbcException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +import java.sql.SQLException; + +import org.hibernate.JDBCException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Hibernate-specific subclass of UncategorizedDataAccessException, + * for JDBC exceptions that Hibernate wrapped. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +public class HibernateJdbcException extends UncategorizedDataAccessException { + + public HibernateJdbcException(JDBCException ex) { + super("JDBC exception on Hibernate data access: SQLException for SQL [" + ex.getSQL() + "]; SQL state [" + + ex.getSQLState() + "]; error code [" + ex.getErrorCode() + "]; " + ex.getMessage(), ex); + } + + /** + * Return the underlying SQLException. + */ + public SQLException getSQLException() { + return ((JDBCException) getCause()).getSQLException(); + } + + /** + * Return the SQL that led to the problem. + */ + public String getSql() { + return ((JDBCException) getCause()).getSQL(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateObjectRetrievalFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateObjectRetrievalFailureException.java new file mode 100644 index 00000000000..726430889c4 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateObjectRetrievalFailureException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 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.hibernate3; + +import org.hibernate.UnresolvableObjectException; +import org.hibernate.WrongClassException; + +import org.springframework.orm.ObjectRetrievalFailureException; + +/** + * Hibernate-specific subclass of ObjectRetrievalFailureException. + * Converts Hibernate's UnresolvableObjectException and WrongClassException. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +public class HibernateObjectRetrievalFailureException extends ObjectRetrievalFailureException { + + public HibernateObjectRetrievalFailureException(UnresolvableObjectException ex) { + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); + } + + public HibernateObjectRetrievalFailureException(WrongClassException ex) { + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateOperations.java new file mode 100644 index 00000000000..d162c8fa6ef --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateOperations.java @@ -0,0 +1,937 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.hibernate.Filter; +import org.hibernate.LockMode; +import org.hibernate.ReplicationMode; +import org.hibernate.criterion.DetachedCriteria; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that specifies a basic set of Hibernate operations, + * implemented by {@link HibernateTemplate}. Not often used, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Defines HibernateTemplate's data access methods that + * mirror various {@link org.hibernate.Session} methods. Users are + * strongly encouraged to read the Hibernate Session javadocs + * for details on the semantics of those methods. + * + *

Note that operations that return an {@link java.util.Iterator} (i.e. + * iterate(..)) are supposed to be used within Spring-driven + * or JTA-driven transactions (with {@link HibernateTransactionManager}, + * {@link org.springframework.transaction.jta.JtaTransactionManager}, + * or EJB CMT). Else, the Iterator won't be able to read + * results from its {@link java.sql.ResultSet} anymore, as the underlying + * Hibernate Session will already have been closed. + * + *

Note that lazy loading will just work with an open Hibernate + * Session, either within a transaction or within + * {@link org.springframework.orm.hibernate3.support.OpenSessionInViewFilter}/ + * {@link org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor}. + * Furthermore, some operations just make sense within transactions, + * for example: contains, evict, lock, + * flush, clear. + * + * @author Juergen Hoeller + * @since 1.2 + * @see HibernateTemplate + * @see org.hibernate.Session + * @see HibernateTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + */ +public interface HibernateOperations { + + /** + * Execute the action specified by the given action object within a + * {@link org.hibernate.Session}. + *

Application exceptions thrown by the action object get propagated + * to the caller (can only be unchecked). Hibernate exceptions are + * transformed into appropriate DAO ones. Allows for returning a result + * object, that is a domain object or a collection of domain objects. + *

Note: Callback code is not supposed to handle transactions itself! + * Use an appropriate transaction manager like + * {@link HibernateTransactionManager}. Generally, callback code must not + * touch any Session lifecycle methods, like close, + * disconnect, or reconnect, to let the template do its work. + * @param action callback object that specifies the Hibernate action + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see HibernateTransactionManager + * @see org.springframework.dao + * @see org.springframework.transaction + * @see org.hibernate.Session + */ + Object execute(HibernateCallback action) throws DataAccessException; + + /** + * Execute the specified action assuming that the result object is a + * {@link List}. + *

This is a convenience method for executing Hibernate find calls or + * queries within an action. + * @param action calback object that specifies the Hibernate action + * @return a List result returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + */ + List executeFind(HibernateCallback action) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for loading individual objects + //------------------------------------------------------------------------- + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or null if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(Class, java.io.Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @return the persistent instance, or null if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, java.io.Serializable) + */ + Object get(Class entityClass, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or null if not found. + *

Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(Class, java.io.Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance, or null if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, java.io.Serializable, org.hibernate.LockMode) + */ + Object get(Class entityClass, Serializable id, LockMode lockMode) + throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or null if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(String, java.io.Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @return the persistent instance, or null if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, java.io.Serializable) + */ + Object get(String entityName, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or null if not found. + * Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(String, java.io.Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance, or null if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, java.io.Serializable, org.hibernate.LockMode) + */ + Object get(String entityName, Serializable id, LockMode lockMode) + throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(Class, java.io.Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, java.io.Serializable) + */ + Object load(Class entityClass, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + * Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(Class, java.io.Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, java.io.Serializable) + */ + Object load(Class entityClass, Serializable id, LockMode lockMode) + throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(String, java.io.Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, java.io.Serializable) + */ + Object load(String entityName, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + *

Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(String, java.io.Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, java.io.Serializable) + */ + Object load(String entityName, Serializable id, LockMode lockMode) + throws DataAccessException; + + /** + * Return all persistent instances of the given entity class. + * Note: Use queries or criteria for retrieving a specific subset. + * @param entityClass a persistent class + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException if there is a Hibernate error + * @see org.hibernate.Session#createCriteria + */ + List loadAll(Class entityClass) throws DataAccessException; + + /** + * Load the persistent instance with the given identifier + * into the given object, throwing an exception if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(Object, java.io.Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entity the object (of the target class) to load into + * @param id the identifier of the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Object, java.io.Serializable) + */ + void load(Object entity, Serializable id) throws DataAccessException; + + /** + * Re-read the state of the given persistent instance. + * @param entity the persistent instance to re-read + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#refresh(Object) + */ + void refresh(Object entity) throws DataAccessException; + + /** + * Re-read the state of the given persistent instance. + * Obtains the specified lock mode for the instance. + * @param entity the persistent instance to re-read + * @param lockMode the lock mode to obtain + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#refresh(Object, org.hibernate.LockMode) + */ + void refresh(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Check whether the given object is in the Session cache. + * @param entity the persistence instance to check + * @return whether the given object is in the Session cache + * @throws org.springframework.dao.DataAccessException if there is a Hibernate error + * @see org.hibernate.Session#contains + */ + boolean contains(Object entity) throws DataAccessException; + + /** + * Remove the given object from the {@link org.hibernate.Session} cache. + * @param entity the persistent instance to evict + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#evict + */ + void evict(Object entity) throws DataAccessException; + + /** + * Force initialization of a Hibernate proxy or persistent collection. + * @param proxy a proxy for a persistent object or a persistent collection + * @throws DataAccessException if we can't initialize the proxy, for example + * because it is not associated with an active Session + * @see org.hibernate.Hibernate#initialize + */ + void initialize(Object proxy) throws DataAccessException; + + /** + * Return an enabled Hibernate {@link Filter} for the given filter name. + * The returned Filter instance can be used to set filter parameters. + * @param filterName the name of the filter + * @return the enabled Hibernate Filter (either already + * enabled or enabled on the fly by this operation) + * @throws IllegalStateException if we are not running within a + * transactional Session (in which case this operation does not make sense) + */ + Filter enableFilter(String filterName) throws IllegalStateException; + + + //------------------------------------------------------------------------- + // Convenience methods for storing individual objects + //------------------------------------------------------------------------- + + /** + * Obtain the specified lock level upon the given object, implicitly + * checking whether the corresponding database entry still exists. + * @param entity the persistent instance to lock + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#lock(Object, org.hibernate.LockMode) + */ + void lock(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Obtain the specified lock level upon the given object, implicitly + * checking whether the corresponding database entry still exists. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to lock + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#lock(String, Object, org.hibernate.LockMode) + */ + void lock(String entityName, Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Persist the given transient instance. + * @param entity the transient instance to persist + * @return the generated identifier + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#save(Object) + */ + Serializable save(Object entity) throws DataAccessException; + + /** + * Persist the given transient instance. + * @param entityName the name of the persistent entity + * @param entity the transient instance to persist + * @return the generated identifier + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#save(String, Object) + */ + Serializable save(String entityName, Object entity) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + * @param entity the persistent instance to update + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(Object) + */ + void update(Object entity) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entity the persistent instance to update + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(Object) + */ + void update(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to update + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(String, Object) + */ + void update(String entityName, Object entity) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to update + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(String, Object) + */ + void update(String entityName, Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Save or update the given persistent instance, + * according to its id (matching the configured "unsaved-value"?). + * Associates the instance with the current Hibernate {@link org.hibernate.Session}. + * @param entity the persistent instance to save or update + * (to be associated with the Hibernate Session) + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#saveOrUpdate(Object) + */ + void saveOrUpdate(Object entity) throws DataAccessException; + + /** + * Save or update the given persistent instance, + * according to its id (matching the configured "unsaved-value"?). + * Associates the instance with the current Hibernate Session. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to save or update + * (to be associated with the Hibernate Session) + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#saveOrUpdate(String, Object) + */ + void saveOrUpdate(String entityName, Object entity) throws DataAccessException; + + /** + * Save or update all given persistent instances, + * according to its id (matching the configured "unsaved-value"?). + * Associates the instances with the current Hibernate Session. + * @param entities the persistent instances to save or update + * (to be associated with the Hibernate Session) + * @throws DataAccessException in case of Hibernate errors + * @deprecated as of Spring 2.5, in favor of individual + * saveOrUpdate or merge usage + */ + void saveOrUpdateAll(Collection entities) throws DataAccessException; + + /** + * Persist the state of the given detached instance according to the + * given replication mode, reusing the current identifier value. + * @param entity the persistent object to replicate + * @param replicationMode the Hibernate ReplicationMode + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#replicate(Object, org.hibernate.ReplicationMode) + */ + void replicate(Object entity, ReplicationMode replicationMode) throws DataAccessException; + + /** + * Persist the state of the given detached instance according to the + * given replication mode, reusing the current identifier value. + * @param entityName the name of the persistent entity + * @param entity the persistent object to replicate + * @param replicationMode the Hibernate ReplicationMode + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#replicate(String, Object, org.hibernate.ReplicationMode) + */ + void replicate(String entityName, Object entity, ReplicationMode replicationMode) throws DataAccessException; + + /** + * Persist the given transient instance. Follows JSR-220 semantics. + *

Similar to save, associating the given object + * with the current Hibernate {@link org.hibernate.Session}. + * @param entity the persistent instance to persist + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#persist(Object) + * @see #save + */ + void persist(Object entity) throws DataAccessException; + + /** + * Persist the given transient instance. Follows JSR-220 semantics. + *

Similar to save, associating the given object + * with the current Hibernate {@link org.hibernate.Session}. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to persist + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#persist(String, Object) + * @see #save + */ + void persist(String entityName, Object entity) throws DataAccessException; + + /** + * Copy the state of the given object onto the persistent object + * with the same identifier. Follows JSR-220 semantics. + *

Similar to saveOrUpdate, but never associates the given + * object with the current Hibernate Session. In case of a new entity, + * the state will be copied over as well. + *

Note that merge will not update the identifiers + * in the passed-in object graph (in contrast to TopLink)! Consider + * registering Spring's IdTransferringMergeEventListener if + * you would like to have newly assigned ids transferred to the original + * object graph too. + * @param entity the object to merge with the corresponding persistence instance + * @return the updated, registered persistent instance + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#merge(Object) + * @see #saveOrUpdate + * @see org.springframework.orm.hibernate3.support.IdTransferringMergeEventListener + */ + Object merge(Object entity) throws DataAccessException; + + /** + * Copy the state of the given object onto the persistent object + * with the same identifier. Follows JSR-220 semantics. + *

Similar to saveOrUpdate, but never associates the given + * object with the current Hibernate {@link org.hibernate.Session}. In + * the case of a new entity, the state will be copied over as well. + *

Note that merge will not update the identifiers + * in the passed-in object graph (in contrast to TopLink)! Consider + * registering Spring's IdTransferringMergeEventListener + * if you would like to have newly assigned ids transferred to the + * original object graph too. + * @param entityName the name of the persistent entity + * @param entity the object to merge with the corresponding persistence instance + * @return the updated, registered persistent instance + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#merge(String, Object) + * @see #saveOrUpdate + */ + Object merge(String entityName, Object entity) throws DataAccessException; + + /** + * Delete the given persistent instance. + * @param entity the persistent instance to delete + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(Object entity) throws DataAccessException; + + /** + * Delete the given persistent instance. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entity the persistent instance to delete + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Delete the given persistent instance. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to delete + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(String entityName, Object entity) throws DataAccessException; + + /** + * Delete the given persistent instance. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to delete + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(String entityName, Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Delete all given persistent instances. + *

This can be combined with any of the find methods to delete by query + * in two lines of code. + * @param entities the persistent instances to delete + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void deleteAll(Collection entities) throws DataAccessException; + + /** + * Flush all pending saves, updates and deletes to the database. + *

Only invoke this for selective eager flushing, for example when + * JDBC code needs to see certain changes within the same transaction. + * Else, it is preferable to rely on auto-flushing at transaction + * completion. + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#flush + */ + void flush() throws DataAccessException; + + /** + * Remove all objects from the {@link org.hibernate.Session} cache, and + * cancel all pending saves, updates and deletes. + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#clear + */ + void clear() throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods for HQL strings + //------------------------------------------------------------------------- + + /** + * Execute an HQL query. + * @param queryString a query expressed in Hibernate's query language + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + */ + List find(String queryString) throws DataAccessException; + + /** + * Execute an HQL query, binding one value to a "?" parameter in the + * query string. + * @param queryString a query expressed in Hibernate's query language + * @param value the value of the parameter + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + */ + List find(String queryString, Object value) throws DataAccessException; + + /** + * Execute an HQL query, binding a number of values to "?" parameters + * in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + */ + List find(String queryString, Object[] values) throws DataAccessException; + + /** + * Execute an HQL query, binding one value to a ":" named parameter + * in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param paramName the name of the parameter + * @param value the value of the parameter + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedParam(String queryString, String paramName, Object value) + throws DataAccessException; + + /** + * Execute an HQL query, binding a number of values to ":" named + * parameters in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param paramNames the names of the parameters + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedParam(String queryString, String[] paramNames, Object[] values) + throws DataAccessException; + + /** + * Execute an HQL query, binding the properties of the given bean to + * named parameters in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param valueBean the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Query#setProperties + * @see org.hibernate.Session#createQuery + */ + List findByValueBean(String queryString, Object valueBean) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods for named queries + //------------------------------------------------------------------------- + + /** + * Execute a named query. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQuery(String queryName) throws DataAccessException; + + /** + * Execute a named query, binding one value to a "?" parameter in + * the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param value the value of the parameter + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQuery(String queryName, Object value) throws DataAccessException; + + /** + * Execute a named query binding a number of values to "?" parameters + * in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQuery(String queryName, Object[] values) throws DataAccessException; + + /** + * Execute a named query, binding one value to a ":" named parameter + * in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param paramName the name of parameter + * @param value the value of the parameter + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) + throws DataAccessException; + + /** + * Execute a named query, binding a number of values to ":" named + * parameters in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param paramNames the names of the parameters + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQueryAndNamedParam(String queryName, String[] paramNames, Object[] values) + throws DataAccessException; + + /** + * Execute a named query, binding the properties of the given bean to + * ":" named parameters in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param valueBean the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Query#setProperties + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQueryAndValueBean(String queryName, Object valueBean) + throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods for detached criteria + //------------------------------------------------------------------------- + + /** + * Execute a query based on a given Hibernate criteria object. + * @param criteria the detached Hibernate criteria object. + * Note: Do not reuse criteria objects! They need to recreated per execution, + * due to the suboptimal design of Hibernate's criteria facility. + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.DetachedCriteria#getExecutableCriteria(org.hibernate.Session) + */ + List findByCriteria(DetachedCriteria criteria) throws DataAccessException; + + /** + * Execute a query based on the given Hibernate criteria object. + * @param criteria the detached Hibernate criteria object. + * Note: Do not reuse criteria objects! They need to recreated per execution, + * due to the suboptimal design of Hibernate's criteria facility. + * @param firstResult the index of the first result object to be retrieved + * (numbered from 0) + * @param maxResults the maximum number of result objects to retrieve + * (or <=0 for no limit) + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.DetachedCriteria#getExecutableCriteria(org.hibernate.Session) + * @see org.hibernate.Criteria#setFirstResult(int) + * @see org.hibernate.Criteria#setMaxResults(int) + */ + List findByCriteria(DetachedCriteria criteria, int firstResult, int maxResults) throws DataAccessException; + + /** + * Execute a query based on the given example entity object. + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + */ + List findByExample(Object exampleEntity) throws DataAccessException; + + /** + * Execute a query based on the given example entity object. + * @param entityName the name of the persistent entity + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + */ + List findByExample(String entityName, Object exampleEntity) throws DataAccessException; + + /** + * Execute a query based on a given example entity object. + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @param firstResult the index of the first result object to be retrieved + * (numbered from 0) + * @param maxResults the maximum number of result objects to retrieve + * (or <=0 for no limit) + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + * @see org.hibernate.Criteria#setFirstResult(int) + * @see org.hibernate.Criteria#setMaxResults(int) + */ + List findByExample(Object exampleEntity, int firstResult, int maxResults) throws DataAccessException; + + /** + * Execute a query based on a given example entity object. + * @param entityName the name of the persistent entity + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @param firstResult the index of the first result object to be retrieved + * (numbered from 0) + * @param maxResults the maximum number of result objects to retrieve + * (or <=0 for no limit) + * @return a {@link List} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + * @see org.hibernate.Criteria#setFirstResult(int) + * @see org.hibernate.Criteria#setMaxResults(int) + */ + List findByExample(String entityName, Object exampleEntity, int firstResult, int maxResults) + throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience query methods for iteration and bulk updates/deletes + //------------------------------------------------------------------------- + + /** + * Execute a query for persistent instances. + *

Returns the results as an {@link Iterator}. Entities returned are + * initialized on demand. See the Hibernate API documentation for details. + * @param queryString a query expressed in Hibernate's query language + * @return an {@link Iterator} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#iterate + */ + Iterator iterate(String queryString) throws DataAccessException; + + /** + * Execute a query for persistent instances, binding one value + * to a "?" parameter in the query string. + *

Returns the results as an {@link Iterator}. Entities returned are + * initialized on demand. See the Hibernate API documentation for details. + * @param queryString a query expressed in Hibernate's query language + * @param value the value of the parameter + * @return an {@link Iterator} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#iterate + */ + Iterator iterate(String queryString, Object value) throws DataAccessException; + + /** + * Execute a query for persistent instances, binding a number of + * values to "?" parameters in the query string. + *

Returns the results as an {@link Iterator}. Entities returned are + * initialized on demand. See the Hibernate API documentation for details. + * @param queryString a query expressed in Hibernate's query language + * @param values the values of the parameters + * @return an {@link Iterator} containing 0 or more persistent instances + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#iterate + */ + Iterator iterate(String queryString, Object[] values) throws DataAccessException; + + /** + * Immediately close an {@link Iterator} created by any of the various + * iterate(..) operations, instead of waiting until the + * session is closed or disconnected. + * @param it the Iterator to close + * @throws DataAccessException if the Iterator could not be closed + * @see org.hibernate.Hibernate#close + */ + void closeIterator(Iterator it) throws DataAccessException; + + /** + * Update/delete all objects according to the given query. + * @param queryString an update/delete query expressed in Hibernate's query language + * @return the number of instances updated/deleted + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#executeUpdate + */ + int bulkUpdate(String queryString) throws DataAccessException; + + /** + * Update/delete all objects according to the given query, binding one value + * to a "?" parameter in the query string. + * @param queryString an update/delete query expressed in Hibernate's query language + * @param value the value of the parameter + * @return the number of instances updated/deleted + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#executeUpdate + */ + int bulkUpdate(String queryString, Object value) throws DataAccessException; + + /** + * Update/delete all objects according to the given query, binding a number of + * values to "?" parameters in the query string. + * @param queryString an update/delete query expressed in Hibernate's query language + * @param values the values of the parameters + * @return the number of instances updated/deleted + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#executeUpdate + */ + int bulkUpdate(String queryString, Object[] values) throws DataAccessException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateOptimisticLockingFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateOptimisticLockingFailureException.java new file mode 100644 index 00000000000..fb58c8efcc8 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateOptimisticLockingFailureException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 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.hibernate3; + +import org.hibernate.StaleObjectStateException; +import org.hibernate.StaleStateException; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +/** + * Hibernate-specific subclass of ObjectOptimisticLockingFailureException. + * Converts Hibernate's StaleObjectStateException and StaleStateException. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +public class HibernateOptimisticLockingFailureException extends ObjectOptimisticLockingFailureException { + + public HibernateOptimisticLockingFailureException(StaleObjectStateException ex) { + super(ex.getEntityName(), ex.getIdentifier(), ex); + } + + public HibernateOptimisticLockingFailureException(StaleStateException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateQueryException.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateQueryException.java new file mode 100644 index 00000000000..743600991b6 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateQueryException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2005 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.hibernate3; + +import org.hibernate.QueryException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Hibernate-specific subclass of InvalidDataAccessResourceUsageException, + * thrown on invalid HQL query syntax. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +public class HibernateQueryException extends InvalidDataAccessResourceUsageException { + + public HibernateQueryException(QueryException ex) { + super(ex.getMessage(), ex); + } + + /** + * Return the HQL query string that was invalid. + */ + public String getQueryString() { + return ((QueryException) getCause()).getQueryString(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateSystemException.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateSystemException.java new file mode 100644 index 00000000000..83fdb0f3d51 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateSystemException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2005 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.hibernate3; + +import org.hibernate.HibernateException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Hibernate-specific subclass of UncategorizedDataAccessException, + * for Hibernate system errors that do not match any concrete + * org.springframework.dao exceptions. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +public class HibernateSystemException extends UncategorizedDataAccessException { + + /** + * Create a new HibernateSystemException, + * wrapping an arbitrary HibernateException. + * @param cause the HibernateException thrown + */ + public HibernateSystemException(HibernateException cause) { + super(cause != null ? cause.getMessage() : null, cause); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateTemplate.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateTemplate.java new file mode 100644 index 00000000000..86515061e17 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateTemplate.java @@ -0,0 +1,1312 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.hibernate.Criteria; +import org.hibernate.Filter; +import org.hibernate.FlushMode; +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.Query; +import org.hibernate.ReplicationMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.DetachedCriteria; +import org.hibernate.criterion.Example; +import org.hibernate.engine.SessionImplementor; +import org.hibernate.event.EventSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.util.Assert; + +/** + * Helper class that simplifies Hibernate data access code. Automatically + * converts HibernateExceptions into DataAccessExceptions, following the + * org.springframework.dao exception hierarchy. + * + *

The central method is execute, supporting Hibernate access code + * implementing the {@link HibernateCallback} interface. It provides Hibernate Session + * handling such that neither the HibernateCallback implementation nor the calling + * code needs to explicitly care about retrieving/closing Hibernate Sessions, + * or handling Session lifecycle exceptions. For typical single step actions, + * there are various convenience methods (find, load, saveOrUpdate, delete). + * + *

Can be used within a service implementation via direct instantiation + * with a SessionFactory reference, or get prepared in an application context + * and given to services as bean reference. Note: The SessionFactory 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. + * + *

NOTE: As of Hibernate 3.0.1, transactional Hibernate access code can + * also be coded in plain Hibernate style. Hence, for newly started projects, + * consider adopting the standard Hibernate3 style of coding data access objects + * instead, based on {@link org.hibernate.SessionFactory#getCurrentSession()}. + * + *

This class can be considered as direct alternative to working with the raw + * Hibernate3 Session API (through SessionFactory.getCurrentSession()). + * The major advantage is its automatic conversion to DataAccessExceptions as well + * as its capability to fall back to 'auto-commit' style behavior when used outside + * of transactions. Note that HibernateTemplate will perform its own Session + * management, not participating in a custom Hibernate CurrentSessionContext + * unless you explicitly switch {@link #setAllowCreate "allowCreate"} to "false". + * + *

{@link LocalSessionFactoryBean} is the preferred way of obtaining a reference + * to a specific Hibernate SessionFactory, at least in a non-EJB environment. + * The Spring application context will manage its lifecycle, initializing and + * shutting down the factory as part of the application. + * + *

Note that operations that return an Iterator (i.e. iterate) + * are supposed to be used within Spring-driven or JTA-driven transactions + * (with HibernateTransactionManager, JtaTransactionManager, or EJB CMT). + * Else, the Iterator won't be able to read results from its ResultSet anymore, + * as the underlying Hibernate Session will already have been closed. + * + *

Lazy loading will also just work with an open Hibernate Session, + * either within a transaction or within OpenSessionInViewFilter/Interceptor. + * Furthermore, some operations just make sense within transactions, + * for example: contains, evict, lock, + * flush, clear. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setSessionFactory + * @see HibernateCallback + * @see org.hibernate.Session + * @see LocalSessionFactoryBean + * @see HibernateTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + */ +public class HibernateTemplate extends HibernateAccessor implements HibernateOperations { + + private boolean allowCreate = true; + + private boolean alwaysUseNewSession = false; + + private boolean exposeNativeSession = false; + + private boolean checkWriteOperations = true; + + private boolean cacheQueries = false; + + private String queryCacheRegion; + + private int fetchSize = 0; + + private int maxResults = 0; + + + /** + * Create a new HibernateTemplate instance. + */ + public HibernateTemplate() { + } + + /** + * Create a new HibernateTemplate instance. + * @param sessionFactory SessionFactory to create Sessions + */ + public HibernateTemplate(SessionFactory sessionFactory) { + setSessionFactory(sessionFactory); + afterPropertiesSet(); + } + + /** + * Create a new HibernateTemplate instance. + * @param sessionFactory SessionFactory to create Sessions + * @param allowCreate if a non-transactional Session should be created when no + * transactional Session can be found for the current thread + */ + public HibernateTemplate(SessionFactory sessionFactory, boolean allowCreate) { + setSessionFactory(sessionFactory); + setAllowCreate(allowCreate); + afterPropertiesSet(); + } + + + /** + * Set if a new {@link Session} should be created when no transactional + * Session can be found for the current thread. + * The default value is true. + *

HibernateTemplate is aware of a corresponding + * Session bound to the current thread, for example when using + * {@link HibernateTransactionManager}. If allowCreate is + * true, a new non-transactional Session will be + * created if none is found, which needs to be closed at the end of the operation. + * If false, an {@link IllegalStateException} will get thrown in + * this case. + *

NOTE: As of Spring 2.5, switching allowCreate + * to false will delegate to Hibernate's + * {@link org.hibernate.SessionFactory#getCurrentSession()} method, + * which - with Spring-based setup - will by default delegate to Spring's + * SessionFactoryUtils.getSession(sessionFactory, false). + * This mode also allows for custom Hibernate CurrentSessionContext strategies + * to be plugged in, whereas allowCreate set to true + * will always use a Spring-managed Hibernate Session. + * @see SessionFactoryUtils#getSession(SessionFactory, boolean) + */ + public void setAllowCreate(boolean allowCreate) { + this.allowCreate = allowCreate; + } + + /** + * Return if a new Session should be created if no thread-bound found. + */ + public boolean isAllowCreate() { + return this.allowCreate; + } + + /** + * Set whether to always use a new Hibernate Session for this template. + * Default is "false"; if activated, all operations on this template will + * work on a new Hibernate Session even in case of a pre-bound Session + * (for example, within a transaction or OpenSessionInViewFilter). + *

Within a transaction, a new Hibernate Session used by this template + * will participate in the transaction through using the same JDBC + * Connection. In such a scenario, multiple Sessions will participate + * in the same database transaction. + *

Turn this on for operations that are supposed to always execute + * independently, without side effects caused by a shared Hibernate Session. + */ + public void setAlwaysUseNewSession(boolean alwaysUseNewSession) { + this.alwaysUseNewSession = alwaysUseNewSession; + } + + /** + * Return whether to always use a new Hibernate Session for this template. + */ + public boolean isAlwaysUseNewSession() { + return this.alwaysUseNewSession; + } + + /** + * Set whether to expose the native Hibernate Session to + * HibernateCallback code. + *

Default is "false": a Session proxy will be returned, suppressing + * close calls and automatically applying query cache + * settings and transaction timeouts. + * @see HibernateCallback + * @see org.hibernate.Session + * @see #setCacheQueries + * @see #setQueryCacheRegion + * @see #prepareQuery + * @see #prepareCriteria + */ + public void setExposeNativeSession(boolean exposeNativeSession) { + this.exposeNativeSession = exposeNativeSession; + } + + /** + * Return whether to expose the native Hibernate Session to + * HibernateCallback code, or rather a Session proxy. + */ + public boolean isExposeNativeSession() { + return this.exposeNativeSession; + } + + /** + * Set whether to check that the Hibernate Session is not in read-only mode + * in case of write operations (save/update/delete). + *

Default is "true", for fail-fast behavior when attempting write operations + * within a read-only transaction. Turn this off to allow save/update/delete + * on a Session with flush mode NEVER. + * @see #setFlushMode + * @see #checkWriteOperationAllowed + * @see org.springframework.transaction.TransactionDefinition#isReadOnly + */ + public void setCheckWriteOperations(boolean checkWriteOperations) { + this.checkWriteOperations = checkWriteOperations; + } + + /** + * Return whether to check that the Hibernate Session is not in read-only + * mode in case of write operations (save/update/delete). + */ + public boolean isCheckWriteOperations() { + return this.checkWriteOperations; + } + + /** + * Set whether to cache all queries executed by this template. + *

If this is "true", all Query and Criteria objects created by + * this template will be marked as cacheable (including all + * queries through find methods). + *

To specify the query region to be used for queries cached + * by this template, set the "queryCacheRegion" property. + * @see #setQueryCacheRegion + * @see org.hibernate.Query#setCacheable + * @see org.hibernate.Criteria#setCacheable + */ + public void setCacheQueries(boolean cacheQueries) { + this.cacheQueries = cacheQueries; + } + + /** + * Return whether to cache all queries executed by this template. + */ + public boolean isCacheQueries() { + return this.cacheQueries; + } + + /** + * Set the name of the cache region for queries executed by this template. + *

If this is specified, it will be applied to all Query and Criteria objects + * created by this template (including all queries through find methods). + *

The cache region will not take effect unless queries created by this + * template are configured to be cached via the "cacheQueries" property. + * @see #setCacheQueries + * @see org.hibernate.Query#setCacheRegion + * @see org.hibernate.Criteria#setCacheRegion + */ + public void setQueryCacheRegion(String queryCacheRegion) { + this.queryCacheRegion = queryCacheRegion; + } + + /** + * Return the name of the cache region for queries executed by this template. + */ + public String getQueryCacheRegion() { + return this.queryCacheRegion; + } + + /** + * Set the fetch size for this HibernateTemplate. This is important for processing + * large result sets: Setting this higher than the default value will increase + * processing speed at the cost of memory consumption; setting this lower can + * avoid transferring row data that will never be read by the application. + *

Default is 0, indicating to use the JDBC driver's default. + */ + public void setFetchSize(int fetchSize) { + this.fetchSize = fetchSize; + } + + /** + * Return the fetch size specified for this HibernateTemplate. + */ + public int getFetchSize() { + return this.fetchSize; + } + + /** + * Set the maximum number of rows for this HibernateTemplate. This is important + * for processing subsets of large result sets, avoiding to read and hold + * the entire result set in the database or in the JDBC driver if we're + * never interested in the entire result in the first place (for example, + * when performing searches that might return a large number of matches). + *

Default is 0, indicating to use the JDBC driver's default. + */ + public void setMaxResults(int maxResults) { + this.maxResults = maxResults; + } + + /** + * Return the maximum number of rows specified for this HibernateTemplate. + */ + public int getMaxResults() { + return this.maxResults; + } + + + public Object execute(HibernateCallback action) throws DataAccessException { + return doExecute(action, false, false); + } + + public List executeFind(HibernateCallback action) throws DataAccessException { + Object result = doExecute(action, false, false); + if (result != null && !(result instanceof List)) { + throw new InvalidDataAccessApiUsageException( + "Result object returned from HibernateCallback isn't a List: [" + result + "]"); + } + return (List) result; + } + + /** + * Execute the action specified by the given action object within a + * new {@link org.hibernate.Session}. + *

This execute variant overrides the template-wide + * {@link #isAlwaysUseNewSession() "alwaysUseNewSession"} setting. + * @param action callback object that specifies the Hibernate action + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + */ + public Object executeWithNewSession(HibernateCallback action) { + return doExecute(action, true, false); + } + + /** + * Execute the action specified by the given action object within a + * native {@link org.hibernate.Session}. + *

This execute variant overrides the template-wide + * {@link #isExposeNativeSession() "exposeNativeSession"} setting. + * @param action callback object that specifies the Hibernate action + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + */ + public Object executeWithNativeSession(HibernateCallback action) { + return doExecute(action, false, true); + } + + /** + * Execute the action specified by the given action object within a Session. + * @param action callback object that specifies the Hibernate action + * @param enforceNativeSession whether to enforce exposure of the native + * Hibernate Session to callback code + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + * @deprecated as of Spring 2.5, in favor of {@link #executeWithNativeSession} + */ + public Object execute(HibernateCallback action, boolean enforceNativeSession) throws DataAccessException { + return doExecute(action, false, enforceNativeSession); + } + + /** + * Execute the action specified by the given action object within a Session. + * @param action callback object that specifies the Hibernate action + * @param enforceNewSession whether to enforce a new Session for this template + * even if there is a pre-bound transactional Session + * @param enforceNativeSession whether to enforce exposure of the native + * Hibernate Session to callback code + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of Hibernate errors + */ + protected Object doExecute(HibernateCallback action, boolean enforceNewSession, boolean enforceNativeSession) + throws DataAccessException { + + Assert.notNull(action, "Callback object must not be null"); + + Session session = (enforceNewSession ? + SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()) : getSession()); + boolean existingTransaction = (!enforceNewSession && + (!isAllowCreate() || SessionFactoryUtils.isSessionTransactional(session, getSessionFactory()))); + if (existingTransaction) { + logger.debug("Found thread-bound Session for HibernateTemplate"); + } + + FlushMode previousFlushMode = null; + try { + previousFlushMode = applyFlushMode(session, existingTransaction); + enableFilters(session); + Session sessionToExpose = + (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session)); + Object result = action.doInHibernate(sessionToExpose); + flushIfNecessary(session, existingTransaction); + return result; + } + catch (HibernateException ex) { + throw convertHibernateAccessException(ex); + } + catch (SQLException ex) { + throw convertJdbcAccessException(ex); + } + catch (RuntimeException ex) { + // Callback code threw application exception... + throw ex; + } + finally { + if (existingTransaction) { + logger.debug("Not closing pre-bound Hibernate Session after HibernateTemplate"); + disableFilters(session); + if (previousFlushMode != null) { + session.setFlushMode(previousFlushMode); + } + } + else { + // Never use deferred close for an explicitly new Session. + if (isAlwaysUseNewSession()) { + SessionFactoryUtils.closeSession(session); + } + else { + SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory()); + } + } + } + } + + /** + * Return a Session for use by this template. + *

Returns a new Session in case of "alwaysUseNewSession" (using the same + * JDBC Connection as a transactional Session, if applicable), a pre-bound + * Session in case of "allowCreate" turned off, and a pre-bound or new Session + * otherwise (new only if no transactional or otherwise pre-bound Session exists). + * @return the Session to use (never null) + * @see SessionFactoryUtils#getSession + * @see SessionFactoryUtils#getNewSession + * @see #setAlwaysUseNewSession + * @see #setAllowCreate + */ + protected Session getSession() { + if (isAlwaysUseNewSession()) { + return SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()); + } + else if (isAllowCreate()) { + return SessionFactoryUtils.getSession( + getSessionFactory(), getEntityInterceptor(), getJdbcExceptionTranslator()); + } + else if (SessionFactoryUtils.hasTransactionalSession(getSessionFactory())) { + return SessionFactoryUtils.getSession(getSessionFactory(), false); + } + else { + try { + return getSessionFactory().getCurrentSession(); + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not obtain current Hibernate Session", ex); + } + } + } + + /** + * Create a close-suppressing proxy for the given Hibernate Session. + * The proxy also prepares returned Query and Criteria objects. + * @param session the Hibernate Session to create a proxy for + * @return the Session proxy + * @see org.hibernate.Session#close() + * @see #prepareQuery + * @see #prepareCriteria + */ + protected Session createSessionProxy(Session session) { + Class[] sessionIfcs = null; + Class mainIfc = (session instanceof org.hibernate.classic.Session ? + org.hibernate.classic.Session.class : Session.class); + if (session instanceof EventSource) { + sessionIfcs = new Class[] {mainIfc, EventSource.class}; + } + else if (session instanceof SessionImplementor) { + sessionIfcs = new Class[] {mainIfc, SessionImplementor.class}; + } + else { + sessionIfcs = new Class[] {mainIfc}; + } + return (Session) Proxy.newProxyInstance( + session.getClass().getClassLoader(), sessionIfcs, + new CloseSuppressingInvocationHandler(session)); + } + + + //------------------------------------------------------------------------- + // Convenience methods for loading individual objects + //------------------------------------------------------------------------- + + public Object get(Class entityClass, Serializable id) throws DataAccessException { + return get(entityClass, id, null); + } + + public Object get(final Class entityClass, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return session.get(entityClass, id, lockMode); + } + else { + return session.get(entityClass, id); + } + } + }); + } + + public Object get(String entityName, Serializable id) throws DataAccessException { + return get(entityName, id, null); + } + + public Object get(final String entityName, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return session.get(entityName, id, lockMode); + } + else { + return session.get(entityName, id); + } + } + }); + } + + public Object load(Class entityClass, Serializable id) throws DataAccessException { + return load(entityClass, id, null); + } + + public Object load(final Class entityClass, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return session.load(entityClass, id, lockMode); + } + else { + return session.load(entityClass, id); + } + } + }); + } + + public Object load(String entityName, Serializable id) throws DataAccessException { + return load(entityName, id, null); + } + + public Object load(final String entityName, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return session.load(entityName, id, lockMode); + } + else { + return session.load(entityName, id); + } + } + }); + } + + public List loadAll(final Class entityClass) throws DataAccessException { + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Criteria criteria = session.createCriteria(entityClass); + criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); + prepareCriteria(criteria); + return criteria.list(); + } + }); + } + + public void load(final Object entity, final Serializable id) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + session.load(entity, id); + return null; + } + }); + } + + public void refresh(final Object entity) throws DataAccessException { + refresh(entity, null); + } + + public void refresh(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + session.refresh(entity, lockMode); + } + else { + session.refresh(entity); + } + return null; + } + }); + } + + public boolean contains(final Object entity) throws DataAccessException { + Boolean result = (Boolean) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) { + return (session.contains(entity) ? Boolean.TRUE : Boolean.FALSE); + } + }); + return result.booleanValue(); + } + + public void evict(final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + session.evict(entity); + return null; + } + }); + } + + public void initialize(Object proxy) throws DataAccessException { + try { + Hibernate.initialize(proxy); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + + public Filter enableFilter(String filterName) throws IllegalStateException { + Session session = SessionFactoryUtils.getSession(getSessionFactory(), false); + Filter filter = session.getEnabledFilter(filterName); + if (filter == null) { + filter = session.enableFilter(filterName); + } + return filter; + } + + + //------------------------------------------------------------------------- + // Convenience methods for storing individual objects + //------------------------------------------------------------------------- + + public void lock(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + session.lock(entity, lockMode); + return null; + } + }); + } + + public void lock(final String entityName, final Object entity, final LockMode lockMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + session.lock(entityName, entity, lockMode); + return null; + } + }); + } + + public Serializable save(final Object entity) throws DataAccessException { + return (Serializable) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return session.save(entity); + } + }); + } + + public Serializable save(final String entityName, final Object entity) throws DataAccessException { + return (Serializable) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return session.save(entityName, entity); + } + }); + } + + public void update(Object entity) throws DataAccessException { + update(entity, null); + } + + public void update(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.update(entity); + if (lockMode != null) { + session.lock(entity, lockMode); + } + return null; + } + }); + } + + public void update(String entityName, Object entity) throws DataAccessException { + update(entityName, entity, null); + } + + public void update(final String entityName, final Object entity, final LockMode lockMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.update(entityName, entity); + if (lockMode != null) { + session.lock(entity, lockMode); + } + return null; + } + }); + } + + public void saveOrUpdate(final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.saveOrUpdate(entity); + return null; + } + }); + } + + public void saveOrUpdate(final String entityName, final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.saveOrUpdate(entityName, entity); + return null; + } + }); + } + + public void saveOrUpdateAll(final Collection entities) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + for (Iterator it = entities.iterator(); it.hasNext();) { + session.saveOrUpdate(it.next()); + } + return null; + } + }); + } + + public void replicate(final Object entity, final ReplicationMode replicationMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.replicate(entity, replicationMode); + return null; + } + }); + } + + public void replicate(final String entityName, final Object entity, final ReplicationMode replicationMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.replicate(entityName, entity, replicationMode); + return null; + } + }); + } + + public void persist(final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.persist(entity); + return null; + } + }); + } + + public void persist(final String entityName, final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.persist(entityName, entity); + return null; + } + }); + } + + public Object merge(final Object entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return session.merge(entity); + } + }); + } + + public Object merge(final String entityName, final Object entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return session.merge(entityName, entity); + } + }); + } + + public void delete(Object entity) throws DataAccessException { + delete(entity, null); + } + + public void delete(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + if (lockMode != null) { + session.lock(entity, lockMode); + } + session.delete(entity); + return null; + } + }); + } + + public void delete(String entityName, Object entity) throws DataAccessException { + delete(entityName, entity, null); + } + + public void delete(final String entityName, final Object entity, final LockMode lockMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + if (lockMode != null) { + session.lock(entityName, entity, lockMode); + } + session.delete(entityName, entity); + return null; + } + }); + } + + public void deleteAll(final Collection entities) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + for (Iterator it = entities.iterator(); it.hasNext();) { + session.delete(it.next()); + } + return null; + } + }); + } + + public void flush() throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + session.flush(); + return null; + } + }); + } + + public void clear() throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) { + session.clear(); + return null; + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods for HQL strings + //------------------------------------------------------------------------- + + public List find(String queryString) throws DataAccessException { + return find(queryString, (Object[]) null); + } + + public List find(String queryString, Object value) throws DataAccessException { + return find(queryString, new Object[] {value}); + } + + public List find(final String queryString, final Object[] values) throws DataAccessException { + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.list(); + } + }); + } + + public List findByNamedParam(String queryString, String paramName, Object value) + throws DataAccessException { + + return findByNamedParam(queryString, new String[] {paramName}, new Object[] {value}); + } + + public List findByNamedParam(final String queryString, final String[] paramNames, final Object[] values) + throws DataAccessException { + + if (paramNames.length != values.length) { + throw new IllegalArgumentException("Length of paramNames array must match length of values array"); + } + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + applyNamedParameterToQuery(queryObject, paramNames[i], values[i]); + } + } + return queryObject.list(); + } + }); + } + + public List findByValueBean(final String queryString, final Object valueBean) + throws DataAccessException { + + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + queryObject.setProperties(valueBean); + return queryObject.list(); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods for named queries + //------------------------------------------------------------------------- + + public List findByNamedQuery(String queryName) throws DataAccessException { + return findByNamedQuery(queryName, (Object[]) null); + } + + public List findByNamedQuery(String queryName, Object value) throws DataAccessException { + return findByNamedQuery(queryName, new Object[] {value}); + } + + public List findByNamedQuery(final String queryName, final Object[] values) throws DataAccessException { + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.getNamedQuery(queryName); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.list(); + } + }); + } + + public List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) + throws DataAccessException { + + return findByNamedQueryAndNamedParam(queryName, new String[] {paramName}, new Object[] {value}); + } + + public List findByNamedQueryAndNamedParam( + final String queryName, final String[] paramNames, final Object[] values) + throws DataAccessException { + + if (paramNames != null && values != null && paramNames.length != values.length) { + throw new IllegalArgumentException("Length of paramNames array must match length of values array"); + } + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.getNamedQuery(queryName); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + applyNamedParameterToQuery(queryObject, paramNames[i], values[i]); + } + } + return queryObject.list(); + } + }); + } + + public List findByNamedQueryAndValueBean(final String queryName, final Object valueBean) + throws DataAccessException { + + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.getNamedQuery(queryName); + prepareQuery(queryObject); + queryObject.setProperties(valueBean); + return queryObject.list(); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods for detached criteria + //------------------------------------------------------------------------- + + public List findByCriteria(DetachedCriteria criteria) throws DataAccessException { + return findByCriteria(criteria, -1, -1); + } + + public List findByCriteria(final DetachedCriteria criteria, final int firstResult, final int maxResults) + throws DataAccessException { + + Assert.notNull(criteria, "DetachedCriteria must not be null"); + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Criteria executableCriteria = criteria.getExecutableCriteria(session); + prepareCriteria(executableCriteria); + if (firstResult >= 0) { + executableCriteria.setFirstResult(firstResult); + } + if (maxResults > 0) { + executableCriteria.setMaxResults(maxResults); + } + return executableCriteria.list(); + } + }); + } + + public List findByExample(Object exampleEntity) throws DataAccessException { + return findByExample(null, exampleEntity, -1, -1); + } + + public List findByExample(String entityName, Object exampleEntity) throws DataAccessException { + return findByExample(entityName, exampleEntity, -1, -1); + } + + public List findByExample(Object exampleEntity, int firstResult, int maxResults) throws DataAccessException { + return findByExample(null, exampleEntity, firstResult, maxResults); + } + + public List findByExample( + final String entityName, final Object exampleEntity, final int firstResult, final int maxResults) + throws DataAccessException { + + Assert.notNull(exampleEntity, "Example entity must not be null"); + return (List) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Criteria executableCriteria = (entityName != null ? + session.createCriteria(entityName) : session.createCriteria(exampleEntity.getClass())); + executableCriteria.add(Example.create(exampleEntity)); + prepareCriteria(executableCriteria); + if (firstResult >= 0) { + executableCriteria.setFirstResult(firstResult); + } + if (maxResults > 0) { + executableCriteria.setMaxResults(maxResults); + } + return executableCriteria.list(); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience query methods for iteration and bulk updates/deletes + //------------------------------------------------------------------------- + + public Iterator iterate(String queryString) throws DataAccessException { + return iterate(queryString, (Object[]) null); + } + + public Iterator iterate(String queryString, Object value) throws DataAccessException { + return iterate(queryString, new Object[] {value}); + } + + public Iterator iterate(final String queryString, final Object[] values) throws DataAccessException { + return (Iterator) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.iterate(); + } + }); + } + + public void closeIterator(Iterator it) throws DataAccessException { + try { + Hibernate.close(it); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + + public int bulkUpdate(String queryString) throws DataAccessException { + return bulkUpdate(queryString, (Object[]) null); + } + + public int bulkUpdate(String queryString, Object value) throws DataAccessException { + return bulkUpdate(queryString, new Object[] {value}); + } + + public int bulkUpdate(final String queryString, final Object[] values) throws DataAccessException { + Integer updateCount = (Integer) executeWithNativeSession(new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return new Integer(queryObject.executeUpdate()); + } + }); + return updateCount.intValue(); + } + + + //------------------------------------------------------------------------- + // Helper methods used by the operations above + //------------------------------------------------------------------------- + + /** + * Check whether write operations are allowed on the given Session. + *

Default implementation throws an InvalidDataAccessApiUsageException in + * case of FlushMode.NEVER/MANUAL. Can be overridden in subclasses. + * @param session current Hibernate Session + * @throws InvalidDataAccessApiUsageException if write operations are not allowed + * @see #setCheckWriteOperations + * @see #getFlushMode() + * @see #FLUSH_EAGER + * @see org.hibernate.Session#getFlushMode() + * @see org.hibernate.FlushMode#NEVER + * @see org.hibernate.FlushMode#MANUAL + */ + protected void checkWriteOperationAllowed(Session session) throws InvalidDataAccessApiUsageException { + if (isCheckWriteOperations() && getFlushMode() != FLUSH_EAGER && + session.getFlushMode().lessThan(FlushMode.COMMIT)) { + throw new InvalidDataAccessApiUsageException( + "Write operations are not allowed in read-only mode (FlushMode.NEVER/MANUAL): "+ + "Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition."); + } + } + + /** + * Prepare the given Query object, applying cache settings and/or + * a transaction timeout. + * @param queryObject the Query object to prepare + * @see #setCacheQueries + * @see #setQueryCacheRegion + * @see SessionFactoryUtils#applyTransactionTimeout + */ + protected void prepareQuery(Query queryObject) { + if (isCacheQueries()) { + queryObject.setCacheable(true); + if (getQueryCacheRegion() != null) { + queryObject.setCacheRegion(getQueryCacheRegion()); + } + } + if (getFetchSize() > 0) { + queryObject.setFetchSize(getFetchSize()); + } + if (getMaxResults() > 0) { + queryObject.setMaxResults(getMaxResults()); + } + SessionFactoryUtils.applyTransactionTimeout(queryObject, getSessionFactory()); + } + + /** + * Prepare the given Criteria object, applying cache settings and/or + * a transaction timeout. + * @param criteria the Criteria object to prepare + * @see #setCacheQueries + * @see #setQueryCacheRegion + * @see SessionFactoryUtils#applyTransactionTimeout + */ + protected void prepareCriteria(Criteria criteria) { + if (isCacheQueries()) { + criteria.setCacheable(true); + if (getQueryCacheRegion() != null) { + criteria.setCacheRegion(getQueryCacheRegion()); + } + } + if (getFetchSize() > 0) { + criteria.setFetchSize(getFetchSize()); + } + if (getMaxResults() > 0) { + criteria.setMaxResults(getMaxResults()); + } + SessionFactoryUtils.applyTransactionTimeout(criteria, getSessionFactory()); + } + + /** + * Apply the given name parameter to the given Query object. + * @param queryObject the Query object + * @param paramName the name of the parameter + * @param value the value of the parameter + * @throws HibernateException if thrown by the Query object + */ + protected void applyNamedParameterToQuery(Query queryObject, String paramName, Object value) + throws HibernateException { + + if (value instanceof Collection) { + queryObject.setParameterList(paramName, (Collection) value); + } + else if (value instanceof Object[]) { + queryObject.setParameterList(paramName, (Object[]) value); + } + else { + queryObject.setParameter(paramName, value); + } + } + + + /** + * Invocation handler that suppresses close calls on Hibernate Sessions. + * Also prepares returned Query and Criteria objects. + * @see org.hibernate.Session#close + */ + private class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Session target; + + public CloseSuppressingInvocationHandler(Session target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on Session interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Session proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return null; + } + + // Invoke method on target Session. + try { + Object retVal = method.invoke(this.target, args); + + // If return value is a Query or Criteria, apply transaction timeout. + // Applies to createQuery, getNamedQuery, createCriteria. + if (retVal instanceof Query) { + prepareQuery(((Query) retVal)); + } + if (retVal instanceof Criteria) { + prepareCriteria(((Criteria) retVal)); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateTransactionManager.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateTransactionManager.java new file mode 100644 index 00000000000..69dd16fe63e --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/HibernateTransactionManager.java @@ -0,0 +1,911 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.hibernate.ConnectionReleaseMode; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Interceptor; +import org.hibernate.JDBCException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.exception.GenericJDBCException; +import org.hibernate.impl.SessionImpl; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} + * implementation for a single Hibernate {@link org.hibernate.SessionFactory}. + * Binds a Hibernate Session from the specified factory to the thread, potentially + * allowing for one thread-bound Session per factory. {@link SessionFactoryUtils} + * and {@link HibernateTemplate} are aware of thread-bound Sessions and participate + * in such transactions automatically. Using either of those or going through + * SessionFactory.getCurrentSession() is required for Hibernate + * access code that needs to support this transaction handling mechanism. + * + *

Supports custom isolation levels, and timeouts that get applied as + * Hibernate transaction timeouts. + * + *

This transaction manager is appropriate for applications that use a single + * Hibernate SessionFactory for transactional data access, but it also supports + * direct DataSource access within a transaction (i.e. plain JDBC code working + * with the same DataSource). This allows for mixing services which access Hibernate + * and services which use plain JDBC (without being aware of Hibernate)! + * Application code needs to stick to the same simple Connection lookup pattern as + * with {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} + * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection} + * or going through a + * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}). + * + *

Note: To be able to register a DataSource's Connection for plain JDBC code, + * this instance needs to be aware of the DataSource ({@link #setDataSource}). + * The given DataSource should obviously match the one used by the given + * SessionFactory. To achieve this, configure both to the same JNDI DataSource, + * or preferably create the SessionFactory with {@link LocalSessionFactoryBean} and + * a local DataSource (which will be autodetected by this transaction manager). + * + *

JTA (usually through {@link org.springframework.transaction.jta.JtaTransactionManager}) + * is necessary for accessing multiple transactional resources within the same + * transaction. The DataSource that Hibernate uses needs to be JTA-enabled in + * such a scenario (see container setup). Normally, JTA setup for Hibernate is + * somewhat container-specific due to the JTA TransactionManager lookup, required + * for proper transactional handling of the SessionFactory-level read-write cache. + * + *

Fortunately, there is an easier way with Spring: {@link SessionFactoryUtils} + * (and thus {@link HibernateTemplate}) registers synchronizations with Spring's + * {@link org.springframework.transaction.support.TransactionSynchronizationManager} + * (as used by {@link org.springframework.transaction.jta.JtaTransactionManager}), + * for proper after-completion callbacks. Therefore, as long as Spring's + * JtaTransactionManager drives the JTA transactions, Hibernate does not require + * any special configuration for proper JTA participation. Note that there are + * special restrictions with EJB CMT and restrictive JTA subsystems: See + * {@link org.springframework.transaction.jta.JtaTransactionManager}'s javadoc for details. + * + *

On JDBC 3.0, this transaction manager supports nested transactions via JDBC 3.0 + * Savepoints. The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} + * flag defaults to "false", though, as nested transactions will just apply to the + * JDBC Connection, not to the Hibernate Session and its cached objects. You can + * manually set the flag to "true" if you want to use nested transactions for + * JDBC access code which participates in Hibernate transactions (provided that + * your JDBC driver supports Savepoints). Note that Hibernate itself does not + * support nested transactions! Hence, do not expect Hibernate access code to + * semantically participate in a nested transaction. + * + *

Requires Hibernate 3.1 or later, as of Spring 2.5. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setSessionFactory + * @see #setDataSource + * @see LocalSessionFactoryBean + * @see SessionFactoryUtils#getSession + * @see SessionFactoryUtils#applyTransactionTimeout + * @see SessionFactoryUtils#releaseSession + * @see HibernateTemplate + * @see org.hibernate.SessionFactory#getCurrentSession() + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class HibernateTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, BeanFactoryAware, InitializingBean { + + private SessionFactory sessionFactory; + + private DataSource dataSource; + + private boolean autodetectDataSource = true; + + private boolean prepareConnection = true; + + private boolean hibernateManagedSession = false; + + private boolean earlyFlushBeforeCommit = false; + + private Object entityInterceptor; + + private SQLExceptionTranslator jdbcExceptionTranslator; + + private SQLExceptionTranslator defaultJdbcExceptionTranslator; + + /** + * Just needed for entityInterceptorBeanName. + * @see #setEntityInterceptorBeanName + */ + private BeanFactory beanFactory; + + + /** + * Create a new HibernateTransactionManager instance. + * A SessionFactory has to be set to be able to use it. + * @see #setSessionFactory + */ + public HibernateTransactionManager() { + } + + /** + * Create a new HibernateTransactionManager instance. + * @param sessionFactory SessionFactory to manage transactions for + */ + public HibernateTransactionManager(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + afterPropertiesSet(); + } + + + /** + * Set the SessionFactory that this instance should manage transactions for. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the SessionFactory that this instance should manage transactions for. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + * The DataSource should match the one used by the Hibernate SessionFactory: + * for example, you could specify the same JNDI DataSource for both. + *

If the SessionFactory was configured with LocalDataSourceConnectionProvider, + * i.e. by Spring's LocalSessionFactoryBean with a specified "dataSource", + * the DataSource will be auto-detected: You can still explictly specify the + * DataSource, but you don't need to in this case. + *

A transactional JDBC Connection for this DataSource will be provided to + * application code accessing this DataSource directly via DataSourceUtils + * or JdbcTemplate. The Connection will be taken from the Hibernate Session. + *

The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + * @see #setAutodetectDataSource + * @see LocalDataSourceConnectionProvider + * @see LocalSessionFactoryBean#setDataSource + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.DataSourceUtils + * @see org.springframework.jdbc.core.JdbcTemplate + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set whether to autodetect a JDBC DataSource used by the Hibernate SessionFactory, + * if set via LocalSessionFactoryBean's setDataSource. Default is "true". + *

Can be turned off to deliberately ignore an available DataSource, in order + * to not expose Hibernate transactions as JDBC transactions for that DataSource. + * @see #setDataSource + * @see LocalSessionFactoryBean#setDataSource + */ + public void setAutodetectDataSource(boolean autodetectDataSource) { + this.autodetectDataSource = autodetectDataSource; + } + + /** + * Set whether to prepare the underlying JDBC Connection of a transactional + * Hibernate Session, that is, whether to apply a transaction-specific + * isolation level and/or the transaction's read-only flag to the underlying + * JDBC Connection. + *

Default is "true". If you turn this flag off, the transaction manager + * will not support per-transaction isolation levels anymore. It will not + * call Connection.setReadOnly(true) for read-only transactions + * anymore either. If this flag is turned off, no cleanup of a JDBC Connection + * is required after a transaction, since no Connection settings will get modified. + *

It is recommended to turn this flag off if running against Hibernate 3.1 + * and a connection pool that does not reset connection settings (for example, + * Jakarta Commons DBCP). To keep this flag turned on, you can set the + * "hibernate.connection.release_mode" property to "on_close" instead, + * or consider using a smarter connection pool (for example, C3P0). + * @see java.sql.Connection#setTransactionIsolation + * @see java.sql.Connection#setReadOnly + */ + public void setPrepareConnection(boolean prepareConnection) { + this.prepareConnection = prepareConnection; + } + + /** + * Set whether to operate on a Hibernate-managed Session instead of a + * Spring-managed Session, that is, whether to obtain the Session through + * Hibernate's {@link org.hibernate.SessionFactory#getCurrentSession()} + * instead of {@link org.hibernate.SessionFactory#openSession()} (with a Spring + * {@link org.springframework.transaction.support.TransactionSynchronizationManager} + * check preceding it). + *

Default is "false", i.e. using a Spring-managed Session: taking the current + * thread-bound Session if available (e.g. in an Open-Session-in-View scenario), + * creating a new Session for the current transaction otherwise. + *

Switch this flag to "true" in order to enforce use of a Hibernate-managed Session. + * Note that this requires {@link org.hibernate.SessionFactory#getCurrentSession()} + * to always return a proper Session when called for a Spring-managed transaction; + * transaction begin will fail if the getCurrentSession() call fails. + *

This mode will typically be used in combination with a custom Hibernate + * {@link org.hibernate.context.CurrentSessionContext} implementation that stores + * Sessions in a place other than Spring's TransactionSynchronizationManager. + * It may also be used in combination with Spring's Open-Session-in-View support + * (using Spring's default {@link SpringSessionContext}), in which case it subtly + * differs from the Spring-managed Session mode: The pre-bound Session will not + * receive a clear() call (on rollback) or a disconnect() + * call (on transaction completion) in such a scenario; this is rather left up + * to a custom CurrentSessionContext implementation (if desired). + */ + public void setHibernateManagedSession(boolean hibernateManagedSession) { + this.hibernateManagedSession = hibernateManagedSession; + } + + /** + * Set whether to perform an early flush before proceeding with a commit. + *

Default is "false", performing an implicit flush as part of the + * actual commit step. Switch this to "true" in order to enforce an + * explicit flush before the before-commit synchronization phase, making + * flushed state visible to beforeCommit callbacks of registered + * {@link org.springframework.transaction.support.TransactionSynchronization} + * objects. + *

Such explicit flush behavior is also consistent with Spring-driven + * flushing in a JTA transaction environment, so may also be enforced for + * consistency with JTA transaction behavior. + * @see #prepareForCommit + */ + public void setEarlyFlushBeforeCommit(boolean earlyFlushBeforeCommit) { + this.earlyFlushBeforeCommit = earlyFlushBeforeCommit; + } + + /** + * Set the bean name of a Hibernate entity interceptor that allows to inspect + * and change property values before writing to and reading from the database. + * Will get applied to any new Session created by this transaction manager. + *

Requires the bean factory to be known, to be able to resolve the bean + * name to an interceptor instance on session creation. Typically used for + * prototype interceptors, i.e. a new interceptor instance per session. + *

Can also be used for shared interceptor instances, but it is recommended + * to set the interceptor reference directly in such a scenario. + * @param entityInterceptorBeanName the name of the entity interceptor in + * the bean factory + * @see #setBeanFactory + * @see #setEntityInterceptor + */ + public void setEntityInterceptorBeanName(String entityInterceptorBeanName) { + this.entityInterceptor = entityInterceptorBeanName; + } + + /** + * Set a Hibernate entity interceptor that allows to inspect and change + * property values before writing to and reading from the database. + * Will get applied to any new Session created by this transaction manager. + *

Such an interceptor can either be set at the SessionFactory level, + * i.e. on LocalSessionFactoryBean, or at the Session level, i.e. on + * HibernateTemplate, HibernateInterceptor, and HibernateTransactionManager. + * It's preferable to set it on LocalSessionFactoryBean or HibernateTransactionManager + * to avoid repeated configuration and guarantee consistent behavior in transactions. + * @see LocalSessionFactoryBean#setEntityInterceptor + * @see HibernateTemplate#setEntityInterceptor + * @see HibernateInterceptor#setEntityInterceptor + */ + public void setEntityInterceptor(Interceptor entityInterceptor) { + this.entityInterceptor = entityInterceptor; + } + + /** + * Return the current Hibernate entity interceptor, or null if none. + * Resolves an entity interceptor bean name via the bean factory, + * if necessary. + * @throws IllegalStateException if bean name specified but no bean factory set + * @throws BeansException if bean name resolution via the bean factory failed + * @see #setEntityInterceptor + * @see #setEntityInterceptorBeanName + * @see #setBeanFactory + */ + public Interceptor getEntityInterceptor() throws IllegalStateException, BeansException { + if (this.entityInterceptor instanceof Interceptor) { + return (Interceptor) entityInterceptor; + } + else if (this.entityInterceptor instanceof String) { + if (this.beanFactory == null) { + throw new IllegalStateException("Cannot get entity interceptor via bean name if no bean factory set"); + } + String beanName = (String) this.entityInterceptor; + return (Interceptor) this.beanFactory.getBean(beanName, Interceptor.class); + } + else { + return null; + } + } + + /** + * Set the JDBC exception translator for this transaction manager. + *

Applied to any SQLException root cause of a Hibernate JDBCException that + * is thrown on flush, overriding Hibernate's default SQLException translation + * (which is based on Hibernate's Dialect for a specific target database). + * @param jdbcExceptionTranslator the exception translator + * @see java.sql.SQLException + * @see org.hibernate.JDBCException + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + /** + * Return the JDBC exception translator for this transaction manager, if any. + */ + public SQLExceptionTranslator getJdbcExceptionTranslator() { + return this.jdbcExceptionTranslator; + } + + /** + * The bean factory just needs to be known for resolving entity interceptor + * bean names. It does not need to be set for any other mode of operation. + * @see #setEntityInterceptorBeanName + */ + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public void afterPropertiesSet() { + if (getSessionFactory() == null) { + throw new IllegalArgumentException("Property 'sessionFactory' is required"); + } + if (this.entityInterceptor instanceof String && this.beanFactory == null) { + throw new IllegalArgumentException("Property 'beanFactory' is required for 'entityInterceptorBeanName'"); + } + + // Check for SessionFactory's DataSource. + if (this.autodetectDataSource && getDataSource() == null) { + DataSource sfds = SessionFactoryUtils.getDataSource(getSessionFactory()); + if (sfds != null) { + // Use the SessionFactory's DataSource for exposing transactions to JDBC code. + if (logger.isInfoEnabled()) { + logger.info("Using DataSource [" + sfds + + "] of Hibernate SessionFactory for HibernateTransactionManager"); + } + setDataSource(sfds); + } + } + } + + + public Object getResourceFactory() { + return getSessionFactory(); + } + + protected Object doGetTransaction() { + HibernateTransactionObject txObject = new HibernateTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory()); + if (sessionHolder != null) { + if (logger.isDebugEnabled()) { + logger.debug("Found thread-bound Session [" + + SessionFactoryUtils.toString(sessionHolder.getSession()) + "] for Hibernate transaction"); + } + txObject.setSessionHolder(sessionHolder); + } + else if (this.hibernateManagedSession) { + try { + Session session = getSessionFactory().getCurrentSession(); + if (logger.isDebugEnabled()) { + logger.debug("Found Hibernate-managed Session [" + + SessionFactoryUtils.toString(session) + "] for Spring-managed transaction"); + } + txObject.setExistingSession(session); + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException( + "Could not obtain Hibernate-managed Session for Spring-managed transaction", ex); + } + } + + if (getDataSource() != null) { + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.getResource(getDataSource()); + txObject.setConnectionHolder(conHolder); + } + + return txObject; + } + + protected boolean isExistingTransaction(Object transaction) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + return (txObject.hasSpringManagedTransaction() || + (this.hibernateManagedSession && txObject.hasHibernateManagedTransaction())); + } + + protected void doBegin(Object transaction, TransactionDefinition definition) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + + if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + throw new IllegalTransactionStateException( + "Pre-bound JDBC Connection found! HibernateTransactionManager does not support " + + "running within DataSourceTransactionManager if told to manage the DataSource itself. " + + "It is recommended to use a single HibernateTransactionManager for all transactions " + + "on a single DataSource, no matter whether Hibernate or JDBC access."); + } + + Session session = null; + + try { + if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) { + Interceptor entityInterceptor = getEntityInterceptor(); + Session newSession = (entityInterceptor != null ? + getSessionFactory().openSession(entityInterceptor) : getSessionFactory().openSession()); + if (logger.isDebugEnabled()) { + logger.debug("Opened new Session [" + SessionFactoryUtils.toString(newSession) + + "] for Hibernate transaction"); + } + txObject.setSession(newSession); + } + + session = txObject.getSessionHolder().getSession(); + + if (this.prepareConnection && isSameConnectionForEntireSession(session)) { + // We're allowed to change the transaction settings of the JDBC Connection. + if (logger.isDebugEnabled()) { + logger.debug( + "Preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]"); + } + Connection con = session.connection(); + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel); + } + else { + // Not allowed to change the transaction settings of the JDBC Connection. + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + // We should set a specific isolation level but are not allowed to... + throw new InvalidIsolationLevelException( + "HibernateTransactionManager is not allowed to support custom isolation levels: " + + "make sure that its 'prepareConnection' flag is on (the default) and that the " + + "Hibernate connection release mode is set to 'on_close' (SpringTransactionFactory's default). " + + "Make sure that your LocalSessionFactoryBean actually uses SpringTransactionFactory: Your " + + "Hibernate properties should *not* include a 'hibernate.transaction.factory_class' property!"); + } + if (logger.isDebugEnabled()) { + logger.debug( + "Not preparing JDBC Connection of Hibernate Session [" + SessionFactoryUtils.toString(session) + "]"); + } + } + + if (definition.isReadOnly() && txObject.isNewSession()) { + // Just set to NEVER in case of a new Session for this transaction. + session.setFlushMode(FlushMode.NEVER); + } + + if (!definition.isReadOnly() && !txObject.isNewSession()) { + // We need AUTO or COMMIT for a non-read-only transaction. + FlushMode flushMode = session.getFlushMode(); + if (flushMode.lessThan(FlushMode.COMMIT)) { + session.setFlushMode(FlushMode.AUTO); + txObject.getSessionHolder().setPreviousFlushMode(flushMode); + } + } + + Transaction hibTx = null; + + // Register transaction timeout. + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1 + // Applies to all statements, also to inserts, updates and deletes! + hibTx = session.getTransaction(); + hibTx.setTimeout(timeout); + hibTx.begin(); + } + else { + // Open a plain Hibernate transaction without specified timeout. + hibTx = session.beginTransaction(); + } + + // Add the Hibernate transaction to the session holder. + txObject.getSessionHolder().setTransaction(hibTx); + + // Register the Hibernate Session's JDBC Connection for the DataSource, if set. + if (getDataSource() != null) { + Connection con = session.connection(); + ConnectionHolder conHolder = new ConnectionHolder(con); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + conHolder.setTimeoutInSeconds(timeout); + } + if (logger.isDebugEnabled()) { + logger.debug("Exposing Hibernate transaction as JDBC transaction [" + con + "]"); + } + TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); + txObject.setConnectionHolder(conHolder); + } + + // Bind the session holder to the thread. + if (txObject.isNewSessionHolder()) { + TransactionSynchronizationManager.bindResource(getSessionFactory(), txObject.getSessionHolder()); + } + txObject.getSessionHolder().setSynchronizedWithTransaction(true); + } + + catch (Exception ex) { + if (txObject.isNewSession()) { + try { + if (session.getTransaction().isActive()) { + session.getTransaction().rollback(); + } + } + catch (Throwable ex2) { + logger.debug("Could not rollback Session after failed transaction begin", ex); + } + finally { + SessionFactoryUtils.closeSession(session); + } + } + throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex); + } + } + + protected Object doSuspend(Object transaction) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + txObject.setSessionHolder(null); + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory()); + txObject.setConnectionHolder(null); + ConnectionHolder connectionHolder = null; + if (getDataSource() != null) { + connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(getDataSource()); + } + return new SuspendedResourcesHolder(sessionHolder, connectionHolder); + } + + protected void doResume(Object transaction, Object suspendedResources) { + SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources; + if (TransactionSynchronizationManager.hasResource(getSessionFactory())) { + // From non-transactional code running in active transaction synchronization + // -> can be safely removed, will be closed on transaction completion. + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + TransactionSynchronizationManager.bindResource(getSessionFactory(), resourcesHolder.getSessionHolder()); + if (getDataSource() != null) { + TransactionSynchronizationManager.bindResource(getDataSource(), resourcesHolder.getConnectionHolder()); + } + } + + protected void prepareForCommit(DefaultTransactionStatus status) { + if (this.earlyFlushBeforeCommit) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + Session session = txObject.getSessionHolder().getSession(); + if (!session.getFlushMode().lessThan(FlushMode.COMMIT)) { + logger.debug("Performing an early flush for Hibernate transaction"); + try { + session.flush(); + } + catch (HibernateException ex) { + throw convertHibernateAccessException(ex); + } + finally { + session.setFlushMode(FlushMode.NEVER); + } + } + } + } + + protected void doCommit(DefaultTransactionStatus status) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Committing Hibernate transaction on Session [" + + SessionFactoryUtils.toString(txObject.getSessionHolder().getSession()) + "]"); + } + try { + txObject.getSessionHolder().getTransaction().commit(); + } + catch (org.hibernate.TransactionException ex) { + // assumably from commit call to the underlying JDBC connection + throw new TransactionSystemException("Could not commit Hibernate transaction", ex); + } + catch (HibernateException ex) { + // assumably failed to flush changes to database + throw convertHibernateAccessException(ex); + } + } + + protected void doRollback(DefaultTransactionStatus status) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Rolling back Hibernate transaction on Session [" + + SessionFactoryUtils.toString(txObject.getSessionHolder().getSession()) + "]"); + } + try { + txObject.getSessionHolder().getTransaction().rollback(); + } + catch (org.hibernate.TransactionException ex) { + throw new TransactionSystemException("Could not roll back Hibernate transaction", ex); + } + catch (HibernateException ex) { + // Shouldn't really happen, as a rollback doesn't cause a flush. + throw convertHibernateAccessException(ex); + } + finally { + if (!txObject.isNewSession() && !this.hibernateManagedSession) { + // Clear all pending inserts/updates/deletes in the Session. + // Necessary for pre-bound Sessions, to avoid inconsistent state. + txObject.getSessionHolder().getSession().clear(); + } + } + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting Hibernate transaction on Session [" + + SessionFactoryUtils.toString(txObject.getSessionHolder().getSession()) + "] rollback-only"); + } + txObject.setRollbackOnly(); + } + + protected void doCleanupAfterCompletion(Object transaction) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + + // Remove the session holder from the thread. + if (txObject.isNewSessionHolder()) { + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + + // Remove the JDBC connection holder from the thread, if exposed. + if (getDataSource() != null) { + TransactionSynchronizationManager.unbindResource(getDataSource()); + } + + Session session = txObject.getSessionHolder().getSession(); + if (this.prepareConnection && session.isConnected() && isSameConnectionForEntireSession(session)) { + // We're running with connection release mode "on_close": We're able to reset + // the isolation level and/or read-only flag of the JDBC Connection here. + // Else, we need to rely on the connection pool to perform proper cleanup. + try { + Connection con = session.connection(); + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (HibernateException ex) { + logger.debug("Could not access JDBC Connection of Hibernate Session", ex); + } + } + + if (txObject.isNewSession()) { + if (logger.isDebugEnabled()) { + logger.debug("Closing Hibernate Session [" + SessionFactoryUtils.toString(session) + + "] after transaction"); + } + SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory()); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Not closing pre-bound Hibernate Session [" + + SessionFactoryUtils.toString(session) + "] after transaction"); + } + if (txObject.getSessionHolder().getPreviousFlushMode() != null) { + session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode()); + } + if (!this.hibernateManagedSession) { + session.disconnect(); + } + } + txObject.getSessionHolder().clear(); + } + + /** + * Return whether the given Hibernate Session will always hold the same + * JDBC Connection. This is used to check whether the transaction manager + * can safely prepare and clean up the JDBC Connection used for a transaction. + *

Default implementation checks the Session's connection release mode + * to be "on_close". Unfortunately, this requires casting to SessionImpl, + * as of Hibernate 3.1. If that cast doesn't work, we'll simply assume + * we're safe and return true. + * @param session the Hibernate Session to check + * @see org.hibernate.impl.SessionImpl#getConnectionReleaseMode() + * @see org.hibernate.ConnectionReleaseMode#ON_CLOSE + */ + protected boolean isSameConnectionForEntireSession(Session session) { + if (!(session instanceof SessionImpl)) { + // The best we can do is to assume we're safe. + return true; + } + ConnectionReleaseMode releaseMode = ((SessionImpl) session).getConnectionReleaseMode(); + return ConnectionReleaseMode.ON_CLOSE.equals(releaseMode); + } + + + /** + * Convert the given HibernateException to an appropriate exception + * from the org.springframework.dao hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * Hibernate JDBCException, else rely on Hibernate's default translation. + * @param ex HibernateException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertHibernateAccessException + * @see #setJdbcExceptionTranslator + */ + protected DataAccessException convertHibernateAccessException(HibernateException ex) { + if (getJdbcExceptionTranslator() != null && ex instanceof JDBCException) { + return convertJdbcAccessException((JDBCException) ex, getJdbcExceptionTranslator()); + } + else if (GenericJDBCException.class.equals(ex.getClass())) { + return convertJdbcAccessException((GenericJDBCException) ex, getDefaultJdbcExceptionTranslator()); + } + return SessionFactoryUtils.convertHibernateAccessException(ex); + } + + /** + * Convert the given Hibernate JDBCException to an appropriate exception + * from the org.springframework.dao hierarchy, using the + * given SQLExceptionTranslator. + * @param ex Hibernate JDBCException that occured + * @param translator the SQLExceptionTranslator to use + * @return a corresponding DataAccessException + */ + protected DataAccessException convertJdbcAccessException(JDBCException ex, SQLExceptionTranslator translator) { + return translator.translate("Hibernate flushing: " + ex.getMessage(), ex.getSQL(), ex.getSQLException()); + } + + /** + * Obtain a default SQLExceptionTranslator, lazily creating it if necessary. + *

Creates a default + * {@link org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator} + * for the SessionFactory's underlying DataSource. + */ + protected synchronized SQLExceptionTranslator getDefaultJdbcExceptionTranslator() { + if (this.defaultJdbcExceptionTranslator == null) { + if (getDataSource() != null) { + this.defaultJdbcExceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(getDataSource()); + } + else { + this.defaultJdbcExceptionTranslator = SessionFactoryUtils.newJdbcExceptionTranslator(getSessionFactory()); + } + } + return this.defaultJdbcExceptionTranslator; + } + + + /** + * Hibernate transaction object, representing a SessionHolder. + * Used as transaction object by HibernateTransactionManager. + */ + private static class HibernateTransactionObject extends JdbcTransactionObjectSupport { + + private SessionHolder sessionHolder; + + private boolean newSessionHolder; + + private boolean newSession; + + public void setSession(Session session) { + this.sessionHolder = new SessionHolder(session); + this.newSessionHolder = true; + this.newSession = true; + } + + public void setExistingSession(Session session) { + this.sessionHolder = new SessionHolder(session); + this.newSessionHolder = true; + this.newSession = false; + } + + public void setSessionHolder(SessionHolder sessionHolder) { + this.sessionHolder = sessionHolder; + this.newSessionHolder = false; + this.newSession = false; + } + + public SessionHolder getSessionHolder() { + return this.sessionHolder; + } + + public boolean isNewSessionHolder() { + return this.newSessionHolder; + } + + public boolean isNewSession() { + return this.newSession; + } + + public boolean hasSpringManagedTransaction() { + return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null); + } + + public boolean hasHibernateManagedTransaction() { + return (this.sessionHolder != null && this.sessionHolder.getSession().getTransaction().isActive()); + } + + public void setRollbackOnly() { + getSessionHolder().setRollbackOnly(); + if (hasConnectionHolder()) { + getConnectionHolder().setRollbackOnly(); + } + } + + public boolean isRollbackOnly() { + return getSessionHolder().isRollbackOnly() || + (hasConnectionHolder() && getConnectionHolder().isRollbackOnly()); + } + } + + + /** + * Holder for suspended resources. + * Used internally by doSuspend and doResume. + */ + private static class SuspendedResourcesHolder { + + private final SessionHolder sessionHolder; + + private final ConnectionHolder connectionHolder; + + private SuspendedResourcesHolder(SessionHolder sessionHolder, ConnectionHolder conHolder) { + this.sessionHolder = sessionHolder; + this.connectionHolder = conHolder; + } + + private SessionHolder getSessionHolder() { + return this.sessionHolder; + } + + private ConnectionHolder getConnectionHolder() { + return this.connectionHolder; + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalCacheProviderProxy.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalCacheProviderProxy.java new file mode 100644 index 00000000000..a61273740a0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalCacheProviderProxy.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.util.Properties; + +import org.hibernate.cache.Cache; +import org.hibernate.cache.CacheException; +import org.hibernate.cache.CacheProvider; + +/** + * Proxy for a Hibernate CacheProvider, delegating to a Spring-managed + * CacheProvider instance, determined by LocalSessionFactoryBean's + * "cacheProvider" property. + * + * @author Juergen Hoeller + * @since 2.5.1 + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setCacheProvider + */ +public class LocalCacheProviderProxy implements CacheProvider { + + private final CacheProvider cacheProvider; + + + public LocalCacheProviderProxy() { + CacheProvider cp = LocalSessionFactoryBean.getConfigTimeCacheProvider(); + // absolutely needs thread-bound CacheProvider to initialize + if (cp == null) { + throw new IllegalStateException("No Hibernate CacheProvider found - " + + "'cacheProvider' property must be set on LocalSessionFactoryBean"); + } + this.cacheProvider = cp; + } + + + public Cache buildCache(String regionName, Properties properties) throws CacheException { + return this.cacheProvider.buildCache(regionName, properties); + } + + public long nextTimestamp() { + return this.cacheProvider.nextTimestamp(); + } + + public void start(Properties properties) throws CacheException { + this.cacheProvider.start(properties); + } + + public void stop() { + this.cacheProvider.stop(); + } + + public boolean isMinimalPutsEnabledByDefault() { + return this.cacheProvider.isMinimalPutsEnabledByDefault(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalDataSourceConnectionProvider.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalDataSourceConnectionProvider.java new file mode 100644 index 00000000000..9b3fbc37640 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalDataSourceConnectionProvider.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.hibernate.HibernateException; +import org.hibernate.connection.ConnectionProvider; +import org.hibernate.util.JDBCExceptionReporter; + +/** + * Hibernate connection provider for local DataSource instances + * in an application context. This provider will be used if + * LocalSessionFactoryBean's "dataSource" property is set + * without a Hibernate TransactionManagerLookup. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocalSessionFactoryBean#setDataSource + */ +public class LocalDataSourceConnectionProvider implements ConnectionProvider { + + private DataSource dataSource; + + private DataSource dataSourceToUse; + + + public void configure(Properties props) throws HibernateException { + this.dataSource = LocalSessionFactoryBean.getConfigTimeDataSource(); + // absolutely needs thread-bound DataSource to initialize + if (this.dataSource == null) { + throw new HibernateException("No local DataSource found for configuration - " + + "'dataSource' property must be set on LocalSessionFactoryBean"); + } + this.dataSourceToUse = getDataSourceToUse(this.dataSource); + } + + /** + * Return the DataSource to use for retrieving Connections. + *

This implementation returns the passed-in DataSource as-is. + * @param originalDataSource the DataSource as configured by the user + * on LocalSessionFactoryBean + * @return the DataSource to actually retrieve Connections from + * (potentially wrapped) + * @see LocalSessionFactoryBean#setDataSource + */ + protected DataSource getDataSourceToUse(DataSource originalDataSource) { + return originalDataSource; + } + + /** + * Return the DataSource that this ConnectionProvider wraps. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * This implementation delegates to the underlying DataSource. + * @see javax.sql.DataSource#getConnection() + */ + public Connection getConnection() throws SQLException { + try { + return this.dataSourceToUse.getConnection(); + } + catch (SQLException ex) { + JDBCExceptionReporter.logExceptions(ex); + throw ex; + } + } + + /** + * This implementation simply calls Connection.close. + * @see java.sql.Connection#close() + */ + public void closeConnection(Connection con) throws SQLException { + try { + con.close(); + } + catch (SQLException ex) { + JDBCExceptionReporter.logExceptions(ex); + throw ex; + } + } + + /** + * This implementation does nothing: + * We're dealing with an externally managed DataSource. + */ + public void close() { + } + + /** + * This implementation returns false: We cannot guarantee + * to receive the same Connection within a transaction, not even when + * dealing with a JNDI DataSource. + */ + public boolean supportsAggressiveRelease() { + return false; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalJtaDataSourceConnectionProvider.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalJtaDataSourceConnectionProvider.java new file mode 100644 index 00000000000..304fcbb432e --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalJtaDataSourceConnectionProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +/** + * Subclass of LocalDataSourceConnectionProvider that will be used + * if LocalSessionFactoryBean's "dataSource" property is set + * in combination with a Hibernate TransactionManagerLookup. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public class LocalJtaDataSourceConnectionProvider extends LocalDataSourceConnectionProvider { + + /** + * This implementation returns true, + * since we're assuming a JTA DataSource. + */ + public boolean supportsAggressiveRelease() { + return true; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalSessionFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalSessionFactoryBean.java new file mode 100644 index 00000000000..08df08cfaa8 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalSessionFactoryBean.java @@ -0,0 +1,1024 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.io.File; +import java.lang.reflect.Array; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; +import javax.transaction.TransactionManager; + +import org.hibernate.HibernateException; +import org.hibernate.Interceptor; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cache.CacheProvider; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.cfg.Mappings; +import org.hibernate.cfg.NamingStrategy; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.FilterDefinition; +import org.hibernate.event.EventListeners; +import org.hibernate.tool.hbm2ddl.DatabaseMetadata; +import org.hibernate.transaction.JTATransactionFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a + * Hibernate {@link org.hibernate.SessionFactory}. This is the usual way to + * set up a shared Hibernate SessionFactory in a Spring application context; + * the SessionFactory can then be passed to Hibernate-based DAOs via + * dependency injection. + * + *

Configuration settings can either be read from a Hibernate XML file, + * specified as "configLocation", or completely via this class. A typical + * local configuration consists of one or more "mappingResources", various + * "hibernateProperties" (not strictly necessary), and a "dataSource" that the + * SessionFactory should use. The latter can also be specified via Hibernate + * properties, but "dataSource" supports any Spring-configured DataSource, + * instead of relying on Hibernate's own connection providers. + * + *

This SessionFactory handling strategy is appropriate for most types of + * applications, from Hibernate-only single database apps to ones that need + * distributed transactions. Either {@link HibernateTransactionManager} or + * {@link org.springframework.transaction.jta.JtaTransactionManager} can be + * used for transaction demarcation, with the latter only necessary for + * transactions which span multiple databases. + * + *

This factory bean will by default expose a transaction-aware SessionFactory + * proxy, letting data access code work with the plain Hibernate SessionFactory + * and its getCurrentSession() method, while still being able to + * participate in current Spring-managed transactions: with any transaction + * management strategy, either local or JTA / EJB CMT, and any transaction + * synchronization mechanism, either Spring or JTA. Furthermore, + * getCurrentSession() will also seamlessly work with + * a request-scoped Session managed by + * {@link org.springframework.orm.hibernate3.support.OpenSessionInViewFilter} / + * {@link org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor}. + * + *

Requires Hibernate 3.1 or later. Note that this factory will use + * "on_close" as default Hibernate connection release mode, unless in the + * case of a "jtaTransactionManager" specified, for the reason that + * this is appropriate for most Spring-based applications (in particular when + * using Spring's HibernateTransactionManager). Hibernate 3.0 used "on_close" + * as its own default too; however, Hibernate 3.1 changed this to "auto" + * (i.e. "after_statement" or "after_transaction"). + * + * @author Juergen Hoeller + * @since 1.2 + * @see HibernateTemplate#setSessionFactory + * @see HibernateTransactionManager#setSessionFactory + * @see #setExposeTransactionAwareSessionFactory + * @see #setJtaTransactionManager + * @see org.hibernate.SessionFactory#getCurrentSession() + * @see HibernateTransactionManager + */ +public class LocalSessionFactoryBean extends AbstractSessionFactoryBean implements BeanClassLoaderAware { + + private static final ThreadLocal configTimeDataSourceHolder = new ThreadLocal(); + + private static final ThreadLocal configTimeTransactionManagerHolder = new ThreadLocal(); + + private static final ThreadLocal configTimeCacheProviderHolder = new ThreadLocal(); + + private static final ThreadLocal configTimeLobHandlerHolder = new ThreadLocal(); + + /** + * Return the DataSource for the currently configured Hibernate SessionFactory, + * to be used by LocalDataSourceConnectionProvoder. + *

This instance will be set before initialization of the corresponding + * SessionFactory, and reset immediately afterwards. It is thus only available + * during configuration. + * @see #setDataSource + * @see LocalDataSourceConnectionProvider + */ + public static DataSource getConfigTimeDataSource() { + return (DataSource) configTimeDataSourceHolder.get(); + } + + /** + * Return the JTA TransactionManager for the currently configured Hibernate + * SessionFactory, to be used by LocalTransactionManagerLookup. + *

This instance will be set before initialization of the corresponding + * SessionFactory, and reset immediately afterwards. It is thus only available + * during configuration. + * @see #setJtaTransactionManager + * @see LocalTransactionManagerLookup + */ + public static TransactionManager getConfigTimeTransactionManager() { + return (TransactionManager) configTimeTransactionManagerHolder.get(); + } + + /** + * Return the CacheProvider for the currently configured Hibernate SessionFactory, + * to be used by LocalCacheProviderProxy. + *

This instance will be set before initialization of the corresponding + * SessionFactory, and reset immediately afterwards. It is thus only available + * during configuration. + * @see #setCacheProvider + */ + public static CacheProvider getConfigTimeCacheProvider() { + return (CacheProvider) configTimeCacheProviderHolder.get(); + } + + /** + * Return the LobHandler for the currently configured Hibernate SessionFactory, + * to be used by UserType implementations like ClobStringType. + *

This instance will be set before initialization of the corresponding + * SessionFactory, and reset immediately afterwards. It is thus only available + * during configuration. + * @see #setLobHandler + * @see org.springframework.orm.hibernate3.support.ClobStringType + * @see org.springframework.orm.hibernate3.support.BlobByteArrayType + * @see org.springframework.orm.hibernate3.support.BlobSerializableType + */ + public static LobHandler getConfigTimeLobHandler() { + return (LobHandler) configTimeLobHandlerHolder.get(); + } + + + private Class configurationClass = Configuration.class; + + private Resource[] configLocations; + + private String[] mappingResources; + + private Resource[] mappingLocations; + + private Resource[] cacheableMappingLocations; + + private Resource[] mappingJarLocations; + + private Resource[] mappingDirectoryLocations; + + private Properties hibernateProperties; + + private TransactionManager jtaTransactionManager; + + private CacheProvider cacheProvider; + + private LobHandler lobHandler; + + private Interceptor entityInterceptor; + + private NamingStrategy namingStrategy; + + private TypeDefinitionBean[] typeDefinitions; + + private FilterDefinition[] filterDefinitions; + + private Properties entityCacheStrategies; + + private Properties collectionCacheStrategies; + + private Map eventListeners; + + private boolean schemaUpdate = false; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Configuration configuration; + + + /** + * Specify the Hibernate Configuration class to use. + * Default is "org.hibernate.cfg.Configuration"; any subclass of + * this default Hibernate Configuration class can be specified. + *

Can be set to "org.hibernate.cfg.AnnotationConfiguration" for + * using Hibernate3 annotation support (initially only available as + * alpha download separate from the main Hibernate3 distribution). + *

Annotated packages and annotated classes can be specified via the + * corresponding tags in "hibernate.cfg.xml" then, so this will usually + * be combined with a "configLocation" property that points at such a + * standard Hibernate configuration file. + * @see #setConfigLocation + * @see org.hibernate.cfg.Configuration + * @see org.hibernate.cfg.AnnotationConfiguration + */ + public void setConfigurationClass(Class configurationClass) { + if (configurationClass == null || !Configuration.class.isAssignableFrom(configurationClass)) { + throw new IllegalArgumentException( + "configurationClass must be assignable to [org.hibernate.cfg.Configuration]"); + } + this.configurationClass = configurationClass; + } + + /** + * Set the location of a single Hibernate XML config file, for example as + * classpath resource "classpath:hibernate.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see org.hibernate.cfg.Configuration#configure(java.net.URL) + */ + public void setConfigLocation(Resource configLocation) { + this.configLocations = new Resource[] {configLocation}; + } + + /** + * Set the locations of multiple Hibernate XML config files, for example as + * classpath resources "classpath:hibernate.cfg.xml,classpath:extension.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see org.hibernate.cfg.Configuration#configure(java.net.URL) + */ + public void setConfigLocations(Resource[] configLocations) { + this.configLocations = configLocations; + } + + /** + * Set Hibernate mapping resources to be found in the class path, + * like "example.hbm.xml" or "mypackage/example.hbm.xml". + * Analogous to mapping entries in a Hibernate XML config file. + * Alternative to the more generic setMappingLocations method. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see #setMappingLocations + * @see org.hibernate.cfg.Configuration#addResource + */ + public void setMappingResources(String[] mappingResources) { + this.mappingResources = mappingResources; + } + + /** + * Set locations of Hibernate mapping files, for example as classpath + * resource "classpath:example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, for example relative paths like + * "WEB-INF/mappings/example.hbm.xml" when running in an application context. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addInputStream + */ + public void setMappingLocations(Resource[] mappingLocations) { + this.mappingLocations = mappingLocations; + } + + /** + * Set locations of cacheable Hibernate mapping files, for example as web app + * resource "/WEB-INF/mapping/example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, as long as the resource can be resolved + * in the file system. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addCacheableFile(java.io.File) + */ + public void setCacheableMappingLocations(Resource[] cacheableMappingLocations) { + this.cacheableMappingLocations = cacheableMappingLocations; + } + + /** + * Set locations of jar files that contain Hibernate mapping resources, + * like "WEB-INF/lib/example.hbm.jar". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addJar(java.io.File) + */ + public void setMappingJarLocations(Resource[] mappingJarLocations) { + this.mappingJarLocations = mappingJarLocations; + } + + /** + * Set locations of directories that contain Hibernate mapping resources, + * like "WEB-INF/mappings". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see org.hibernate.cfg.Configuration#addDirectory(java.io.File) + */ + public void setMappingDirectoryLocations(Resource[] mappingDirectoryLocations) { + this.mappingDirectoryLocations = mappingDirectoryLocations; + } + + /** + * Set Hibernate properties, such as "hibernate.dialect". + *

Can be used to override values in a Hibernate XML config file, + * or to specify all necessary properties locally. + *

Note: Do not specify a transaction provider here when using + * Spring-driven transactions. It is also advisable to omit connection + * provider settings and use a Spring-set DataSource instead. + * @see #setDataSource + */ + public void setHibernateProperties(Properties hibernateProperties) { + this.hibernateProperties = hibernateProperties; + } + + /** + * Return the Hibernate properties, if any. Mainly available for + * configuration through property paths that specify individual keys. + */ + public Properties getHibernateProperties() { + if (this.hibernateProperties == null) { + this.hibernateProperties = new Properties(); + } + return this.hibernateProperties; + } + + /** + * Set the JTA TransactionManager to be used for Hibernate's + * TransactionManagerLookup. Allows for using a Spring-managed + * JTA TransactionManager for Hibernate's cache synchronization. + *

Note: If this is set, the Hibernate settings should not define a + * transaction manager lookup to avoid meaningless double configuration. + * @see LocalTransactionManagerLookup + */ + public void setJtaTransactionManager(TransactionManager jtaTransactionManager) { + this.jtaTransactionManager = jtaTransactionManager; + } + + /** + * Set the Hibernate CacheProvider to use for the SessionFactory. + * Allows for using a Spring-managed CacheProvider instance. + *

Note: If this is set, the Hibernate settings should not define a + * cache provider to avoid meaningless double configuration. + * @see LocalCacheProviderProxy + */ + public void setCacheProvider(CacheProvider cacheProvider) { + this.cacheProvider = cacheProvider; + } + + /** + * Set the LobHandler to be used by the SessionFactory. + * Will be exposed at config time for UserType implementations. + * @see #getConfigTimeLobHandler + * @see org.hibernate.usertype.UserType + * @see org.springframework.orm.hibernate3.support.ClobStringType + * @see org.springframework.orm.hibernate3.support.BlobByteArrayType + * @see org.springframework.orm.hibernate3.support.BlobSerializableType + */ + public void setLobHandler(LobHandler lobHandler) { + this.lobHandler = lobHandler; + } + + /** + * Set a Hibernate entity interceptor that allows to inspect and change + * property values before writing to and reading from the database. + * Will get applied to any new Session created by this factory. + *

Such an interceptor can either be set at the SessionFactory level, i.e. on + * LocalSessionFactoryBean, or at the Session level, i.e. on HibernateTemplate, + * HibernateInterceptor, and HibernateTransactionManager. It's preferable to set + * it on LocalSessionFactoryBean or HibernateTransactionManager to avoid repeated + * configuration and guarantee consistent behavior in transactions. + * @see HibernateTemplate#setEntityInterceptor + * @see HibernateInterceptor#setEntityInterceptor + * @see HibernateTransactionManager#setEntityInterceptor + * @see org.hibernate.cfg.Configuration#setInterceptor + */ + public void setEntityInterceptor(Interceptor entityInterceptor) { + this.entityInterceptor = entityInterceptor; + } + + /** + * Set a Hibernate NamingStrategy for the SessionFactory, determining the + * physical column and table names given the info in the mapping document. + * @see org.hibernate.cfg.Configuration#setNamingStrategy + */ + public void setNamingStrategy(NamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + /** + * Specify the Hibernate type definitions to register with the SessionFactory, + * as Spring TypeDefinitionBean instances. This is an alternative to specifying + * <<typedef> elements in Hibernate mapping files. + *

Unfortunately, Hibernate itself does not define a complete object that + * represents a type definition, hence the need for Spring's TypeDefinitionBean. + * @see TypeDefinitionBean + * @see org.hibernate.cfg.Mappings#addTypeDef(String, String, java.util.Properties) + */ + public void setTypeDefinitions(TypeDefinitionBean[] typeDefinitions) { + this.typeDefinitions = typeDefinitions; + } + + /** + * Specify the Hibernate FilterDefinitions to register with the SessionFactory. + * This is an alternative to specifying <<filter-def> elements in + * Hibernate mapping files. + *

Typically, the passed-in FilterDefinition objects will have been defined + * as Spring FilterDefinitionFactoryBeans, probably as inner beans within the + * LocalSessionFactoryBean definition. + * @see FilterDefinitionFactoryBean + * @see org.hibernate.cfg.Configuration#addFilterDefinition + */ + public void setFilterDefinitions(FilterDefinition[] filterDefinitions) { + this.filterDefinitions = filterDefinitions; + } + + /** + * Specify the cache strategies for entities (persistent classes or named entities). + * This configuration setting corresponds to the <class-cache> entry + * in the "hibernate.cfg.xml" configuration format. + *

For example: + *

+	 * <property name="entityCacheStrategies">
+	 *   <props>
+	 *     <prop key="com.mycompany.Customer">read-write</prop>
+	 *     <prop key="com.mycompany.Product">read-only,myRegion</prop>
+	 *   </props>
+	 * </property>
+ * Note that appending a cache region name (with a comma separator) is only + * supported on Hibernate 3.1, where this functionality is publically available. + * @param entityCacheStrategies properties that define entity cache strategies, + * with class names as keys and cache concurrency strategies as values + * @see org.hibernate.cfg.Configuration#setCacheConcurrencyStrategy(String, String) + */ + public void setEntityCacheStrategies(Properties entityCacheStrategies) { + this.entityCacheStrategies = entityCacheStrategies; + } + + /** + * Specify the cache strategies for persistent collections (with specific roles). + * This configuration setting corresponds to the <collection-cache> entry + * in the "hibernate.cfg.xml" configuration format. + *

For example: + *

+	 * <property name="collectionCacheStrategies">
+	 *   <props>
+	 *     <prop key="com.mycompany.Order.items">read-write</prop>
+	 *     <prop key="com.mycompany.Product.categories">read-only,myRegion</prop>
+	 *   </props>
+	 * </property>
+ * Note that appending a cache region name (with a comma separator) is only + * supported on Hibernate 3.1, where this functionality is publically available. + * @param collectionCacheStrategies properties that define collection cache strategies, + * with collection roles as keys and cache concurrency strategies as values + * @see org.hibernate.cfg.Configuration#setCollectionCacheConcurrencyStrategy(String, String) + */ + public void setCollectionCacheStrategies(Properties collectionCacheStrategies) { + this.collectionCacheStrategies = collectionCacheStrategies; + } + + /** + * Specify the Hibernate event listeners to register, with listener types + * as keys and listener objects as values. + *

Instead of a single listener object, you can also pass in a list + * or set of listeners objects as value. However, this is only supported + * on Hibernate 3.1. + *

See the Hibernate documentation for further details on listener types + * and associated listener interfaces. + * @param eventListeners Map with listener type Strings as keys and + * listener objects as values + * @see org.hibernate.cfg.Configuration#setListener(String, Object) + */ + public void setEventListeners(Map eventListeners) { + this.eventListeners = eventListeners; + } + + /** + * Set whether to execute a schema update after SessionFactory initialization. + *

For details on how to make schema update scripts work, see the Hibernate + * documentation, as this class leverages the same schema update script support + * in org.hibernate.cfg.Configuration as Hibernate's own SchemaUpdate tool. + * @see org.hibernate.cfg.Configuration#generateSchemaUpdateScript + * @see org.hibernate.tool.hbm2ddl.SchemaUpdate + */ + public void setSchemaUpdate(boolean schemaUpdate) { + this.schemaUpdate = schemaUpdate; + } + + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + protected SessionFactory buildSessionFactory() throws Exception { + // Create Configuration instance. + Configuration config = newConfiguration(); + + DataSource dataSource = getDataSource(); + if (dataSource != null) { + // Make given DataSource available for SessionFactory configuration. + configTimeDataSourceHolder.set(dataSource); + } + if (this.jtaTransactionManager != null) { + // Make Spring-provided JTA TransactionManager available. + configTimeTransactionManagerHolder.set(this.jtaTransactionManager); + } + if (this.cacheProvider != null) { + // Make Spring-provided Hibernate CacheProvider available. + configTimeCacheProviderHolder.set(this.cacheProvider); + } + if (this.lobHandler != null) { + // Make given LobHandler available for SessionFactory configuration. + // Do early because because mapping resource might refer to custom types. + configTimeLobHandlerHolder.set(this.lobHandler); + } + + // Analogous to Hibernate EntityManager's Ejb3Configuration: + // Hibernate doesn't allow setting the bean ClassLoader explicitly, + // so we need to expose it as thread context ClassLoader accordingly. + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); + boolean overrideClassLoader = + (this.beanClassLoader != null && !this.beanClassLoader.equals(threadContextClassLoader)); + if (overrideClassLoader) { + currentThread.setContextClassLoader(this.beanClassLoader); + } + + try { + if (isExposeTransactionAwareSessionFactory()) { + // Set Hibernate 3.1 CurrentSessionContext implementation, + // providing the Spring-managed Session as current Session. + // Can be overridden by a custom value for the corresponding Hibernate property. + config.setProperty( + Environment.CURRENT_SESSION_CONTEXT_CLASS, SpringSessionContext.class.getName()); + } + + if (this.jtaTransactionManager != null) { + // Set Spring-provided JTA TransactionManager as Hibernate property. + config.setProperty( + Environment.TRANSACTION_STRATEGY, JTATransactionFactory.class.getName()); + config.setProperty( + Environment.TRANSACTION_MANAGER_STRATEGY, LocalTransactionManagerLookup.class.getName()); + } + else { + // Makes the Hibernate Session aware of the presence of a Spring-managed transaction. + // Also sets connection release mode to ON_CLOSE by default. + config.setProperty( + Environment.TRANSACTION_STRATEGY, SpringTransactionFactory.class.getName()); + } + + if (this.entityInterceptor != null) { + // Set given entity interceptor at SessionFactory level. + config.setInterceptor(this.entityInterceptor); + } + + if (this.namingStrategy != null) { + // Pass given naming strategy to Hibernate Configuration. + config.setNamingStrategy(this.namingStrategy); + } + + if (this.typeDefinitions != null) { + // Register specified Hibernate type definitions. + Mappings mappings = config.createMappings(); + for (int i = 0; i < this.typeDefinitions.length; i++) { + TypeDefinitionBean typeDef = this.typeDefinitions[i]; + mappings.addTypeDef(typeDef.getTypeName(), typeDef.getTypeClass(), typeDef.getParameters()); + } + } + + if (this.filterDefinitions != null) { + // Register specified Hibernate FilterDefinitions. + for (int i = 0; i < this.filterDefinitions.length; i++) { + config.addFilterDefinition(this.filterDefinitions[i]); + } + } + + if (this.configLocations != null) { + for (int i = 0; i < this.configLocations.length; i++) { + // Load Hibernate configuration from given location. + config.configure(this.configLocations[i].getURL()); + } + } + + if (this.hibernateProperties != null) { + // Add given Hibernate properties to Configuration. + config.addProperties(this.hibernateProperties); + } + + if (dataSource != null) { + Class providerClass = LocalDataSourceConnectionProvider.class; + if (isUseTransactionAwareDataSource() || dataSource instanceof TransactionAwareDataSourceProxy) { + providerClass = TransactionAwareDataSourceConnectionProvider.class; + } + else if (config.getProperty(Environment.TRANSACTION_MANAGER_STRATEGY) != null) { + providerClass = LocalJtaDataSourceConnectionProvider.class; + } + // Set Spring-provided DataSource as Hibernate ConnectionProvider. + config.setProperty(Environment.CONNECTION_PROVIDER, providerClass.getName()); + } + + if (this.cacheProvider != null) { + // Expose Spring-provided Hibernate CacheProvider. + config.setProperty(Environment.CACHE_PROVIDER, LocalCacheProviderProxy.class.getName()); + } + + if (this.mappingResources != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (int i = 0; i < this.mappingResources.length; i++) { + Resource resource = new ClassPathResource(this.mappingResources[i].trim(), this.beanClassLoader); + config.addInputStream(resource.getInputStream()); + } + } + + if (this.mappingLocations != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (int i = 0; i < this.mappingLocations.length; i++) { + config.addInputStream(this.mappingLocations[i].getInputStream()); + } + } + + if (this.cacheableMappingLocations != null) { + // Register given cacheable Hibernate mapping definitions, read from the file system. + for (int i = 0; i < this.cacheableMappingLocations.length; i++) { + config.addCacheableFile(this.cacheableMappingLocations[i].getFile()); + } + } + + if (this.mappingJarLocations != null) { + // Register given Hibernate mapping definitions, contained in jar files. + for (int i = 0; i < this.mappingJarLocations.length; i++) { + Resource resource = this.mappingJarLocations[i]; + config.addJar(resource.getFile()); + } + } + + if (this.mappingDirectoryLocations != null) { + // Register all Hibernate mapping definitions in the given directories. + for (int i = 0; i < this.mappingDirectoryLocations.length; i++) { + File file = this.mappingDirectoryLocations[i].getFile(); + if (!file.isDirectory()) { + throw new IllegalArgumentException( + "Mapping directory location [" + this.mappingDirectoryLocations[i] + + "] does not denote a directory"); + } + config.addDirectory(file); + } + } + + // Tell Hibernate to eagerly compile the mappings that we registered, + // for availability of the mapping information in further processing. + postProcessMappings(config); + config.buildMappings(); + + if (this.entityCacheStrategies != null) { + // Register cache strategies for mapped entities. + for (Enumeration classNames = this.entityCacheStrategies.propertyNames(); classNames.hasMoreElements();) { + String className = (String) classNames.nextElement(); + String[] strategyAndRegion = + StringUtils.commaDelimitedListToStringArray(this.entityCacheStrategies.getProperty(className)); + if (strategyAndRegion.length > 1) { + config.setCacheConcurrencyStrategy(className, strategyAndRegion[0], strategyAndRegion[1]); + } + else if (strategyAndRegion.length > 0) { + config.setCacheConcurrencyStrategy(className, strategyAndRegion[0]); + } + } + } + + if (this.collectionCacheStrategies != null) { + // Register cache strategies for mapped collections. + for (Enumeration collRoles = this.collectionCacheStrategies.propertyNames(); collRoles.hasMoreElements();) { + String collRole = (String) collRoles.nextElement(); + String[] strategyAndRegion = + StringUtils.commaDelimitedListToStringArray(this.collectionCacheStrategies.getProperty(collRole)); + if (strategyAndRegion.length > 1) { + config.setCollectionCacheConcurrencyStrategy(collRole, strategyAndRegion[0], strategyAndRegion[1]); + } + else if (strategyAndRegion.length > 0) { + config.setCollectionCacheConcurrencyStrategy(collRole, strategyAndRegion[0]); + } + } + } + + if (this.eventListeners != null) { + // Register specified Hibernate event listeners. + for (Iterator it = this.eventListeners.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + Assert.isTrue(entry.getKey() instanceof String, "Event listener key needs to be of type String"); + String listenerType = (String) entry.getKey(); + Object listenerObject = entry.getValue(); + if (listenerObject instanceof Collection) { + Collection listeners = (Collection) listenerObject; + EventListeners listenerRegistry = config.getEventListeners(); + Object[] listenerArray = + (Object[]) Array.newInstance(listenerRegistry.getListenerClassFor(listenerType), listeners.size()); + listenerArray = listeners.toArray(listenerArray); + config.setListeners(listenerType, listenerArray); + } + else { + config.setListener(listenerType, listenerObject); + } + } + } + + // Perform custom post-processing in subclasses. + postProcessConfiguration(config); + + // Build SessionFactory instance. + logger.info("Building new Hibernate SessionFactory"); + this.configuration = config; + return newSessionFactory(config); + } + + finally { + if (dataSource != null) { + // Reset DataSource holder. + configTimeDataSourceHolder.set(null); + } + if (this.jtaTransactionManager != null) { + // Reset TransactionManager holder. + configTimeTransactionManagerHolder.set(null); + } + if (this.cacheProvider != null) { + // Reset CacheProvider holder. + configTimeCacheProviderHolder.set(null); + } + if (this.lobHandler != null) { + // Reset LobHandler holder. + configTimeLobHandlerHolder.set(null); + } + if (overrideClassLoader) { + // Reset original thread context ClassLoader. + currentThread.setContextClassLoader(threadContextClassLoader); + } + } + } + + /** + * Subclasses can override this method to perform custom initialization + * of the Configuration instance used for SessionFactory creation. + * The properties of this LocalSessionFactoryBean will be applied to + * the Configuration object that gets returned here. + *

The default implementation creates a new Configuration instance. + * A custom implementation could prepare the instance in a specific way, + * or use a custom Configuration subclass. + * @return the Configuration instance + * @throws HibernateException in case of Hibernate initialization errors + * @see org.hibernate.cfg.Configuration#Configuration() + */ + protected Configuration newConfiguration() throws HibernateException { + return (Configuration) BeanUtils.instantiateClass(this.configurationClass); + } + + /** + * To be implemented by subclasses that want to to register further mappings + * on the Configuration object after this FactoryBean registered its specified + * mappings. + *

Invoked before the Configuration.buildMappings() call, + * so that it can still extend and modify the mapping information. + * @param config the current Configuration object + * @throws HibernateException in case of Hibernate initialization errors + * @see org.hibernate.cfg.Configuration#buildMappings() + */ + protected void postProcessMappings(Configuration config) throws HibernateException { + } + + /** + * To be implemented by subclasses that want to to perform custom + * post-processing of the Configuration object after this FactoryBean + * performed its default initialization. + *

Invoked after the Configuration.buildMappings() call, + * so that it can operate on the completed and fully parsed mapping information. + * @param config the current Configuration object + * @throws HibernateException in case of Hibernate initialization errors + * @see org.hibernate.cfg.Configuration#buildMappings() + */ + protected void postProcessConfiguration(Configuration config) throws HibernateException { + } + + /** + * Subclasses can override this method to perform custom initialization + * of the SessionFactory instance, creating it via the given Configuration + * object that got prepared by this LocalSessionFactoryBean. + *

The default implementation invokes Configuration's buildSessionFactory. + * A custom implementation could prepare the instance in a specific way, + * or use a custom SessionFactoryImpl subclass. + * @param config Configuration prepared by this LocalSessionFactoryBean + * @return the SessionFactory instance + * @throws HibernateException in case of Hibernate initialization errors + * @see org.hibernate.cfg.Configuration#buildSessionFactory + */ + protected SessionFactory newSessionFactory(Configuration config) throws HibernateException { + return config.buildSessionFactory(); + } + + /** + * Return the Configuration object used to build the SessionFactory. + * Allows access to configuration metadata stored there (rarely needed). + * @throws IllegalStateException if the Configuration object has not been initialized yet + */ + public final Configuration getConfiguration() { + if (this.configuration == null) { + throw new IllegalStateException("Configuration not initialized yet"); + } + return this.configuration; + } + + /** + * Executes schema update if requested. + * @see #setSchemaUpdate + * @see #updateDatabaseSchema() + */ + protected void afterSessionFactoryCreation() throws Exception { + if (this.schemaUpdate) { + DataSource dataSource = getDataSource(); + if (dataSource != null) { + // Make given DataSource available for the schema update, + // which unfortunately reinstantiates a ConnectionProvider. + configTimeDataSourceHolder.set(dataSource); + } + try { + updateDatabaseSchema(); + } + finally { + if (dataSource != null) { + // Reset DataSource holder. + configTimeDataSourceHolder.set(null); + } + } + } + } + + /** + * Allows for schema export on shutdown. + */ + public void destroy() throws HibernateException { + DataSource dataSource = getDataSource(); + if (dataSource != null) { + // Make given DataSource available for potential SchemaExport, + // which unfortunately reinstantiates a ConnectionProvider. + configTimeDataSourceHolder.set(dataSource); + } + try { + super.destroy(); + } + finally { + if (dataSource != null) { + // Reset DataSource holder. + configTimeDataSourceHolder.set(null); + } + } + } + + + /** + * Execute schema drop script, determined by the Configuration object + * used for creating the SessionFactory. A replacement for Hibernate's + * SchemaExport class, to be invoked on application setup. + *

Fetch the LocalSessionFactoryBean itself rather than the exposed + * SessionFactory to be able to invoke this method, e.g. via + * LocalSessionFactoryBean lsfb = (LocalSessionFactoryBean) ctx.getBean("&mySessionFactory");. + *

Uses the SessionFactory that this bean generates for accessing a JDBC + * connection to perform the script. + * @throws org.springframework.dao.DataAccessException in case of script execution errors + * @see org.hibernate.cfg.Configuration#generateDropSchemaScript + * @see org.hibernate.tool.hbm2ddl.SchemaExport#drop + */ + public void dropDatabaseSchema() throws DataAccessException { + logger.info("Dropping database schema for Hibernate SessionFactory"); + HibernateTemplate hibernateTemplate = new HibernateTemplate(getSessionFactory()); + hibernateTemplate.execute( + new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException, SQLException { + Connection con = session.connection(); + Dialect dialect = Dialect.getDialect(getConfiguration().getProperties()); + String[] sql = getConfiguration().generateDropSchemaScript(dialect); + executeSchemaScript(con, sql); + return null; + } + } + ); + } + + /** + * Execute schema creation script, determined by the Configuration object + * used for creating the SessionFactory. A replacement for Hibernate's + * SchemaExport class, to be invoked on application setup. + *

Fetch the LocalSessionFactoryBean itself rather than the exposed + * SessionFactory to be able to invoke this method, e.g. via + * LocalSessionFactoryBean lsfb = (LocalSessionFactoryBean) ctx.getBean("&mySessionFactory");. + *

Uses the SessionFactory that this bean generates for accessing a JDBC + * connection to perform the script. + * @throws DataAccessException in case of script execution errors + * @see org.hibernate.cfg.Configuration#generateSchemaCreationScript + * @see org.hibernate.tool.hbm2ddl.SchemaExport#create + */ + public void createDatabaseSchema() throws DataAccessException { + logger.info("Creating database schema for Hibernate SessionFactory"); + HibernateTemplate hibernateTemplate = new HibernateTemplate(getSessionFactory()); + hibernateTemplate.execute( + new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException, SQLException { + Connection con = session.connection(); + Dialect dialect = Dialect.getDialect(getConfiguration().getProperties()); + String[] sql = getConfiguration().generateSchemaCreationScript(dialect); + executeSchemaScript(con, sql); + return null; + } + } + ); + } + + /** + * Execute schema update script, determined by the Configuration object + * used for creating the SessionFactory. A replacement for Hibernate's + * SchemaUpdate class, for automatically executing schema update scripts + * on application startup. Can also be invoked manually. + *

Fetch the LocalSessionFactoryBean itself rather than the exposed + * SessionFactory to be able to invoke this method, e.g. via + * LocalSessionFactoryBean lsfb = (LocalSessionFactoryBean) ctx.getBean("&mySessionFactory");. + *

Uses the SessionFactory that this bean generates for accessing a JDBC + * connection to perform the script. + * @throws DataAccessException in case of script execution errors + * @see #setSchemaUpdate + * @see org.hibernate.cfg.Configuration#generateSchemaUpdateScript + * @see org.hibernate.tool.hbm2ddl.SchemaUpdate + */ + public void updateDatabaseSchema() throws DataAccessException { + logger.info("Updating database schema for Hibernate SessionFactory"); + HibernateTemplate hibernateTemplate = new HibernateTemplate(getSessionFactory()); + hibernateTemplate.setFlushMode(HibernateTemplate.FLUSH_NEVER); + hibernateTemplate.execute( + new HibernateCallback() { + public Object doInHibernate(Session session) throws HibernateException, SQLException { + Connection con = session.connection(); + Dialect dialect = Dialect.getDialect(getConfiguration().getProperties()); + DatabaseMetadata metadata = new DatabaseMetadata(con, dialect); + String[] sql = getConfiguration().generateSchemaUpdateScript(dialect, metadata); + executeSchemaScript(con, sql); + return null; + } + } + ); + } + + /** + * Execute the given schema script on the given JDBC Connection. + *

Note that the default implementation will log unsuccessful statements + * and continue to execute. Override the executeSchemaStatement + * method to treat failures differently. + * @param con the JDBC Connection to execute the script on + * @param sql the SQL statements to execute + * @throws SQLException if thrown by JDBC methods + * @see #executeSchemaStatement + */ + protected void executeSchemaScript(Connection con, String[] sql) throws SQLException { + if (sql != null && sql.length > 0) { + boolean oldAutoCommit = con.getAutoCommit(); + if (!oldAutoCommit) { + con.setAutoCommit(true); + } + try { + Statement stmt = con.createStatement(); + try { + for (int i = 0; i < sql.length; i++) { + executeSchemaStatement(stmt, sql[i]); + } + } + finally { + JdbcUtils.closeStatement(stmt); + } + } + finally { + if (!oldAutoCommit) { + con.setAutoCommit(false); + } + } + } + } + + /** + * Execute the given schema SQL on the given JDBC Statement. + *

Note that the default implementation will log unsuccessful statements + * and continue to execute. Override this method to treat failures differently. + * @param stmt the JDBC Statement to execute the SQL on + * @param sql the SQL statement to execute + * @throws SQLException if thrown by JDBC methods (and considered fatal) + */ + protected void executeSchemaStatement(Statement stmt, String sql) throws SQLException { + if (logger.isDebugEnabled()) { + logger.debug("Executing schema statement: " + sql); + } + try { + stmt.executeUpdate(sql); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Unsuccessful schema statement: " + sql, ex); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalTransactionManagerLookup.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalTransactionManagerLookup.java new file mode 100644 index 00000000000..0990f8384fc --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/LocalTransactionManagerLookup.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.util.Properties; + +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import org.hibernate.transaction.TransactionManagerLookup; + +/** + * Implementation of Hibernate's {@link TransactionManagerLookup} interface + * that returns a Spring-managed JTA {@link TransactionManager}, determined + * by LocalSessionFactoryBean's "jtaTransactionManager" property. + * + *

The main advantage of this TransactionManagerLookup is that it avoids + * double configuration of JTA specifics. A single TransactionManager bean can + * be used for both JtaTransactionManager and LocalSessionFactoryBean, with no + * JTA setup in Hibernate configuration. + * + *

Alternatively, use Hibernate's own TransactionManagerLookup implementations: + * Spring's JtaTransactionManager only requires a TransactionManager for suspending + * and resuming transactions, so you might not need to apply such special Spring + * configuration at all. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocalSessionFactoryBean#setJtaTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager + */ +public class LocalTransactionManagerLookup implements TransactionManagerLookup { + + private final TransactionManager transactionManager; + + + public LocalTransactionManagerLookup() { + TransactionManager tm = LocalSessionFactoryBean.getConfigTimeTransactionManager(); + // absolutely needs thread-bound TransactionManager to initialize + if (tm == null) { + throw new IllegalStateException("No JTA TransactionManager found - " + + "'jtaTransactionManager' property must be set on LocalSessionFactoryBean"); + } + this.transactionManager = tm; + } + + public TransactionManager getTransactionManager(Properties props) { + return this.transactionManager; + } + + public String getUserTransactionName() { + return null; + } + + public Object getTransactionIdentifier(Transaction transaction) { + return transaction; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SessionFactoryUtils.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SessionFactoryUtils.java new file mode 100644 index 00000000000..83a3d05b73a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SessionFactoryUtils.java @@ -0,0 +1,802 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; +import javax.transaction.Status; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.Criteria; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Interceptor; +import org.hibernate.JDBCException; +import org.hibernate.NonUniqueResultException; +import org.hibernate.ObjectDeletedException; +import org.hibernate.PersistentObjectException; +import org.hibernate.PropertyValueException; +import org.hibernate.Query; +import org.hibernate.QueryException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.StaleObjectStateException; +import org.hibernate.StaleStateException; +import org.hibernate.TransientObjectException; +import org.hibernate.UnresolvableObjectException; +import org.hibernate.WrongClassException; +import org.hibernate.connection.ConnectionProvider; +import org.hibernate.engine.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.DataException; +import org.hibernate.exception.JDBCConnectionException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.SQLGrammarException; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; +import org.springframework.transaction.jta.SpringJtaSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class featuring methods for Hibernate Session handling, + * allowing for reuse of Hibernate Session instances within transactions. + * Also provides support for exception translation. + * + *

Supports synchronization with both Spring-managed JTA transactions + * (see {@link org.springframework.transaction.jta.JtaTransactionManager}) + * and non-Spring JTA transactions (i.e. plain JTA or EJB CMT), + * transparently providing transaction-scoped Hibernate Sessions. + * Note that for non-Spring JTA transactions, a JTA TransactionManagerLookup + * has to be specified in the Hibernate configuration. + * + *

Used internally by {@link HibernateTemplate}, {@link HibernateInterceptor} + * and {@link HibernateTransactionManager}. Can also be used directly in + * application code. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #getSession + * @see #releaseSession + * @see HibernateTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public abstract class SessionFactoryUtils { + + /** + * Order value for TransactionSynchronization objects that clean up Hibernate Sessions. + * Returns DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100 + * to execute Session cleanup before JDBC Connection cleanup, if any. + * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER + */ + public static final int SESSION_SYNCHRONIZATION_ORDER = + DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100; + + static final Log logger = LogFactory.getLog(SessionFactoryUtils.class); + + private static final ThreadLocal deferredCloseHolder = + new NamedThreadLocal("Hibernate Sessions registered for deferred close"); + + + /** + * Determine the DataSource of the given SessionFactory. + * @param sessionFactory the SessionFactory to check + * @return the DataSource, or null if none found + * @see org.hibernate.engine.SessionFactoryImplementor#getConnectionProvider + * @see LocalDataSourceConnectionProvider + */ + public static DataSource getDataSource(SessionFactory sessionFactory) { + if (sessionFactory instanceof SessionFactoryImplementor) { + ConnectionProvider cp = ((SessionFactoryImplementor) sessionFactory).getConnectionProvider(); + if (cp instanceof LocalDataSourceConnectionProvider) { + return ((LocalDataSourceConnectionProvider) cp).getDataSource(); + } + } + return null; + } + + /** + * Create an appropriate SQLExceptionTranslator for the given SessionFactory. + * If a DataSource is found, a SQLErrorCodeSQLExceptionTranslator for the DataSource + * is created; else, a SQLStateSQLExceptionTranslator as fallback. + * @param sessionFactory the SessionFactory to create the translator for + * @return the SQLExceptionTranslator + * @see #getDataSource + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public static SQLExceptionTranslator newJdbcExceptionTranslator(SessionFactory sessionFactory) { + DataSource ds = getDataSource(sessionFactory); + if (ds != null) { + return new SQLErrorCodeSQLExceptionTranslator(ds); + } + return new SQLStateSQLExceptionTranslator(); + } + + /** + * Try to retrieve the JTA TransactionManager from the given SessionFactory + * and/or Session. Check the passed-in SessionFactory for implementing + * SessionFactoryImplementor (the usual case), falling back to the + * SessionFactory reference that the Session itself carries. + * @param sessionFactory Hibernate SessionFactory + * @param session Hibernate Session (can also be null) + * @return the JTA TransactionManager, if any + * @see javax.transaction.TransactionManager + * @see SessionFactoryImplementor#getTransactionManager + * @see Session#getSessionFactory + * @see org.hibernate.impl.SessionFactoryImpl + */ + public static TransactionManager getJtaTransactionManager(SessionFactory sessionFactory, Session session) { + SessionFactoryImplementor sessionFactoryImpl = null; + if (sessionFactory instanceof SessionFactoryImplementor) { + sessionFactoryImpl = ((SessionFactoryImplementor) sessionFactory); + } + else if (session != null) { + SessionFactory internalFactory = session.getSessionFactory(); + if (internalFactory instanceof SessionFactoryImplementor) { + sessionFactoryImpl = (SessionFactoryImplementor) internalFactory; + } + } + return (sessionFactoryImpl != null ? sessionFactoryImpl.getTransactionManager() : null); + } + + + /** + * Get a Hibernate Session for the given SessionFactory. Is aware of and will + * return any existing corresponding Session bound to the current thread, for + * example when using {@link HibernateTransactionManager}. Will create a new + * Session otherwise, if "allowCreate" is true. + *

This is the getSession method used by typical data access code, + * in combination with releaseSession called when done with + * the Session. Note that HibernateTemplate allows to write data access code + * without caring about such resource handling. + * @param sessionFactory Hibernate SessionFactory to create the session with + * @param allowCreate whether a non-transactional Session should be created + * when no transactional Session can be found for the current thread + * @return the Hibernate Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and + * "allowCreate" is false + * @see #getSession(SessionFactory, Interceptor, SQLExceptionTranslator) + * @see #releaseSession + * @see HibernateTemplate + */ + public static Session getSession(SessionFactory sessionFactory, boolean allowCreate) + throws DataAccessResourceFailureException, IllegalStateException { + + try { + return doGetSession(sessionFactory, null, null, allowCreate); + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); + } + } + + /** + * Get a Hibernate Session for the given SessionFactory. Is aware of and will + * return any existing corresponding Session bound to the current thread, for + * example when using {@link HibernateTransactionManager}. Will always create + * a new Session otherwise. + *

Supports setting a Session-level Hibernate entity interceptor that allows + * to inspect and change property values before writing to and reading from the + * database. Such an interceptor can also be set at the SessionFactory level + * (i.e. on LocalSessionFactoryBean), on HibernateTransactionManager, or on + * HibernateInterceptor/HibernateTemplate. + * @param sessionFactory Hibernate SessionFactory to create the session with + * @param entityInterceptor Hibernate entity interceptor, or null if none + * @param jdbcExceptionTranslator SQLExcepionTranslator to use for flushing the + * Session on transaction synchronization (may be null; only used + * when actually registering a transaction synchronization) + * @return the Hibernate Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @see LocalSessionFactoryBean#setEntityInterceptor + * @see HibernateInterceptor#setEntityInterceptor + * @see HibernateTemplate#setEntityInterceptor + */ + public static Session getSession( + SessionFactory sessionFactory, Interceptor entityInterceptor, + SQLExceptionTranslator jdbcExceptionTranslator) throws DataAccessResourceFailureException { + + try { + return doGetSession(sessionFactory, entityInterceptor, jdbcExceptionTranslator, true); + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); + } + } + + /** + * Get a Hibernate Session for the given SessionFactory. Is aware of and will + * return any existing corresponding Session bound to the current thread, for + * example when using {@link HibernateTransactionManager}. Will create a new + * Session otherwise, if "allowCreate" is true. + *

Throws the original HibernateException, in contrast to {@link #getSession}. + * @param sessionFactory Hibernate SessionFactory to create the session with + * @param allowCreate whether a non-transactional Session should be created + * when no transactional Session can be found for the current thread + * @return the Hibernate Session + * @throws HibernateException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and allowCreate false + */ + public static Session doGetSession(SessionFactory sessionFactory, boolean allowCreate) + throws HibernateException, IllegalStateException { + + return doGetSession(sessionFactory, null, null, allowCreate); + } + + /** + * Get a Hibernate Session for the given SessionFactory. Is aware of and will + * return any existing corresponding Session bound to the current thread, for + * example when using {@link HibernateTransactionManager}. Will create a new + * Session otherwise, if "allowCreate" is true. + *

Same as {@link #getSession}, but throwing the original HibernateException. + * @param sessionFactory Hibernate SessionFactory to create the session with + * @param entityInterceptor Hibernate entity interceptor, or null if none + * @param jdbcExceptionTranslator SQLExcepionTranslator to use for flushing the + * Session on transaction synchronization (may be null) + * @param allowCreate whether a non-transactional Session should be created + * when no transactional Session can be found for the current thread + * @return the Hibernate Session + * @throws HibernateException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and + * "allowCreate" is false + */ + private static Session doGetSession( + SessionFactory sessionFactory, Interceptor entityInterceptor, + SQLExceptionTranslator jdbcExceptionTranslator, boolean allowCreate) + throws HibernateException, IllegalStateException { + + Assert.notNull(sessionFactory, "No SessionFactory specified"); + + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && !sessionHolder.isEmpty()) { + // pre-bound Hibernate Session + Session session = null; + if (TransactionSynchronizationManager.isSynchronizationActive() && + sessionHolder.doesNotHoldNonDefaultSession()) { + // Spring transaction management is active -> + // register pre-bound Session with it for transactional flushing. + session = sessionHolder.getValidatedSession(); + if (session != null && !sessionHolder.isSynchronizedWithTransaction()) { + logger.debug("Registering Spring transaction synchronization for existing Hibernate Session"); + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(sessionHolder, sessionFactory, jdbcExceptionTranslator, false)); + sessionHolder.setSynchronizedWithTransaction(true); + // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session + // with FlushMode.NEVER, which needs to allow flushing within the transaction. + FlushMode flushMode = session.getFlushMode(); + if (flushMode.lessThan(FlushMode.COMMIT) && + !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setFlushMode(FlushMode.AUTO); + sessionHolder.setPreviousFlushMode(flushMode); + } + } + } + else { + // No Spring transaction management active -> try JTA transaction synchronization. + session = getJtaSynchronizedSession(sessionHolder, sessionFactory, jdbcExceptionTranslator); + } + if (session != null) { + return session; + } + } + + logger.debug("Opening Hibernate Session"); + Session session = (entityInterceptor != null ? + sessionFactory.openSession(entityInterceptor) : sessionFactory.openSession()); + + // Use same Session for further Hibernate actions within the transaction. + // Thread object will get removed by synchronization at transaction completion. + if (TransactionSynchronizationManager.isSynchronizationActive()) { + // We're within a Spring-managed transaction, possibly from JtaTransactionManager. + logger.debug("Registering Spring transaction synchronization for new Hibernate Session"); + SessionHolder holderToUse = sessionHolder; + if (holderToUse == null) { + holderToUse = new SessionHolder(session); + } + else { + holderToUse.addSession(session); + } + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setFlushMode(FlushMode.NEVER); + } + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(holderToUse, sessionFactory, jdbcExceptionTranslator, true)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != sessionHolder) { + TransactionSynchronizationManager.bindResource(sessionFactory, holderToUse); + } + } + else { + // No Spring transaction management active -> try JTA transaction synchronization. + registerJtaSynchronization(session, sessionFactory, jdbcExceptionTranslator, sessionHolder); + } + + // Check whether we are allowed to return the Session. + if (!allowCreate && !isSessionTransactional(session, sessionFactory)) { + closeSession(session); + throw new IllegalStateException("No Hibernate Session bound to thread, " + + "and configuration does not allow creation of non-transactional one here"); + } + + return session; + } + + /** + * Retrieve a Session from the given SessionHolder, potentially from a + * JTA transaction synchronization. + * @param sessionHolder the SessionHolder to check + * @param sessionFactory the SessionFactory to get the JTA TransactionManager from + * @param jdbcExceptionTranslator SQLExcepionTranslator to use for flushing the + * Session on transaction synchronization (may be null) + * @return the associated Session, if any + * @throws DataAccessResourceFailureException if the Session couldn't be created + */ + private static Session getJtaSynchronizedSession( + SessionHolder sessionHolder, SessionFactory sessionFactory, + SQLExceptionTranslator jdbcExceptionTranslator) throws DataAccessResourceFailureException { + + // JTA synchronization is only possible with a javax.transaction.TransactionManager. + // We'll check the Hibernate SessionFactory: If a TransactionManagerLookup is specified + // in Hibernate configuration, it will contain a TransactionManager reference. + TransactionManager jtaTm = getJtaTransactionManager(sessionFactory, sessionHolder.getAnySession()); + if (jtaTm != null) { + // Check whether JTA transaction management is active -> + // fetch pre-bound Session for the current JTA transaction, if any. + // (just necessary for JTA transaction suspension, with an individual + // Hibernate Session per currently active/suspended transaction) + try { + // Look for transaction-specific Session. + Transaction jtaTx = jtaTm.getTransaction(); + if (jtaTx != null) { + int jtaStatus = jtaTx.getStatus(); + if (jtaStatus == Status.STATUS_ACTIVE || jtaStatus == Status.STATUS_MARKED_ROLLBACK) { + Session session = sessionHolder.getValidatedSession(jtaTx); + if (session == null && !sessionHolder.isSynchronizedWithTransaction()) { + // No transaction-specific Session found: If not already marked as + // synchronized with transaction, register the default thread-bound + // Session as JTA-transactional. If there is no default Session, + // we're a new inner JTA transaction with an outer one being suspended: + // In that case, we'll return null to trigger opening of a new Session. + session = sessionHolder.getValidatedSession(); + if (session != null) { + logger.debug("Registering JTA transaction synchronization for existing Hibernate Session"); + sessionHolder.addSession(jtaTx, session); + jtaTx.registerSynchronization( + new SpringJtaSynchronizationAdapter( + new SpringSessionSynchronization(sessionHolder, sessionFactory, jdbcExceptionTranslator, false), + jtaTm)); + sessionHolder.setSynchronizedWithTransaction(true); + // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session + // with FlushMode.NEVER, which needs to allow flushing within the transaction. + FlushMode flushMode = session.getFlushMode(); + if (flushMode.lessThan(FlushMode.COMMIT)) { + session.setFlushMode(FlushMode.AUTO); + sessionHolder.setPreviousFlushMode(flushMode); + } + } + } + return session; + } + } + // No transaction active -> simply return default thread-bound Session, if any + // (possibly from OpenSessionInViewFilter/Interceptor). + return sessionHolder.getValidatedSession(); + } + catch (Throwable ex) { + throw new DataAccessResourceFailureException("Could not check JTA transaction", ex); + } + } + else { + // No JTA TransactionManager -> simply return default thread-bound Session, if any + // (possibly from OpenSessionInViewFilter/Interceptor). + return sessionHolder.getValidatedSession(); + } + } + + /** + * Register a JTA synchronization for the given Session, if any. + * @param sessionHolder the existing thread-bound SessionHolder, if any + * @param session the Session to register + * @param sessionFactory the SessionFactory that the Session was created with + * @param jdbcExceptionTranslator SQLExcepionTranslator to use for flushing the + * Session on transaction synchronization (may be null) + */ + private static void registerJtaSynchronization(Session session, SessionFactory sessionFactory, + SQLExceptionTranslator jdbcExceptionTranslator, SessionHolder sessionHolder) { + + // JTA synchronization is only possible with a javax.transaction.TransactionManager. + // We'll check the Hibernate SessionFactory: If a TransactionManagerLookup is specified + // in Hibernate configuration, it will contain a TransactionManager reference. + TransactionManager jtaTm = getJtaTransactionManager(sessionFactory, session); + if (jtaTm != null) { + try { + Transaction jtaTx = jtaTm.getTransaction(); + if (jtaTx != null) { + int jtaStatus = jtaTx.getStatus(); + if (jtaStatus == Status.STATUS_ACTIVE || jtaStatus == Status.STATUS_MARKED_ROLLBACK) { + logger.debug("Registering JTA transaction synchronization for new Hibernate Session"); + SessionHolder holderToUse = sessionHolder; + // Register JTA Transaction with existing SessionHolder. + // Create a new SessionHolder if none existed before. + if (holderToUse == null) { + holderToUse = new SessionHolder(jtaTx, session); + } + else { + holderToUse.addSession(jtaTx, session); + } + jtaTx.registerSynchronization( + new SpringJtaSynchronizationAdapter( + new SpringSessionSynchronization(holderToUse, sessionFactory, jdbcExceptionTranslator, true), + jtaTm)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != sessionHolder) { + TransactionSynchronizationManager.bindResource(sessionFactory, holderToUse); + } + } + } + } + catch (Throwable ex) { + throw new DataAccessResourceFailureException( + "Could not register synchronization with JTA TransactionManager", ex); + } + } + } + + + /** + * Get a new Hibernate Session from the given SessionFactory. + * Will return a new Session even if there already is a pre-bound + * Session for the given SessionFactory. + *

Within a transaction, this method will create a new Session + * that shares the transaction's JDBC Connection. More specifically, + * it will use the same JDBC Connection as the pre-bound Hibernate Session. + * @param sessionFactory Hibernate SessionFactory to create the session with + * @return the new Session + */ + public static Session getNewSession(SessionFactory sessionFactory) { + return getNewSession(sessionFactory, null); + } + + /** + * Get a new Hibernate Session from the given SessionFactory. + * Will return a new Session even if there already is a pre-bound + * Session for the given SessionFactory. + *

Within a transaction, this method will create a new Session + * that shares the transaction's JDBC Connection. More specifically, + * it will use the same JDBC Connection as the pre-bound Hibernate Session. + * @param sessionFactory Hibernate SessionFactory to create the session with + * @param entityInterceptor Hibernate entity interceptor, or null if none + * @return the new Session + */ + public static Session getNewSession(SessionFactory sessionFactory, Interceptor entityInterceptor) { + Assert.notNull(sessionFactory, "No SessionFactory specified"); + + try { + SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && !sessionHolder.isEmpty()) { + if (entityInterceptor != null) { + return sessionFactory.openSession(sessionHolder.getAnySession().connection(), entityInterceptor); + } + else { + return sessionFactory.openSession(sessionHolder.getAnySession().connection()); + } + } + else { + if (entityInterceptor != null) { + return sessionFactory.openSession(entityInterceptor); + } + else { + return sessionFactory.openSession(); + } + } + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); + } + } + + + /** + * Stringify the given Session for debug logging. + * Returns output equivalent to Object.toString(): + * the fully qualified class name + "@" + the identity hash code. + *

The sole reason why this is necessary is because Hibernate3's + * Session.toString() implementation is broken (and won't be fixed): + * it logs the toString representation of all persistent objects in the Session, + * which might lead to ConcurrentModificationExceptions if the persistent objects + * in turn refer to the Session (for example, for lazy loading). + * @param session the Hibernate Session to stringify + * @return the String representation of the given Session + */ + public static String toString(Session session) { + return session.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(session)); + } + + /** + * Return whether there is a transactional Hibernate Session for the current thread, + * that is, a Session bound to the current thread by Spring's transaction facilities. + * @param sessionFactory Hibernate SessionFactory to check (may be null) + * @return whether there is a transactional Session for current thread + */ + public static boolean hasTransactionalSession(SessionFactory sessionFactory) { + if (sessionFactory == null) { + return false; + } + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + return (sessionHolder != null && !sessionHolder.isEmpty()); + } + + /** + * Return whether the given Hibernate Session is transactional, that is, + * bound to the current thread by Spring's transaction facilities. + * @param session the Hibernate Session to check + * @param sessionFactory Hibernate SessionFactory that the Session was created with + * (may be null) + * @return whether the Session is transactional + */ + public static boolean isSessionTransactional(Session session, SessionFactory sessionFactory) { + if (sessionFactory == null) { + return false; + } + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + return (sessionHolder != null && sessionHolder.containsSession(session)); + } + + /** + * Apply the current transaction timeout, if any, to the given + * Hibernate Query object. + * @param query the Hibernate Query object + * @param sessionFactory Hibernate SessionFactory that the Query was created for + * (may be null) + * @see org.hibernate.Query#setTimeout + */ + public static void applyTransactionTimeout(Query query, SessionFactory sessionFactory) { + Assert.notNull(query, "No Query object specified"); + if (sessionFactory != null) { + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + query.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + } + + /** + * Apply the current transaction timeout, if any, to the given + * Hibernate Criteria object. + * @param criteria the Hibernate Criteria object + * @param sessionFactory Hibernate SessionFactory that the Criteria was created for + * @see org.hibernate.Criteria#setTimeout + */ + public static void applyTransactionTimeout(Criteria criteria, SessionFactory sessionFactory) { + Assert.notNull(criteria, "No Criteria object specified"); + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + criteria.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Convert the given HibernateException to an appropriate exception + * from the org.springframework.dao hierarchy. + * @param ex HibernateException that occured + * @return the corresponding DataAccessException instance + * @see HibernateAccessor#convertHibernateAccessException + * @see HibernateTransactionManager#convertHibernateAccessException + */ + public static DataAccessException convertHibernateAccessException(HibernateException ex) { + if (ex instanceof JDBCConnectionException) { + return new DataAccessResourceFailureException(ex.getMessage(), ex); + } + if (ex instanceof SQLGrammarException) { + return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); + } + if (ex instanceof LockAcquisitionException) { + return new CannotAcquireLockException(ex.getMessage(), ex); + } + if (ex instanceof ConstraintViolationException) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + if (ex instanceof DataException) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + if (ex instanceof JDBCException) { + return new HibernateJdbcException((JDBCException) ex); + } + if (ex instanceof PropertyValueException) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + if (ex instanceof PersistentObjectException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof TransientObjectException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof ObjectDeletedException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof QueryException) { + return new HibernateQueryException((QueryException) ex); + } + if (ex instanceof UnresolvableObjectException) { + return new HibernateObjectRetrievalFailureException((UnresolvableObjectException) ex); + } + if (ex instanceof WrongClassException) { + return new HibernateObjectRetrievalFailureException((WrongClassException) ex); + } + if (ex instanceof NonUniqueResultException) { + return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1); + } + if (ex instanceof StaleObjectStateException) { + return new HibernateOptimisticLockingFailureException((StaleObjectStateException) ex); + } + if (ex instanceof StaleStateException) { + return new HibernateOptimisticLockingFailureException((StaleStateException) ex); + } + + // fallback + return new HibernateSystemException(ex); + } + + + /** + * Determine whether deferred close is active for the current thread + * and the given SessionFactory. + * @param sessionFactory the Hibernate SessionFactory to check + * @return whether deferred close is active + */ + public static boolean isDeferredCloseActive(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, "No SessionFactory specified"); + Map holderMap = (Map) deferredCloseHolder.get(); + return (holderMap != null && holderMap.containsKey(sessionFactory)); + } + + /** + * Initialize deferred close for the current thread and the given SessionFactory. + * Sessions will not be actually closed on close calls then, but rather at a + * {@link #processDeferredClose} call at a finishing point (like request completion). + *

Used by {@link org.springframework.orm.hibernate3.support.OpenSessionInViewFilter} + * and {@link org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor} + * when not configured for a single session. + * @param sessionFactory the Hibernate SessionFactory to initialize deferred close for + * @see #processDeferredClose + * @see #releaseSession + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewFilter#setSingleSession + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor#setSingleSession + */ + public static void initDeferredClose(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, "No SessionFactory specified"); + logger.debug("Initializing deferred close of Hibernate Sessions"); + Map holderMap = (Map) deferredCloseHolder.get(); + if (holderMap == null) { + holderMap = new HashMap(); + deferredCloseHolder.set(holderMap); + } + holderMap.put(sessionFactory, new LinkedHashSet(4)); + } + + /** + * Process all Hibernate Sessions that have been registered for deferred close + * for the given SessionFactory. + * @param sessionFactory the Hibernate SessionFactory to process deferred close for + * @see #initDeferredClose + * @see #releaseSession + */ + public static void processDeferredClose(SessionFactory sessionFactory) { + Assert.notNull(sessionFactory, "No SessionFactory specified"); + + Map holderMap = (Map) deferredCloseHolder.get(); + if (holderMap == null || !holderMap.containsKey(sessionFactory)) { + throw new IllegalStateException("Deferred close not active for SessionFactory [" + sessionFactory + "]"); + } + + logger.debug("Processing deferred close of Hibernate Sessions"); + Set sessions = (Set) holderMap.remove(sessionFactory); + for (Iterator it = sessions.iterator(); it.hasNext();) { + closeSession((Session) it.next()); + } + + if (holderMap.isEmpty()) { + deferredCloseHolder.set(null); + } + } + + /** + * Close the given Session, created via the given factory, + * if it is not managed externally (i.e. not bound to the thread). + * @param session the Hibernate Session to close (may be null) + * @param sessionFactory Hibernate SessionFactory that the Session was created with + * (may be null) + */ + public static void releaseSession(Session session, SessionFactory sessionFactory) { + if (session == null) { + return; + } + // Only close non-transactional Sessions. + if (!isSessionTransactional(session, sessionFactory)) { + closeSessionOrRegisterDeferredClose(session, sessionFactory); + } + } + + /** + * Close the given Session or register it for deferred close. + * @param session the Hibernate Session to close + * @param sessionFactory Hibernate SessionFactory that the Session was created with + * (may be null) + * @see #initDeferredClose + * @see #processDeferredClose + */ + static void closeSessionOrRegisterDeferredClose(Session session, SessionFactory sessionFactory) { + Map holderMap = (Map) deferredCloseHolder.get(); + if (holderMap != null && sessionFactory != null && holderMap.containsKey(sessionFactory)) { + logger.debug("Registering Hibernate Session for deferred close"); + // Switch Session to FlushMode.NEVER for remaining lifetime. + session.setFlushMode(FlushMode.NEVER); + Set sessions = (Set) holderMap.get(sessionFactory); + sessions.add(session); + } + else { + closeSession(session); + } + } + + /** + * Perform actual closing of the Hibernate Session, + * catching and logging any cleanup exceptions thrown. + * @param session the Hibernate Session to close (may be null) + * @see org.hibernate.Session#close() + */ + public static void closeSession(Session session) { + if (session != null) { + logger.debug("Closing Hibernate Session"); + try { + session.close(); + } + catch (HibernateException ex) { + logger.debug("Could not close Hibernate Session", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing Hibernate Session", ex); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SessionHolder.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SessionHolder.java new file mode 100644 index 00000000000..b526ff38e32 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SessionHolder.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.Transaction; + +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Session holder, wrapping a Hibernate Session and a Hibernate Transaction. + * HibernateTransactionManager binds instances of this class to the thread, + * for a given SessionFactory. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 1.2 + * @see HibernateTransactionManager + * @see SessionFactoryUtils + */ +public class SessionHolder extends ResourceHolderSupport { + + private static final Object DEFAULT_KEY = new Object(); + + /** + * This Map needs to be synchronized because there might be multi-threaded + * access in the case of JTA with remote transaction propagation. + */ + private final Map sessionMap = Collections.synchronizedMap(new HashMap(1)); + + private Transaction transaction; + + private FlushMode previousFlushMode; + + + public SessionHolder(Session session) { + addSession(session); + } + + public SessionHolder(Object key, Session session) { + addSession(key, session); + } + + + public Session getSession() { + return getSession(DEFAULT_KEY); + } + + public Session getSession(Object key) { + return (Session) this.sessionMap.get(key); + } + + public Session getValidatedSession() { + return getValidatedSession(DEFAULT_KEY); + } + + public Session getValidatedSession(Object key) { + Session session = (Session) this.sessionMap.get(key); + // Check for dangling Session that's around but already closed. + // Effectively an assertion: that should never happen in practice. + // We'll seamlessly remove the Session here, to not let it cause + // any side effects. + if (session != null && !session.isOpen()) { + this.sessionMap.remove(key); + session = null; + } + return session; + } + + public Session getAnySession() { + synchronized (this.sessionMap) { + if (!this.sessionMap.isEmpty()) { + return (Session) this.sessionMap.values().iterator().next(); + } + return null; + } + } + + public void addSession(Session session) { + addSession(DEFAULT_KEY, session); + } + + public void addSession(Object key, Session session) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(session, "Session must not be null"); + this.sessionMap.put(key, session); + } + + public Session removeSession(Object key) { + return (Session) this.sessionMap.remove(key); + } + + public boolean containsSession(Session session) { + return this.sessionMap.containsValue(session); + } + + public boolean isEmpty() { + return this.sessionMap.isEmpty(); + } + + public boolean doesNotHoldNonDefaultSession() { + synchronized (this.sessionMap) { + return this.sessionMap.isEmpty() || + (this.sessionMap.size() == 1 && this.sessionMap.containsKey(DEFAULT_KEY)); + } + } + + + public void setTransaction(Transaction transaction) { + this.transaction = transaction; + } + + public Transaction getTransaction() { + return this.transaction; + } + + public void setPreviousFlushMode(FlushMode previousFlushMode) { + this.previousFlushMode = previousFlushMode; + } + + public FlushMode getPreviousFlushMode() { + return this.previousFlushMode; + } + + + public void clear() { + super.clear(); + this.transaction = null; + this.previousFlushMode = null; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringSessionContext.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringSessionContext.java new file mode 100644 index 00000000000..e914c19fe4b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringSessionContext.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import org.hibernate.HibernateException; +import org.hibernate.classic.Session; +import org.hibernate.context.CurrentSessionContext; +import org.hibernate.engine.SessionFactoryImplementor; + +/** + * Implementation of Hibernate 3.1's CurrentSessionContext interface + * that delegates to Spring's SessionFactoryUtils for providing a + * Spring-managed current Session. + * + *

Used by Spring's {@link LocalSessionFactoryBean} when told to expose a + * transaction-aware SessionFactory. This is the default as of Spring 2.5. + * + *

This CurrentSessionContext implementation can also be specified in custom + * SessionFactory setup through the "hibernate.current_session_context_class" + * property, with the fully qualified name of this class as value. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SessionFactoryUtils#doGetSession + * @see LocalSessionFactoryBean#setExposeTransactionAwareSessionFactory + */ +public class SpringSessionContext implements CurrentSessionContext { + + private final SessionFactoryImplementor sessionFactory; + + + /** + * Create a new SpringSessionContext for the given Hibernate SessionFactory. + * @param sessionFactory the SessionFactory to provide current Sessions for + */ + public SpringSessionContext(SessionFactoryImplementor sessionFactory) { + this.sessionFactory = sessionFactory; + } + + + /** + * Retrieve the Spring-managed Session for the current thread, if any. + */ + public Session currentSession() throws HibernateException { + try { + return (org.hibernate.classic.Session) SessionFactoryUtils.doGetSession(this.sessionFactory, false); + } + catch (IllegalStateException ex) { + throw new HibernateException(ex.getMessage()); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringSessionSynchronization.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringSessionSynchronization.java new file mode 100644 index 00000000000..3b58cd7cf21 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringSessionSynchronization.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.engine.SessionImplementor; + +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Callback for resource cleanup at the end of a Spring-managed JTA transaction, + * that is, when participating in a JtaTransactionManager transaction. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +class SpringSessionSynchronization implements TransactionSynchronization, Ordered { + + private final SessionHolder sessionHolder; + + private final SessionFactory sessionFactory; + + private final SQLExceptionTranslator jdbcExceptionTranslator; + + private final boolean newSession; + + /** + * Whether Hibernate has a looked-up JTA TransactionManager that it will + * automatically register CacheSynchronizations with on Session connect. + */ + private boolean hibernateTransactionCompletion = false; + + private Transaction jtaTransaction; + + private boolean holderActive = true; + + + public SpringSessionSynchronization( + SessionHolder sessionHolder, SessionFactory sessionFactory, + SQLExceptionTranslator jdbcExceptionTranslator, boolean newSession) { + + this.sessionHolder = sessionHolder; + this.sessionFactory = sessionFactory; + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + this.newSession = newSession; + + // Check whether the SessionFactory has a JTA TransactionManager. + TransactionManager jtaTm = + SessionFactoryUtils.getJtaTransactionManager(sessionFactory, sessionHolder.getAnySession()); + if (jtaTm != null) { + this.hibernateTransactionCompletion = true; + // Fetch current JTA Transaction object + // (just necessary for JTA transaction suspension, with an individual + // Hibernate Session per currently active/suspended transaction). + try { + this.jtaTransaction = jtaTm.getTransaction(); + } + catch (SystemException ex) { + throw new DataAccessResourceFailureException("Could not access JTA transaction", ex); + } + } + } + + /** + * Check whether there is a Hibernate Session for the current JTA + * transaction. Else, fall back to the default thread-bound Session. + */ + private Session getCurrentSession() { + Session session = null; + if (this.jtaTransaction != null) { + session = this.sessionHolder.getSession(this.jtaTransaction); + } + if (session == null) { + session = this.sessionHolder.getSession(); + } + return session; + } + + + public int getOrder() { + return SessionFactoryUtils.SESSION_SYNCHRONIZATION_ORDER; + } + + public void suspend() { + if (this.holderActive) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); + // Eagerly disconnect the Session here, to make release mode "on_close" work on JBoss. + getCurrentSession().disconnect(); + } + } + + public void resume() { + if (this.holderActive) { + TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); + } + } + + public void beforeCommit(boolean readOnly) throws DataAccessException { + if (!readOnly) { + Session session = getCurrentSession(); + // Read-write transaction -> flush the Hibernate Session. + // Further check: only flush when not FlushMode.NEVER/MANUAL. + if (!session.getFlushMode().lessThan(FlushMode.COMMIT)) { + try { + SessionFactoryUtils.logger.debug("Flushing Hibernate Session on transaction synchronization"); + session.flush(); + } + catch (HibernateException ex) { + if (this.jdbcExceptionTranslator != null && ex instanceof JDBCException) { + JDBCException jdbcEx = (JDBCException) ex; + throw this.jdbcExceptionTranslator.translate( + "Hibernate flushing: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); + } + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + } + } + + public void beforeCompletion() { + if (this.jtaTransaction != null) { + // Typically in case of a suspended JTA transaction: + // Remove the Session for the current JTA transaction, but keep the holder. + Session session = this.sessionHolder.removeSession(this.jtaTransaction); + if (session != null) { + if (this.sessionHolder.isEmpty()) { + // No Sessions for JTA transactions bound anymore -> could remove it. + TransactionSynchronizationManager.unbindResourceIfPossible(this.sessionFactory); + this.holderActive = false; + } + // Do not close a pre-bound Session. In that case, we'll find the + // transaction-specific Session the same as the default Session. + if (session != this.sessionHolder.getSession()) { + SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, this.sessionFactory); + } + else { + if (this.sessionHolder.getPreviousFlushMode() != null) { + // In case of pre-bound Session, restore previous flush mode. + session.setFlushMode(this.sessionHolder.getPreviousFlushMode()); + } + // Eagerly disconnect the Session here, to make release mode "on_close" work nicely. + session.disconnect(); + } + return; + } + } + // We'll only get here if there was no specific JTA transaction to handle. + if (this.newSession) { + // Default behavior: unbind and close the thread-bound Hibernate Session. + TransactionSynchronizationManager.unbindResource(this.sessionFactory); + this.holderActive = false; + if (this.hibernateTransactionCompletion) { + // Close the Hibernate Session here in case of a Hibernate TransactionManagerLookup: + // Hibernate will automatically defer the actual closing until JTA transaction completion. + // Else, the Session will be closed in the afterCompletion method, to provide the + // correct transaction status for releasing the Session's cache locks. + SessionFactoryUtils.closeSessionOrRegisterDeferredClose(this.sessionHolder.getSession(), this.sessionFactory); + } + } + else { + Session session = this.sessionHolder.getSession(); + if (this.sessionHolder.getPreviousFlushMode() != null) { + // In case of pre-bound Session, restore previous flush mode. + session.setFlushMode(this.sessionHolder.getPreviousFlushMode()); + } + if (this.hibernateTransactionCompletion) { + // Eagerly disconnect the Session here, to make release mode "on_close" work nicely. + // We know that this is appropriate if a TransactionManagerLookup has been specified. + session.disconnect(); + } + } + } + + public void afterCommit() { + } + + public void afterCompletion(int status) { + if (!this.hibernateTransactionCompletion || !this.newSession) { + // No Hibernate TransactionManagerLookup: apply afterTransactionCompletion callback. + // Always perform explicit afterTransactionCompletion callback for pre-bound Session, + // even with Hibernate TransactionManagerLookup (which only applies to new Sessions). + Session session = this.sessionHolder.getSession(); + // Provide correct transaction status for releasing the Session's cache locks, + // if possible. Else, closing will release all cache locks assuming a rollback. + if (session instanceof SessionImplementor) { + ((SessionImplementor) session).afterTransactionCompletion(status == STATUS_COMMITTED, null); + } + // Close the Hibernate Session here if necessary + // (closed in beforeCompletion in case of TransactionManagerLookup). + if (this.newSession) { + SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, this.sessionFactory); + } + else if (!this.hibernateTransactionCompletion) { + session.disconnect(); + } + } + if (!this.newSession && status != STATUS_COMMITTED) { + // Clear all pending inserts/updates/deletes in the Session. + // Necessary for pre-bound Sessions, to avoid inconsistent state. + this.sessionHolder.getSession().clear(); + } + if (this.sessionHolder.doesNotHoldNonDefaultSession()) { + this.sessionHolder.setSynchronizedWithTransaction(false); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringTransactionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringTransactionFactory.java new file mode 100644 index 00000000000..068d1f9e6b0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/SpringTransactionFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2008 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.hibernate3; + +import java.util.Properties; + +import org.hibernate.ConnectionReleaseMode; +import org.hibernate.Transaction; +import org.hibernate.jdbc.JDBCContext; +import org.hibernate.transaction.JDBCTransaction; +import org.hibernate.transaction.TransactionFactory; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Spring-aware implementation of the Hibernate TransactionFactory interface, aware of + * Spring-synchronized transactions (in particular Spring-managed JTA transactions) + * and asking for default release mode ON_CLOSE. Otherwise identical to Hibernate's + * default {@link org.hibernate.transaction.JDBCTransactionFactory} implementation. + * + * @author Juergen Hoeller + * @since 2.5.4 + * @see org.springframework.transaction.support.TransactionSynchronizationManager + * @see org.hibernate.transaction.JDBCTransactionFactory + */ +public class SpringTransactionFactory implements TransactionFactory { + + /** + * Sets connection release mode "on_close" as default. + *

This was the case for Hibernate 3.0; Hibernate 3.1 changed + * it to "auto" (i.e. "after_statement" or "after_transaction"). + * However, for Spring's resource management (in particular for + * HibernateTransactionManager), "on_close" is the better default. + */ + public ConnectionReleaseMode getDefaultReleaseMode() { + return ConnectionReleaseMode.ON_CLOSE; + } + + public Transaction createTransaction(JDBCContext jdbcContext, Context transactionContext) { + return new JDBCTransaction(jdbcContext, transactionContext); + } + + public void configure(Properties props) { + } + + public boolean isTransactionManagerRequired() { + return false; + } + + public boolean areCallbacksLocalToHibernateTransactions() { + return true; + } + + public boolean isTransactionInProgress( + JDBCContext jdbcContext, Context transactionContext, Transaction transaction) { + + return (transaction != null && transaction.isActive()) || + TransactionSynchronizationManager.isActualTransactionActive(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/TransactionAwareDataSourceConnectionProvider.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/TransactionAwareDataSourceConnectionProvider.java new file mode 100644 index 00000000000..838487a4489 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/TransactionAwareDataSourceConnectionProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2007 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.hibernate3; + +import javax.sql.DataSource; + +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; + +/** + * Subclass of LocalDataSourceConnectionProvider that returns a + * transaction-aware proxy for the exposed DataSource. Used if + * LocalSessionFactoryBean's "useTransactionAwareDataSource" flag is on. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocalSessionFactoryBean#setUseTransactionAwareDataSource + */ +public class TransactionAwareDataSourceConnectionProvider extends LocalDataSourceConnectionProvider { + + /** + * Return a TransactionAwareDataSourceProxy for the given DataSource, + * provided that it isn't a TransactionAwareDataSourceProxy already. + */ + protected DataSource getDataSourceToUse(DataSource originalDataSource) { + if (originalDataSource instanceof TransactionAwareDataSourceProxy) { + return originalDataSource; + } + return new TransactionAwareDataSourceProxy(originalDataSource); + } + + /** + * This implementation returns true: We can guarantee + * to receive the same Connection within a transaction, as we are + * exposing a TransactionAwareDataSourceProxy. + */ + public boolean supportsAggressiveRelease() { + return true; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/TypeDefinitionBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/TypeDefinitionBean.java new file mode 100644 index 00000000000..68140afdf89 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/TypeDefinitionBean.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2005 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.hibernate3; + +import java.util.Properties; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; + +/** + * Bean that encapsulates a Hibernate type definition. + * + *

Typically defined as inner bean within a LocalSessionFactoryBean + * definition, as list element for the "typeDefinitions" bean property. + * For example: + * + *

+ * <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
+ *   ...
+ *   <property name="typeDefinitions">
+ *     <list>
+ *       <bean class="org.springframework.orm.hibernate3.TypeDefinitionBean">
+ *         <property name="typeName" value="myType"/>
+ *         <property name="typeClass" value="mypackage.MyTypeClass"/>
+ *       </bean>
+ *     </list>
+ *   </property>
+ *   ...
+ * </bean>
+ * + * Alternatively, specify a bean id (or name) attribute for the inner bean, + * instead of the "typeName" property. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocalSessionFactoryBean#setTypeDefinitions(TypeDefinitionBean[]) + */ +public class TypeDefinitionBean implements BeanNameAware, InitializingBean { + + private String typeName; + + private String typeClass; + + private Properties parameters = new Properties(); + + + /** + * Set the name of the type. + * @see org.hibernate.cfg.Mappings#addTypeDef(String, String, java.util.Properties) + */ + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + /** + * Return the name of the type. + */ + public String getTypeName() { + return typeName; + } + + /** + * Set the type implementation class. + * @see org.hibernate.cfg.Mappings#addTypeDef(String, String, java.util.Properties) + */ + public void setTypeClass(String typeClass) { + this.typeClass = typeClass; + } + + /** + * Return the type implementation class. + */ + public String getTypeClass() { + return typeClass; + } + + /** + * Specify default parameters for the type. + * This only applies to parameterized types. + * @see org.hibernate.cfg.Mappings#addTypeDef(String, String, java.util.Properties) + * @see org.hibernate.usertype.ParameterizedType + */ + public void setParameters(Properties parameters) { + this.parameters = parameters; + } + + /** + * Return the default parameters for the type. + */ + public Properties getParameters() { + return parameters; + } + + + /** + * If no explicit type name has been specified, the bean name of + * the TypeDefinitionBean will be used. + * @see #setTypeName + */ + public void setBeanName(String name) { + if (this.typeName == null) { + this.typeName = name; + } + } + + public void afterPropertiesSet() { + if (this.typeName == null) { + throw new IllegalArgumentException("typeName is required"); + } + if (this.typeClass == null) { + throw new IllegalArgumentException("typeClass is required"); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/annotation/AnnotationSessionFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/annotation/AnnotationSessionFactoryBean.java new file mode 100644 index 00000000000..0560a595cb6 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/annotation/AnnotationSessionFactoryBean.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2008 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.hibernate3.annotation; + +import java.io.IOException; + +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.cfg.AnnotationConfiguration; +import org.hibernate.cfg.Configuration; + +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.orm.hibernate3.LocalSessionFactoryBean; +import org.springframework.util.ClassUtils; + +/** + * Subclass of Spring's standard LocalSessionFactoryBean for Hibernate, + * supporting JDK 1.5+ annotation metadata for mappings. + * + *

Note: This class requires Hibernate 3.2 or higher, with the + * Java Persistence API and the Hibernate Annotations add-on present. + * + *

Example for an AnnotationSessionFactoryBean bean definition: + * + *

+ * <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
+ *   <property name="dataSource" ref="dataSource"/>
+ *   <property name="annotatedClasses">
+ *     <list>
+ *       <value>test.package.Foo</value>
+ *       <value>test.package.Bar</value>
+ *     </list>
+ *   </property>
+ * </bean>
+ * + * Or when using classpath scanning for autodetection of entity classes: + * + *
+ * <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
+ *   <property name="dataSource" ref="dataSource"/>
+ *   <property name="packagesToScan" value="test.package"/>
+ * </bean>
+ * + * @author Juergen Hoeller + * @since 1.2.2 + * @see #setDataSource + * @see #setHibernateProperties + * @see #setAnnotatedClasses + * @see #setAnnotatedPackages + */ +public class AnnotationSessionFactoryBean extends LocalSessionFactoryBean implements ResourceLoaderAware { + + private static final String RESOURCE_PATTERN = "**/*.class"; + + + private Class[] annotatedClasses; + + private String[] annotatedPackages; + + private String[] packagesToScan; + + private TypeFilter[] entityTypeFilters = new TypeFilter[] { + new AnnotationTypeFilter(Entity.class, false), + new AnnotationTypeFilter(Embeddable.class, false), + new AnnotationTypeFilter(MappedSuperclass.class, false)}; + + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + + + public AnnotationSessionFactoryBean() { + setConfigurationClass(AnnotationConfiguration.class); + } + + + public void setConfigurationClass(Class configurationClass) { + if (configurationClass == null || !AnnotationConfiguration.class.isAssignableFrom(configurationClass)) { + throw new IllegalArgumentException( + "AnnotationSessionFactoryBean only supports AnnotationConfiguration or subclasses"); + } + super.setConfigurationClass(configurationClass); + } + + /** + * Specify annotated classes, for which mappings will be read from + * class-level JDK 1.5+ annotation metadata. + * @see org.hibernate.cfg.AnnotationConfiguration#addAnnotatedClass(Class) + */ + public void setAnnotatedClasses(Class[] annotatedClasses) { + this.annotatedClasses = annotatedClasses; + } + + /** + * Specify the names of annotated packages, for which package-level + * JDK 1.5+ annotation metadata will be read. + * @see org.hibernate.cfg.AnnotationConfiguration#addPackage(String) + */ + public void setAnnotatedPackages(String[] annotatedPackages) { + this.annotatedPackages = annotatedPackages; + } + + /** + * Set whether to use Spring-based scanning for entity classes in the classpath + * instead of listing annotated classes explicitly. + *

Default is none. Specify packages to search for autodetection of your entity + * classes in the classpath. This is analogous to Spring's component-scan feature + * ({@link org.springframework.context.annotation.ClassPathBeanDefinitionScanner}). + */ + public void setPackagesToScan(String[] packagesToScan) { + this.packagesToScan = packagesToScan; + } + + /** + * Specify custom type filters for Spring-based scanning for entity classes. + *

Default is to search all specified packages for classes annotated with + * @javax.persistence.Entity, @javax.persistence.Embeddable + * or @javax.persistence.MappedSuperclass. + * @see #setPackagesToScan + */ + public void setEntityTypeFilters(TypeFilter[] entityTypeFilters) { + this.entityTypeFilters = entityTypeFilters; + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = (resourceLoader != null ? + ResourcePatternUtils.getResourcePatternResolver(resourceLoader) : + new PathMatchingResourcePatternResolver()); + } + + + /** + * Reads metadata from annotated classes and packages into the + * AnnotationConfiguration instance. + */ + protected void postProcessMappings(Configuration config) throws HibernateException { + AnnotationConfiguration annConfig = (AnnotationConfiguration) config; + if (this.annotatedClasses != null) { + for (int i = 0; i < this.annotatedClasses.length; i++) { + annConfig.addAnnotatedClass(this.annotatedClasses[i]); + } + } + if (this.annotatedPackages != null) { + for (int i = 0; i < this.annotatedPackages.length; i++) { + annConfig.addPackage(this.annotatedPackages[i]); + } + } + scanPackages(annConfig); + } + + /** + * Perform Spring-based scanning for entity classes. + * @see #setPackagesToScan + */ + protected void scanPackages(AnnotationConfiguration config) { + if (this.packagesToScan != null) { + try { + for (String pkg : this.packagesToScan) { + String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN; + Resource[] resources = this.resourcePatternResolver.getResources(pattern); + MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); + for (Resource resource : resources) { + if (resource.isReadable()) { + MetadataReader reader = readerFactory.getMetadataReader(resource); + String className = reader.getClassMetadata().getClassName(); + if (matchesFilter(reader, readerFactory)) { + config.addAnnotatedClass(this.resourcePatternResolver.getClassLoader().loadClass(className)); + } + } + } + } + } + catch (IOException ex) { + throw new MappingException("Failed to scan classpath for unlisted classes", ex); + } + catch (ClassNotFoundException ex) { + throw new MappingException("Failed to load annotated classes from classpath", ex); + } + } + } + + /** + * Check whether any of the configured entity type filters matches + * the current class descriptor contained in the metadata reader. + */ + private boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException { + if (this.entityTypeFilters != null) { + for (TypeFilter filter : this.entityTypeFilters) { + if (filter.match(reader, readerFactory)) { + return true; + } + } + } + return false; + } + + + /** + * Delegates to {@link #postProcessAnnotationConfiguration}. + */ + protected final void postProcessConfiguration(Configuration config) throws HibernateException { + postProcessAnnotationConfiguration((AnnotationConfiguration) config); + } + + /** + * To be implemented by subclasses that want to to perform custom + * post-processing of the AnnotationConfiguration object after this + * FactoryBean performed its default initialization. + * @param config the current AnnotationConfiguration object + * @throws HibernateException in case of Hibernate initialization errors + */ + protected void postProcessAnnotationConfiguration(AnnotationConfiguration config) + throws HibernateException { + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/annotation/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/annotation/package.html new file mode 100644 index 00000000000..744987ba13a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/annotation/package.html @@ -0,0 +1,8 @@ + + + +Support package for the Hibernate3 Annotation add-on, +which supports EJB3-compliant JDK 1.5+ annotations for mappings. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/package.html new file mode 100644 index 00000000000..6d7e9e55018 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/package.html @@ -0,0 +1,16 @@ + + + +Package providing integration of +Hibernate3 +with Spring concepts. + +

Contains SessionFactory helper classes, a template plus callback +for Hibernate access, and an implementation of Spring's transaction SPI +for local Hibernate transactions. + +

This package supports Hibernate 3.x only. +See the org.springframework.orm.hibernate package for Hibernate 2.1 support. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/AbstractLobType.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/AbstractLobType.java new file mode 100644 index 00000000000..5ab3c68bbbe --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/AbstractLobType.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2006 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.hibernate3.support; + +import java.io.IOException; +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.transaction.Status; +import javax.transaction.TransactionManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.HibernateException; +import org.hibernate.usertype.UserType; +import org.hibernate.util.EqualsHelper; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.lob.JtaLobCreatorSynchronization; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.jdbc.support.lob.SpringLobCreatorSynchronization; +import org.springframework.jdbc.support.lob.LobCreatorUtils; +import org.springframework.orm.hibernate3.LocalSessionFactoryBean; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Abstract base class for Hibernate UserType implementations that map to LOBs. + * Retrieves the LobHandler to use from LocalSessionFactoryBean at config time. + * + *

For writing LOBs, either an active Spring transaction synchronization + * or an active JTA transaction (with "jtaTransactionManager" specified on + * LocalSessionFactoryBean or a Hibernate TransactionManagerLookup configured + * through the corresponding Hibernate property) is required. + * + *

Offers template methods for setting parameters and getting result values, + * passing in the LobHandler or LobCreator to use. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jdbc.support.lob.LobHandler + * @see org.springframework.jdbc.support.lob.LobCreator + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setLobHandler + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setJtaTransactionManager + */ +public abstract class AbstractLobType implements UserType { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final LobHandler lobHandler; + + private final TransactionManager jtaTransactionManager; + + + /** + * Constructor used by Hibernate: fetches config-time LobHandler and + * config-time JTA TransactionManager from LocalSessionFactoryBean. + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeLobHandler + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeTransactionManager + */ + protected AbstractLobType() { + this(LocalSessionFactoryBean.getConfigTimeLobHandler(), + LocalSessionFactoryBean.getConfigTimeTransactionManager()); + } + + /** + * Constructor used for testing: takes an explicit LobHandler + * and an explicit JTA TransactionManager (can be null). + */ + protected AbstractLobType(LobHandler lobHandler, TransactionManager jtaTransactionManager) { + this.lobHandler = lobHandler; + this.jtaTransactionManager = jtaTransactionManager; + } + + + /** + * This implementation returns false. + */ + public boolean isMutable() { + return false; + } + + /** + * This implementation delegates to the Hibernate EqualsHelper. + * @see org.hibernate.util.EqualsHelper#equals + */ + public boolean equals(Object x, Object y) throws HibernateException { + return EqualsHelper.equals(x, y); + } + + /** + * This implementation returns the hashCode of the given objectz. + */ + public int hashCode(Object x) throws HibernateException { + return x.hashCode(); + } + + /** + * This implementation returns the passed-in value as-is. + */ + public Object deepCopy(Object value) throws HibernateException { + return value; + } + + /** + * This implementation returns the passed-in value as-is. + */ + public Serializable disassemble(Object value) throws HibernateException { + return (Serializable) value; + } + + /** + * This implementation returns the passed-in value as-is. + */ + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return cached; + } + + /** + * This implementation returns the passed-in original as-is. + */ + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return original; + } + + + /** + * This implementation delegates to nullSafeGetInternal, + * passing in the LobHandler of this type. + * @see #nullSafeGetInternal + */ + public final Object nullSafeGet(ResultSet rs, String[] names, Object owner) + throws HibernateException, SQLException { + + if (this.lobHandler == null) { + throw new IllegalStateException("No LobHandler found for configuration - " + + "lobHandler property must be set on LocalSessionFactoryBean"); + } + + try { + return nullSafeGetInternal(rs, names, owner, this.lobHandler); + } + catch (IOException ex) { + throw new HibernateException("I/O errors during LOB access", ex); + } + } + + /** + * This implementation delegates to nullSafeSetInternal, + * passing in a transaction-synchronized LobCreator for the + * LobHandler of this type. + * @see #nullSafeSetInternal + */ + public final void nullSafeSet(PreparedStatement st, Object value, int index) + throws HibernateException, SQLException { + + if (this.lobHandler == null) { + throw new IllegalStateException("No LobHandler found for configuration - " + + "lobHandler property must be set on LocalSessionFactoryBean"); + } + + LobCreator lobCreator = this.lobHandler.getLobCreator(); + try { + nullSafeSetInternal(st, index, value, lobCreator); + } + catch (IOException ex) { + throw new HibernateException("I/O errors during LOB access", ex); + } + LobCreatorUtils.registerTransactionSynchronization(lobCreator, this.jtaTransactionManager); + } + + /** + * Template method to extract a value from the given result set. + * @param rs the ResultSet to extract from + * @param names the column names + * @param owner the containing entity + * @param lobHandler the LobHandler to use + * @return the extracted value + * @throws SQLException if thrown by JDBC methods + * @throws IOException if thrown by streaming methods + * @throws HibernateException in case of any other exceptions + */ + protected abstract Object nullSafeGetInternal( + ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException, IOException, HibernateException; + + /** + * Template method to set the given parameter value on the given statement. + * @param ps the PreparedStatement to set on + * @param index the statement parameter index + * @param value the value to set + * @param lobCreator the LobCreator to use + * @throws SQLException if thrown by JDBC methods + * @throws IOException if thrown by streaming methods + * @throws HibernateException in case of any other exceptions + */ + protected abstract void nullSafeSetInternal( + PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException, IOException, HibernateException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobByteArrayType.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobByteArrayType.java new file mode 100644 index 00000000000..d4f02dd7740 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobByteArrayType.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2005 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.hibernate3.support; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; + +import javax.transaction.TransactionManager; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * Hibernate UserType implementation for byte arrays that get mapped to BLOBs. + * Retrieves the LobHandler to use from LocalSessionFactoryBean at config time. + * + *

Can also be defined in generic Hibernate mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be BLOB: For databases like MySQL and MS SQL Server, any + * large enough binary type will work. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setLobHandler + */ +public class BlobByteArrayType extends AbstractLobType { + + /** + * Constructor used by Hibernate: fetches config-time LobHandler and + * config-time JTA TransactionManager from LocalSessionFactoryBean. + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeLobHandler + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeTransactionManager + */ + public BlobByteArrayType() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler + * and an explicit JTA TransactionManager (can be null). + */ + protected BlobByteArrayType(LobHandler lobHandler, TransactionManager jtaTransactionManager) { + super(lobHandler, jtaTransactionManager); + } + + public int[] sqlTypes() { + return new int[] {Types.BLOB}; + } + + public Class returnedClass() { + return byte[].class; + } + + public boolean isMutable() { + return true; + } + + public boolean equals(Object x, Object y) { + return (x == y) || + (x instanceof byte[] && y instanceof byte[] && Arrays.equals((byte[]) x, (byte[]) y)); + } + + public Object deepCopy(Object value) { + if (value == null) { + return null; + } + byte[] original = (byte[]) value; + byte[] copy = new byte[original.length]; + System.arraycopy(original, 0, copy, 0, original.length); + return copy; + } + + protected Object nullSafeGetInternal( + ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException { + + return lobHandler.getBlobAsBytes(rs, names[0]); + } + + protected void nullSafeSetInternal( + PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException { + + lobCreator.setBlobAsBytes(ps, index, (byte[]) value); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobSerializableType.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobSerializableType.java new file mode 100644 index 00000000000..a8f3865d0af --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobSerializableType.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2005 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.hibernate3.support; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import javax.transaction.TransactionManager; + +import org.hibernate.HibernateException; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * Hibernate UserType implementation for arbitrary objects that get serialized to BLOBs. + * Retrieves the LobHandler to use from LocalSessionFactoryBean at config time. + * + *

Can also be defined in generic Hibernate mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be BLOB: For databases like MySQL and MS SQL Server, any + * large enough binary type will work. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setLobHandler + */ +public class BlobSerializableType extends AbstractLobType { + + /** + * Initial size for ByteArrayOutputStreams used for serialization output. + *

If a serialized object is larger than these 1024 bytes, the size of + * the byte array used by the output stream will be doubled each time the + * limit is reached. + */ + private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 1024; + + /** + * Constructor used by Hibernate: fetches config-time LobHandler and + * config-time JTA TransactionManager from LocalSessionFactoryBean. + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeLobHandler + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeTransactionManager + */ + public BlobSerializableType() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler + * and an explicit JTA TransactionManager (can be null). + */ + protected BlobSerializableType(LobHandler lobHandler, TransactionManager jtaTransactionManager) { + super(lobHandler, jtaTransactionManager); + } + + public int[] sqlTypes() { + return new int[] {Types.BLOB}; + } + + public Class returnedClass() { + return Serializable.class; + } + + public boolean isMutable() { + return true; + } + + public Object deepCopy(Object value) throws HibernateException { + try { + // Write to new byte array to clone. + ByteArrayOutputStream baos = new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE); + ObjectOutputStream oos = new ObjectOutputStream(baos); + try { + oos.writeObject(value); + } + finally { + oos.close(); + } + + // Read it back and return a true copy. + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + try { + return ois.readObject(); + } + finally { + ois.close(); + } + } + catch (ClassNotFoundException ex) { + throw new HibernateException("Couldn't clone BLOB contents", ex); + } + catch (IOException ex) { + throw new HibernateException("Couldn't clone BLOB contents", ex); + } + } + + protected Object nullSafeGetInternal( + ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException, IOException, HibernateException { + + InputStream is = lobHandler.getBlobAsBinaryStream(rs, names[0]); + if (is != null) { + ObjectInputStream ois = new ObjectInputStream(is); + try { + return ois.readObject(); + } + catch (ClassNotFoundException ex) { + throw new HibernateException("Could not deserialize BLOB contents", ex); + } + finally { + ois.close(); + } + } + else { + return null; + } + } + + protected void nullSafeSetInternal( + PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException, IOException { + + if (value != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE); + ObjectOutputStream oos = new ObjectOutputStream(baos); + try { + oos.writeObject(value); + oos.flush(); + lobCreator.setBlobAsBytes(ps, index, baos.toByteArray()); + } + finally { + oos.close(); + } + } + else { + lobCreator.setBlobAsBytes(ps, index, null); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobStringType.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobStringType.java new file mode 100644 index 00000000000..a3206bb4d99 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/BlobStringType.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2006 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.hibernate3.support; + +import java.io.UnsupportedEncodingException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import javax.transaction.TransactionManager; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * Hibernate UserType implementation for Strings that get mapped to BLOBs. + * Retrieves the LobHandler to use from LocalSessionFactoryBean at config time. + * + *

This is intended for the (arguably unnatural, but still common) case + * where character data is stored in a binary LOB. This requires encoding + * and decoding the characters within this UserType; see the javadoc of the + * getCharacterEncoding() method. + * + *

Can also be defined in generic Hibernate mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be BLOB: For databases like MySQL and MS SQL Server, any + * large enough binary type will work. + * + * @author Juergen Hoeller + * @since 1.2.7 + * @see #getCharacterEncoding() + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setLobHandler + */ +public class BlobStringType extends AbstractLobType { + + /** + * Constructor used by Hibernate: fetches config-time LobHandler and + * config-time JTA TransactionManager from LocalSessionFactoryBean. + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeLobHandler + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeTransactionManager + */ + public BlobStringType() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler + * and an explicit JTA TransactionManager (can be null). + */ + protected BlobStringType(LobHandler lobHandler, TransactionManager jtaTransactionManager) { + super(lobHandler, jtaTransactionManager); + } + + public int[] sqlTypes() { + return new int[] {Types.BLOB}; + } + + public Class returnedClass() { + return String.class; + } + + protected Object nullSafeGetInternal( + ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException, UnsupportedEncodingException { + + byte[] bytes = lobHandler.getBlobAsBytes(rs, names[0]); + if (bytes != null) { + String encoding = getCharacterEncoding(); + return (encoding != null ? new String(bytes, encoding) : new String(bytes)); + } + else { + return null; + } + } + + protected void nullSafeSetInternal( + PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException, UnsupportedEncodingException { + + if (value != null) { + String str = (String) value; + String encoding = getCharacterEncoding(); + byte[] bytes = (encoding != null ? str.getBytes(encoding) : str.getBytes()); + lobCreator.setBlobAsBytes(ps, index, bytes); + } + else { + lobCreator.setBlobAsBytes(ps, index, null); + } + } + + /** + * Determine the character encoding to apply to the BLOB's bytes + * to turn them into a String. + *

Default is null, indicating to use the platform + * default encoding. To be overridden in subclasses for a specific + * encoding such as "ISO-8859-1" or "UTF-8". + * @return the character encoding to use, or null + * to use the platform default encoding + * @see java.lang.String#String(byte[], String) + * @see java.lang.String#getBytes(String) + */ + protected String getCharacterEncoding() { + return null; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/ClobStringType.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/ClobStringType.java new file mode 100644 index 00000000000..82409b2c7fb --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/ClobStringType.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2005 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.hibernate3.support; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import javax.transaction.TransactionManager; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * Hibernate UserType implementation for Strings that get mapped to CLOBs. + * Retrieves the LobHandler to use from LocalSessionFactoryBean at config time. + * + *

Particularly useful for storing Strings with more than 4000 characters in an + * Oracle database (only possible via CLOBs), in combination with OracleLobHandler. + * + *

Can also be defined in generic Hibernate mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be CLOB: For databases like MySQL and MS SQL Server, any + * large enough character type will work. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setLobHandler + */ +public class ClobStringType extends AbstractLobType { + + /** + * Constructor used by Hibernate: fetches config-time LobHandler and + * config-time JTA TransactionManager from LocalSessionFactoryBean. + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeLobHandler + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#getConfigTimeTransactionManager + */ + public ClobStringType() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler + * and an explicit JTA TransactionManager (can be null). + */ + protected ClobStringType(LobHandler lobHandler, TransactionManager jtaTransactionManager) { + super(lobHandler, jtaTransactionManager); + } + + public int[] sqlTypes() { + return new int[] {Types.CLOB}; + } + + public Class returnedClass() { + return String.class; + } + + protected Object nullSafeGetInternal( + ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException { + + return lobHandler.getClobAsString(rs, names[0]); + } + + protected void nullSafeSetInternal( + PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException { + + lobCreator.setClobAsString(ps, index, (String) value); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/HibernateDaoSupport.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/HibernateDaoSupport.java new file mode 100644 index 00000000000..199f05ae220 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/HibernateDaoSupport.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2008 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.hibernate3.support; + +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.support.DaoSupport; +import org.springframework.orm.hibernate3.HibernateTemplate; +import org.springframework.orm.hibernate3.SessionFactoryUtils; + +/** + * Convenient super class for Hibernate-based data access objects. + * + *

Requires a {@link org.hibernate.SessionFactory} to be set, providing a + * {@link org.springframework.orm.hibernate3.HibernateTemplate} based on it to + * subclasses through the {@link #getHibernateTemplate()} method. + * Can alternatively be initialized directly with a HibernateTemplate, + * in order to reuse the latter's settings such as the SessionFactory, + * exception translator, flush mode, etc. + * + *

This base class is mainly intended for HibernateTemplate usage but can + * also be used when working with a Hibernate Session directly, for example + * when relying on transactional Sessions. Convenience {@link #getSession} + * and {@link #releaseSession} methods are provided for that usage style. + * + *

This class will create its own HibernateTemplate instance if a SessionFactory + * is passed in. The "allowCreate" flag on that HibernateTemplate will be "true" + * by default. A custom HibernateTemplate instance can be used through overriding + * {@link #createHibernateTemplate}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setSessionFactory + * @see #getHibernateTemplate + * @see org.springframework.orm.hibernate3.HibernateTemplate + */ +public abstract class HibernateDaoSupport extends DaoSupport { + + private HibernateTemplate hibernateTemplate; + + + /** + * Set the Hibernate SessionFactory to be used by this DAO. + * Will automatically create a HibernateTemplate for the given SessionFactory. + * @see #createHibernateTemplate + * @see #setHibernateTemplate + */ + public final void setSessionFactory(SessionFactory sessionFactory) { + if (this.hibernateTemplate == null || sessionFactory != this.hibernateTemplate.getSessionFactory()) { + this.hibernateTemplate = createHibernateTemplate(sessionFactory); + } + } + + /** + * Create a HibernateTemplate for the given SessionFactory. + * Only invoked if populating the DAO with a SessionFactory reference! + *

Can be overridden in subclasses to provide a HibernateTemplate instance + * with different configuration, or a custom HibernateTemplate subclass. + * @param sessionFactory the Hibernate SessionFactory to create a HibernateTemplate for + * @return the new HibernateTemplate instance + * @see #setSessionFactory + */ + protected HibernateTemplate createHibernateTemplate(SessionFactory sessionFactory) { + return new HibernateTemplate(sessionFactory); + } + + /** + * Return the Hibernate SessionFactory used by this DAO. + */ + public final SessionFactory getSessionFactory() { + return (this.hibernateTemplate != null ? this.hibernateTemplate.getSessionFactory() : null); + } + + /** + * Set the HibernateTemplate for this DAO explicitly, + * as an alternative to specifying a SessionFactory. + * @see #setSessionFactory + */ + public final void setHibernateTemplate(HibernateTemplate hibernateTemplate) { + this.hibernateTemplate = hibernateTemplate; + } + + /** + * Return the HibernateTemplate for this DAO, + * pre-initialized with the SessionFactory or set explicitly. + *

Note: The returned HibernateTemplate is a shared instance. + * You may introspect its configuration, but not modify the configuration + * (other than from within an {@link #initDao} implementation). + * Consider creating a custom HibernateTemplate instance via + * new HibernateTemplate(getSessionFactory()), in which + * case you're allowed to customize the settings on the resulting instance. + */ + public final HibernateTemplate getHibernateTemplate() { + return this.hibernateTemplate; + } + + protected final void checkDaoConfig() { + if (this.hibernateTemplate == null) { + throw new IllegalArgumentException("'sessionFactory' or 'hibernateTemplate' is required"); + } + } + + + /** + * Obtain a Hibernate Session, either from the current transaction or + * a new one. The latter is only allowed if the + * {@link org.springframework.orm.hibernate3.HibernateTemplate#setAllowCreate "allowCreate"} + * setting of this bean's {@link #setHibernateTemplate HibernateTemplate} is "true". + *

Note that this is not meant to be invoked from HibernateTemplate code + * but rather just in plain Hibernate code. Either rely on a thread-bound + * Session or use it in combination with {@link #releaseSession}. + *

In general, it is recommended to use HibernateTemplate, either with + * the provided convenience operations or with a custom HibernateCallback + * that provides you with a Session to work on. HibernateTemplate will care + * for all resource management and for proper exception conversion. + * @return the Hibernate Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and allowCreate=false + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession(SessionFactory, boolean) + */ + protected final Session getSession() + throws DataAccessResourceFailureException, IllegalStateException { + + return getSession(this.hibernateTemplate.isAllowCreate()); + } + + /** + * Obtain a Hibernate Session, either from the current transaction or + * a new one. The latter is only allowed if "allowCreate" is true. + *

Note that this is not meant to be invoked from HibernateTemplate code + * but rather just in plain Hibernate code. Either rely on a thread-bound + * Session or use it in combination with {@link #releaseSession}. + *

In general, it is recommended to use + * {@link #getHibernateTemplate() HibernateTemplate}, either with + * the provided convenience operations or with a custom + * {@link org.springframework.orm.hibernate3.HibernateCallback} that + * provides you with a Session to work on. HibernateTemplate will care + * for all resource management and for proper exception conversion. + * @param allowCreate if a non-transactional Session should be created when no + * transactional Session can be found for the current thread + * @return the Hibernate Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and allowCreate=false + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession(SessionFactory, boolean) + */ + protected final Session getSession(boolean allowCreate) + throws DataAccessResourceFailureException, IllegalStateException { + + return (!allowCreate ? + SessionFactoryUtils.getSession(getSessionFactory(), false) : + SessionFactoryUtils.getSession( + getSessionFactory(), + this.hibernateTemplate.getEntityInterceptor(), + this.hibernateTemplate.getJdbcExceptionTranslator())); + } + + /** + * Convert the given HibernateException to an appropriate exception from the + * org.springframework.dao hierarchy. Will automatically detect + * wrapped SQLExceptions and convert them accordingly. + *

Delegates to the + * {@link org.springframework.orm.hibernate3.HibernateTemplate#convertHibernateAccessException} + * method of this DAO's HibernateTemplate. + *

Typically used in plain Hibernate code, in combination with + * {@link #getSession} and {@link #releaseSession}. + * @param ex HibernateException that occured + * @return the corresponding DataAccessException instance + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#convertHibernateAccessException + */ + protected final DataAccessException convertHibernateAccessException(HibernateException ex) { + return this.hibernateTemplate.convertHibernateAccessException(ex); + } + + /** + * Close the given Hibernate Session, created via this DAO's SessionFactory, + * if it isn't bound to the thread (i.e. isn't a transactional Session). + *

Typically used in plain Hibernate code, in combination with + * {@link #getSession} and {@link #convertHibernateAccessException}. + * @param session the Session to close + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#releaseSession + */ + protected final void releaseSession(Session session) { + SessionFactoryUtils.releaseSession(session, getSessionFactory()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/IdTransferringMergeEventListener.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/IdTransferringMergeEventListener.java new file mode 100644 index 00000000000..d067a3fce53 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/IdTransferringMergeEventListener.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2007 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.hibernate3.support; + +import java.io.Serializable; +import java.util.Map; + +import org.hibernate.engine.SessionImplementor; +import org.hibernate.event.MergeEvent; +import org.hibernate.event.def.DefaultMergeEventListener; +import org.hibernate.persister.entity.EntityPersister; + +/** + * Extension of Hibernate's DefaultMergeEventListener, transferring the ids + * of newly saved objects to the corresponding original objects (that are part + * of the detached object graph passed into the merge method). + * + *

Transferring newly assigned ids to the original graph allows for continuing + * to use the original object graph, despite merged copies being registered with + * the current Hibernate Session. This is particularly useful for web applications + * that might want to store an object graph and then render it in a web view, + * with links that include the id of certain (potentially newly saved) objects. + * + *

The merge behavior given by this MergeEventListener is nearly identical + * to TopLink's merge behavior. See PetClinic for an example, which relies on + * ids being available for newly saved objects: the HibernateClinic + * and TopLinkClinic DAO implementations both use straight merge + * calls, with the Hibernate SessionFactory configuration specifying an + * IdTransferringMergeEventListener. + * + *

Typically specified as entry for LocalSessionFactoryBean's "eventListeners" + * map, with key "merge". + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.orm.hibernate3.LocalSessionFactoryBean#setEventListeners(java.util.Map) + */ +public class IdTransferringMergeEventListener extends DefaultMergeEventListener { + + /** + * Hibernate 3.1 implementation of ID transferral. + */ + protected void entityIsTransient(MergeEvent event, Map copyCache) { + super.entityIsTransient(event, copyCache); + SessionImplementor session = event.getSession(); + EntityPersister persister = session.getEntityPersister(event.getEntityName(), event.getEntity()); + // Extract id from merged copy (which is currently registered with Session). + Serializable id = persister.getIdentifier(event.getResult(), session.getEntityMode()); + // Set id on original object (which remains detached). + persister.setIdentifier(event.getOriginal(), id, session.getEntityMode()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java new file mode 100644 index 00000000000..b3eaec38e47 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2007 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.hibernate3.support; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.hibernate3.SessionFactoryUtils; +import org.springframework.orm.hibernate3.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Servlet 2.3 Filter that binds a Hibernate Session to the thread for the entire + * processing of the request. Intended for the "Open Session in View" pattern, + * i.e. to allow for lazy loading in web views despite the original transactions + * already being completed. + * + *

This filter makes Hibernate Sessions available via the current thread, which + * will be autodetected by transaction managers. It is suitable for service layer + * transactions via {@link org.springframework.orm.hibernate3.HibernateTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager} as well + * as for non-transactional execution (if configured appropriately). + * + *

NOTE: This filter will by default not flush the Hibernate Session, + * with the flush mode set to FlushMode.NEVER. It assumes to be used + * in combination with service layer transactions that care for the flushing: The + * active transaction manager will temporarily change the flush mode to + * FlushMode.AUTO during a read-write transaction, with the flush + * mode reset to FlushMode.NEVER at the end of each transaction. + * If you intend to use this filter without transactions, consider changing + * the default flush mode (through the "flushMode" property). + * + *

WARNING: Applying this filter to existing logic can cause issues that + * have not appeared before, through the use of a single Hibernate Session for the + * processing of an entire request. In particular, the reassociation of persistent + * objects with a Hibernate Session has to occur at the very beginning of request + * processing, to avoid clashes with already loaded instances of the same objects. + * + *

Alternatively, turn this filter into deferred close mode, by specifying + * "singleSession"="false": It will not use a single session per request then, + * but rather let each data access operation or transaction use its own session + * (like without Open Session in View). Each of those sessions will be registered + * for deferred close, though, actually processed at request completion. + * + *

A single session per request allows for most efficient first-level caching, + * but can cause side effects, for example on saveOrUpdate or when + * continuing after a rolled-back transaction. The deferred close strategy is as safe + * as no Open Session in View in that respect, while still allowing for lazy loading + * in views (but not providing a first-level cache for the entire request). + * + *

Looks up the SessionFactory in Spring's root web application context. + * Supports a "sessionFactoryBeanName" filter init-param in web.xml; + * the default bean name is "sessionFactory". Looks up the SessionFactory on each + * request, to avoid initialization order issues (when using ContextLoaderServlet, + * the root application context will get initialized after this filter). + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setSingleSession + * @see #setFlushMode + * @see #lookupSessionFactory + * @see OpenSessionInViewInterceptor + * @see org.springframework.orm.hibernate3.HibernateInterceptor + * @see org.springframework.orm.hibernate3.HibernateTransactionManager + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class OpenSessionInViewFilter extends OncePerRequestFilter { + + public static final String DEFAULT_SESSION_FACTORY_BEAN_NAME = "sessionFactory"; + + + private String sessionFactoryBeanName = DEFAULT_SESSION_FACTORY_BEAN_NAME; + + private boolean singleSession = true; + + private FlushMode flushMode = FlushMode.NEVER; + + + /** + * Set the bean name of the SessionFactory to fetch from Spring's + * root application context. Default is "sessionFactory". + * @see #DEFAULT_SESSION_FACTORY_BEAN_NAME + */ + public void setSessionFactoryBeanName(String sessionFactoryBeanName) { + this.sessionFactoryBeanName = sessionFactoryBeanName; + } + + /** + * Return the bean name of the SessionFactory to fetch from Spring's + * root application context. + */ + protected String getSessionFactoryBeanName() { + return this.sessionFactoryBeanName; + } + + /** + * Set whether to use a single session for each request. Default is "true". + *

If set to "false", each data access operation or transaction will use + * its own session (like without Open Session in View). Each of those + * sessions will be registered for deferred close, though, actually + * processed at request completion. + * @see SessionFactoryUtils#initDeferredClose + * @see SessionFactoryUtils#processDeferredClose + */ + public void setSingleSession(boolean singleSession) { + this.singleSession = singleSession; + } + + /** + * Return whether to use a single session for each request. + */ + protected boolean isSingleSession() { + return this.singleSession; + } + + /** + * Specify the Hibernate FlushMode to apply to this filter's + * {@link org.hibernate.Session}. Only applied in single session mode. + *

Can be populated with the corresponding constant name in XML bean + * definitions: e.g. "AUTO". + *

The default is "NEVER". Specify "AUTO" if you intend to use + * this filter without service layer transactions. + * @see org.hibernate.Session#setFlushMode + * @see org.hibernate.FlushMode#NEVER + * @see org.hibernate.FlushMode#AUTO + */ + public void setFlushMode(FlushMode flushMode) { + this.flushMode = flushMode; + } + + /** + * Return the Hibernate FlushMode that this filter applies to its + * {@link org.hibernate.Session} (in single session mode). + */ + protected FlushMode getFlushMode() { + return this.flushMode; + } + + + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + SessionFactory sessionFactory = lookupSessionFactory(request); + boolean participate = false; + + if (isSingleSession()) { + // single session mode + if (TransactionSynchronizationManager.hasResource(sessionFactory)) { + // Do not modify the Session: just set the participate flag. + participate = true; + } + else { + logger.debug("Opening single Hibernate Session in OpenSessionInViewFilter"); + Session session = getSession(sessionFactory); + TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)); + } + } + else { + // deferred close mode + if (SessionFactoryUtils.isDeferredCloseActive(sessionFactory)) { + // Do not modify deferred close: just set the participate flag. + participate = true; + } + else { + SessionFactoryUtils.initDeferredClose(sessionFactory); + } + } + + try { + filterChain.doFilter(request, response); + } + + finally { + if (!participate) { + if (isSingleSession()) { + // single session mode + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory); + logger.debug("Closing single Hibernate Session in OpenSessionInViewFilter"); + closeSession(sessionHolder.getSession(), sessionFactory); + } + else { + // deferred close mode + SessionFactoryUtils.processDeferredClose(sessionFactory); + } + } + } + } + + /** + * Look up the SessionFactory that this filter should use, + * taking the current HTTP request as argument. + *

The default implementation delegates to the {@link #lookupSessionFactory()} + * variant without arguments. + * @param request the current request + * @return the SessionFactory to use + */ + protected SessionFactory lookupSessionFactory(HttpServletRequest request) { + return lookupSessionFactory(); + } + + /** + * Look up the SessionFactory that this filter should use. + *

The default implementation looks for a bean with the specified name + * in Spring's root application context. + * @return the SessionFactory to use + * @see #getSessionFactoryBeanName + */ + protected SessionFactory lookupSessionFactory() { + if (logger.isDebugEnabled()) { + logger.debug("Using SessionFactory '" + getSessionFactoryBeanName() + "' for OpenSessionInViewFilter"); + } + WebApplicationContext wac = + WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + return (SessionFactory) wac.getBean(getSessionFactoryBeanName(), SessionFactory.class); + } + + /** + * Get a Session for the SessionFactory that this filter uses. + * Note that this just applies in single session mode! + *

The default implementation delegates to the + * SessionFactoryUtils.getSession method and + * sets the Session's flush mode to "NEVER". + *

Can be overridden in subclasses for creating a Session with a + * custom entity interceptor or JDBC exception translator. + * @param sessionFactory the SessionFactory that this filter uses + * @return the Session to use + * @throws DataAccessResourceFailureException if the Session could not be created + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession(SessionFactory, boolean) + * @see org.hibernate.FlushMode#NEVER + */ + protected Session getSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException { + Session session = SessionFactoryUtils.getSession(sessionFactory, true); + FlushMode flushMode = getFlushMode(); + if (flushMode != null) { + session.setFlushMode(flushMode); + } + return session; + } + + /** + * Close the given Session. + * Note that this just applies in single session mode! + *

Can be overridden in subclasses, e.g. for flushing the Session before + * closing it. See class-level javadoc for a discussion of flush handling. + * Note that you should also override getSession accordingly, to set + * the flush mode to something else than NEVER. + * @param session the Session used for filtering + * @param sessionFactory the SessionFactory that this filter uses + */ + protected void closeSession(Session session, SessionFactory sessionFactory) { + SessionFactoryUtils.closeSession(session); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java new file mode 100644 index 00000000000..4a31dae7d52 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/OpenSessionInViewInterceptor.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2007 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.hibernate3.support; + +import org.hibernate.HibernateException; +import org.hibernate.Session; + +import org.springframework.dao.DataAccessException; +import org.springframework.orm.hibernate3.HibernateAccessor; +import org.springframework.orm.hibernate3.SessionFactoryUtils; +import org.springframework.orm.hibernate3.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.ui.ModelMap; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.context.request.WebRequestInterceptor; + +/** + * Spring web request interceptor that binds a Hibernate Session to the + * thread for the entire processing of the request. + * + *

This class is a concrete expression of the "Open Session in View" pattern, which + * is a pattern that allows for the lazy loading of associations in web views despite + * the original transactions already being completed. + * + *

This interceptor makes Hibernate Sessions available via the current + * thread, which will be autodetected by transaction managers. It is suitable for + * service layer transactions via + * {@link org.springframework.orm.hibernate3.HibernateTransactionManager} or + * {@link org.springframework.transaction.jta.JtaTransactionManager} as well as for + * non-transactional execution (if configured appropriately). + * + *

NOTE: This interceptor will by default not flush the Hibernate + * Session, with the flush mode being set to FlushMode.NEVER. + * It assumes that it will be used in combination with service layer transactions + * that handle the flushing: the active transaction manager will temporarily change + * the flush mode to FlushMode.AUTO during a read-write transaction, + * with the flush mode reset to FlushMode.NEVER at the end of each + * transaction. If you intend to use this interceptor without transactions, consider + * changing the default flush mode (through the + * {@link #setFlushMode(int) "flushMode"} property). + * + *

In contrast to {@link OpenSessionInViewFilter}, this interceptor is + * configured in a Spring application context and can thus take advantage of bean + * wiring. It inherits common Hibernate configuration properties from + * {@link org.springframework.orm.hibernate3.HibernateAccessor}, + * to be configured in a bean definition. + * + *

WARNING: Applying this interceptor to existing logic can cause issues + * that have not appeared before, through the use of a single Hibernate + * Session for the processing of an entire request. In particular, the + * reassociation of persistent objects with a Hibernate Session has to + * occur at the very beginning of request processing, to avoid clashes with already + * loaded instances of the same objects. + * + *

Alternatively, turn this interceptor into deferred close mode, by specifying + * "singleSession"="false": It will not use a single session per request then, + * but rather let each data access operation or transaction use its own session + * (as would be the case without Open Session in View). Each of those sessions will + * be registered for deferred close though, which will actually be processed at + * request completion. + * + *

A single session per request allows for the most efficient first-level caching, + * but can cause side effects, for example on saveOrUpdate or when + * continuing after a rolled-back transaction. The deferred close strategy is as safe + * as no Open Session in View in that respect, while still allowing for lazy loading + * in views (but not providing a first-level cache for the entire request). + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setSingleSession + * @see #setFlushMode + * @see OpenSessionInViewFilter + * @see org.springframework.orm.hibernate3.HibernateInterceptor + * @see org.springframework.orm.hibernate3.HibernateTransactionManager + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class OpenSessionInViewInterceptor extends HibernateAccessor implements WebRequestInterceptor { + + /** + * Suffix that gets appended to the SessionFactory + * toString() representation for the "participate in existing + * session handling" request attribute. + * @see #getParticipateAttributeName + */ + public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE"; + + + private boolean singleSession = true; + + + /** + * Create a new OpenSessionInViewInterceptor, + * turning the default flushMode to FLUSH_NEVER. + * @see #setFlushMode + */ + public OpenSessionInViewInterceptor() { + setFlushMode(FLUSH_NEVER); + } + + /** + * Set whether to use a single session for each request. Default is "true". + *

If set to false, each data access operation or transaction will use + * its own session (like without Open Session in View). Each of those + * sessions will be registered for deferred close, though, actually + * processed at request completion. + * @see SessionFactoryUtils#initDeferredClose + * @see SessionFactoryUtils#processDeferredClose + */ + public void setSingleSession(boolean singleSession) { + this.singleSession = singleSession; + } + + /** + * Return whether to use a single session for each request. + */ + protected boolean isSingleSession() { + return singleSession; + } + + + /** + * Open a new Hibernate Session according to the settings of this + * HibernateAccessor and bind it to the thread via the + * {@link TransactionSynchronizationManager}. + * @see org.springframework.orm.hibernate3.SessionFactoryUtils#getSession + */ + public void preHandle(WebRequest request) throws DataAccessException { + if ((isSingleSession() && TransactionSynchronizationManager.hasResource(getSessionFactory())) || + SessionFactoryUtils.isDeferredCloseActive(getSessionFactory())) { + // Do not modify the Session: just mark the request accordingly. + String participateAttributeName = getParticipateAttributeName(); + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + int newCount = (count != null) ? count.intValue() + 1 : 1; + request.setAttribute(getParticipateAttributeName(), new Integer(newCount), WebRequest.SCOPE_REQUEST); + } + else { + if (isSingleSession()) { + // single session mode + logger.debug("Opening single Hibernate Session in OpenSessionInViewInterceptor"); + Session session = SessionFactoryUtils.getSession( + getSessionFactory(), getEntityInterceptor(), getJdbcExceptionTranslator()); + applyFlushMode(session, false); + TransactionSynchronizationManager.bindResource(getSessionFactory(), new SessionHolder(session)); + } + else { + // deferred close mode + SessionFactoryUtils.initDeferredClose(getSessionFactory()); + } + } + } + + /** + * Flush the Hibernate Session before view rendering, if necessary. + *

Note that this just applies in {@link #isSingleSession() single session mode}! + *

The default is FLUSH_NEVER to avoid this extra flushing, + * assuming that service layer transactions have flushed their changes on commit. + * @see #setFlushMode + */ + public void postHandle(WebRequest request, ModelMap model) throws DataAccessException { + if (isSingleSession()) { + // Only potentially flush in single session mode. + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory()); + logger.debug("Flushing single Hibernate Session in OpenSessionInViewInterceptor"); + try { + flushIfNecessary(sessionHolder.getSession(), false); + } + catch (HibernateException ex) { + throw convertHibernateAccessException(ex); + } + } + } + + /** + * Unbind the Hibernate Session from the thread and close it (in + * single session mode), or process deferred close for all sessions that have + * been opened during the current request (in deferred close mode). + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ + public void afterCompletion(WebRequest request, Exception ex) throws DataAccessException { + String participateAttributeName = getParticipateAttributeName(); + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + if (count != null) { + // Do not modify the Session: just clear the marker. + if (count.intValue() > 1) { + request.setAttribute(participateAttributeName, new Integer(count.intValue() - 1), WebRequest.SCOPE_REQUEST); + } + else { + request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + } + } + else { + if (isSingleSession()) { + // single session mode + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory()); + logger.debug("Closing single Hibernate Session in OpenSessionInViewInterceptor"); + SessionFactoryUtils.closeSession(sessionHolder.getSession()); + } + else { + // deferred close mode + SessionFactoryUtils.processDeferredClose(getSessionFactory()); + } + } + } + + /** + * Return the name of the request attribute that identifies that a request is + * already intercepted. + *

The default implementation takes the toString() representation + * of the SessionFactory instance and appends {@link #PARTICIPATE_SUFFIX}. + */ + protected String getParticipateAttributeName() { + return getSessionFactory().toString() + PARTICIPATE_SUFFIX; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/ScopedBeanInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/ScopedBeanInterceptor.java new file mode 100644 index 00000000000..9ea0b94946c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/ScopedBeanInterceptor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2007 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.hibernate3.support; + +import org.hibernate.EmptyInterceptor; + +import org.springframework.aop.scope.ScopedObject; +import org.springframework.aop.support.AopUtils; + +/** + * Hibernate3 interceptor used for getting the proper entity name for scoped + * beans. As scoped bean classes are proxies generated at runtime, they are + * unrecognized by the persisting framework. Using this interceptor, the + * original scoped bean class is retrieved end exposed to Hibernate for + * persisting. + * + *

Usage example: + * + *

+ * <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
+ *   ...
+ *   <property name="entityInterceptor">
+ *     <bean class="org.springframework.orm.hibernate3.support.ScopedBeanInterceptor"/>
+ *   </property>
+ * </bean>
+ * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +public class ScopedBeanInterceptor extends EmptyInterceptor { + + public String getEntityName(Object entity) { + if (entity instanceof ScopedObject) { + // Determine underlying object's type. + Object targetObject = ((ScopedObject) entity).getTargetObject(); + return AopUtils.getTargetClass(targetObject).getName(); + } + + // Any other object: delegate to the default implementation. + return super.getEntityName(entity); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/package.html new file mode 100644 index 00000000000..1ef04906ded --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/hibernate3/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.orm.hibernate3 package. +Contains a DAO base class for HibernateTemplate usage. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientCallback.java new file mode 100644 index 00000000000..0723464d1e0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientCallback.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2006 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.ibatis; + +import java.sql.SQLException; + +import com.ibatis.sqlmap.client.SqlMapExecutor; + +/** + * Callback interface for data access code that works with the iBATIS + * {@link com.ibatis.sqlmap.client.SqlMapExecutor} interface. To be used + * with {@link SqlMapClientTemplate}'s execute method, + * assumably often as anonymous classes within a method implementation. + * + * @author Juergen Hoeller + * @since 24.02.2004 + * @see SqlMapClientTemplate + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + */ +public interface SqlMapClientCallback { + + /** + * Gets called by SqlMapClientTemplate.execute with an active + * SqlMapExecutor. Does not need to care about activating + * or closing the SqlMapExecutor, or handling transactions. + * + *

If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * underlying JDBC connection with its transactional semantics. If using + * a JTA-aware DataSource, the JDBC connection and thus the callback code + * will be transactional if a JTA transaction is active. + * + *

Allows for returning a result object created within the callback, + * i.e. a domain object or a collection of domain objects. + * A thrown custom RuntimeException is treated as an application exception: + * It gets propagated to the caller of the template. + * + * @param executor an active iBATIS SqlMapSession, passed-in as + * SqlMapExecutor interface here to avoid manual lifecycle handling + * @return a result object, or null if none + * @throws SQLException if thrown by the iBATIS SQL Maps API + * @see SqlMapClientTemplate#execute + * @see SqlMapClientTemplate#executeWithListResult + * @see SqlMapClientTemplate#executeWithMapResult + */ + Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientFactoryBean.java new file mode 100644 index 00000000000..603e908b345 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientFactoryBean.java @@ -0,0 +1,419 @@ +/* + * Copyright 2002-2008 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.ibatis; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.Properties; + +import javax.sql.DataSource; + +import com.ibatis.common.xml.NodeletException; +import com.ibatis.sqlmap.client.SqlMapClient; +import com.ibatis.sqlmap.client.SqlMapClientBuilder; +import com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser; +import com.ibatis.sqlmap.engine.builder.xml.SqlMapParser; +import com.ibatis.sqlmap.engine.builder.xml.XmlParserState; +import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient; +import com.ibatis.sqlmap.engine.transaction.TransactionConfig; +import com.ibatis.sqlmap.engine.transaction.TransactionManager; +import com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.NestedIOException; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.util.ObjectUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates an + * iBATIS {@link com.ibatis.sqlmap.client.SqlMapClient}. This is the usual + * way to set up a shared iBATIS SqlMapClient in a Spring application context; + * the SqlMapClient can then be passed to iBATIS-based DAOs via dependency + * injection. + * + *

Either {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager} can be + * used for transaction demarcation in combination with a SqlMapClient, + * with JTA only necessary for transactions which span multiple databases. + * + *

Allows for specifying a DataSource at the SqlMapClient level. This + * is preferable to per-DAO DataSource references, as it allows for lazy + * loading and avoids repeated DataSource references in every DAO. + * + *

Note: As of Spring 2.5.5, this class (finally) requires iBATIS 2.3 + * or higher. The new "mappingLocations" feature requires iBATIS 2.3.2. + * + * @author Juergen Hoeller + * @since 24.02.2004 + * @see #setConfigLocation + * @see #setDataSource + * @see SqlMapClientTemplate#setSqlMapClient + * @see SqlMapClientTemplate#setDataSource + */ +public class SqlMapClientFactoryBean implements FactoryBean, InitializingBean { + + private static final ThreadLocal configTimeLobHandlerHolder = new ThreadLocal(); + + /** + * Return the LobHandler for the currently configured iBATIS SqlMapClient, + * to be used by TypeHandler implementations like ClobStringTypeHandler. + *

This instance will be set before initialization of the corresponding + * SqlMapClient, and reset immediately afterwards. It is thus only available + * during configuration. + * @see #setLobHandler + * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler + * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler + * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler + */ + public static LobHandler getConfigTimeLobHandler() { + return (LobHandler) configTimeLobHandlerHolder.get(); + } + + + private Resource[] configLocations; + + private Resource[] mappingLocations; + + private Properties sqlMapClientProperties; + + private DataSource dataSource; + + private boolean useTransactionAwareDataSource = true; + + private Class transactionConfigClass = ExternalTransactionConfig.class; + + private Properties transactionConfigProperties; + + private LobHandler lobHandler; + + private SqlMapClient sqlMapClient; + + + public SqlMapClientFactoryBean() { + this.transactionConfigProperties = new Properties(); + this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false"); + } + + /** + * Set the location of the iBATIS SqlMapClient config file. + * A typical value is "WEB-INF/sql-map-config.xml". + * @see #setConfigLocations + */ + public void setConfigLocation(Resource configLocation) { + this.configLocations = (configLocation != null ? new Resource[] {configLocation} : null); + } + + /** + * Set multiple locations of iBATIS SqlMapClient config files that + * are going to be merged into one unified configuration at runtime. + */ + public void setConfigLocations(Resource[] configLocations) { + this.configLocations = configLocations; + } + + /** + * Set locations of iBATIS sql-map mapping files that are going to be + * merged into the SqlMapClient configuration at runtime. + *

This is an alternative to specifying "<sqlMap>" entries + * in a sql-map-client config file. This property being based on Spring's + * resource abstraction also allows for specifying resource patterns here: + * e.g. "/myApp/*-map.xml". + *

Note that this feature requires iBATIS 2.3.2; it will not work + * with any previous iBATIS version. + */ + public void setMappingLocations(Resource[] mappingLocations) { + this.mappingLocations = mappingLocations; + } + + /** + * Set optional properties to be passed into the SqlMapClientBuilder, as + * alternative to a <properties> tag in the sql-map-config.xml + * file. Will be used to resolve placeholders in the config file. + * @see #setConfigLocation + * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, java.util.Properties) + */ + public void setSqlMapClientProperties(Properties sqlMapClientProperties) { + this.sqlMapClientProperties = sqlMapClientProperties; + } + + /** + * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to the + * SqlMapClient as part of a TransactionConfig instance. + *

If specified, this will override corresponding settings in the SqlMapClient + * properties. Usually, you will specify DataSource and transaction configuration + * either here or in SqlMapClient properties. + *

Specifying a DataSource for the SqlMapClient rather than for each individual + * DAO allows for lazy loading, for example when using PaginatedList results. + *

With a DataSource passed in here, you don't need to specify one for each DAO. + * Passing the SqlMapClient to the DAOs is enough, as it already carries a DataSource. + * Thus, it's recommended to specify the DataSource at this central location only. + *

Thanks to Brandon Goodin from the iBATIS team for the hint on how to make + * this work with Spring's integration strategy! + * @see #setTransactionConfigClass + * @see #setTransactionConfigProperties + * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource + * @see SqlMapClientTemplate#setDataSource + * @see SqlMapClientTemplate#queryForPaginatedList + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set whether to use a transaction-aware DataSource for the SqlMapClient, + * i.e. whether to automatically wrap the passed-in DataSource with Spring's + * TransactionAwareDataSourceProxy. + *

Default is "true": When the SqlMapClient performs direct database operations + * outside of Spring's SqlMapClientTemplate (for example, lazy loading or direct + * SqlMapClient access), it will still participate in active Spring-managed + * transactions. + *

As a further effect, using a transaction-aware DataSource will apply + * remaining transaction timeouts to all created JDBC Statements. This means + * that all operations performed by the SqlMapClient will automatically + * participate in Spring-managed transaction timeouts. + *

Turn this flag off to get raw DataSource handling, without Spring transaction + * checks. Operations on Spring's SqlMapClientTemplate will still detect + * Spring-managed transactions, but lazy loading or direct SqlMapClient access won't. + * @see #setDataSource + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see SqlMapClientTemplate + * @see com.ibatis.sqlmap.client.SqlMapClient + */ + public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) { + this.useTransactionAwareDataSource = useTransactionAwareDataSource; + } + + /** + * Set the iBATIS TransactionConfig class to use. Default is + * com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig. + *

Will only get applied when using a Spring-managed DataSource. + * An instance of this class will get populated with the given DataSource + * and initialized with the given properties. + *

The default ExternalTransactionConfig is appropriate if there is + * external transaction management that the SqlMapClient should participate + * in: be it Spring transaction management, EJB CMT or plain JTA. This + * should be the typical scenario. If there is no active transaction, + * SqlMapClient operations will execute SQL statements non-transactionally. + *

JdbcTransactionConfig or JtaTransactionConfig is only necessary + * when using the iBATIS SqlMapTransactionManager API instead of external + * transactions. If there is no explicit transaction, SqlMapClient operations + * will automatically start a transaction for their own scope (in contrast + * to the external transaction mode, see above). + *

It is strongly recommended to use iBATIS SQL Maps with Spring + * transaction management (or EJB CMT). In this case, the default + * ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations + * without explicit transaction demarcation will execute non-transactionally. + *

Even with Spring transaction management, it might be desirable to + * specify JdbcTransactionConfig: This will still participate in existing + * Spring-managed transactions, but lazy loading and operations without + * explicit transaction demaration will execute in their own auto-started + * transactions. However, this is usually not necessary. + * @see #setDataSource + * @see #setTransactionConfigProperties + * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig + * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig + * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig + * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig + * @see com.ibatis.sqlmap.client.SqlMapTransactionManager + */ + public void setTransactionConfigClass(Class transactionConfigClass) { + if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) { + throw new IllegalArgumentException("Invalid transactionConfigClass: does not implement " + + "com.ibatis.sqlmap.engine.transaction.TransactionConfig"); + } + this.transactionConfigClass = transactionConfigClass; + } + + /** + * Set properties to be passed to the TransactionConfig instance used + * by this SqlMapClient. Supported properties depend on the concrete + * TransactionConfig implementation used: + *

+ * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize + * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig + * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig + * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig + */ + public void setTransactionConfigProperties(Properties transactionConfigProperties) { + this.transactionConfigProperties = transactionConfigProperties; + } + + /** + * Set the LobHandler to be used by the SqlMapClient. + * Will be exposed at config time for TypeHandler implementations. + * @see #getConfigTimeLobHandler + * @see com.ibatis.sqlmap.engine.type.TypeHandler + * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler + * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler + * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler + */ + public void setLobHandler(LobHandler lobHandler) { + this.lobHandler = lobHandler; + } + + + public void afterPropertiesSet() throws Exception { + if (this.lobHandler != null) { + // Make given LobHandler available for SqlMapClient configuration. + // Do early because because mapping resource might refer to custom types. + configTimeLobHandlerHolder.set(this.lobHandler); + } + + try { + this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties); + + // Tell the SqlMapClient to use the given DataSource, if any. + if (this.dataSource != null) { + TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass.newInstance(); + DataSource dataSourceToUse = this.dataSource; + if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) { + dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource); + } + transactionConfig.setDataSource(dataSourceToUse); + transactionConfig.initialize(this.transactionConfigProperties); + applyTransactionConfig(this.sqlMapClient, transactionConfig); + } + } + + finally { + if (this.lobHandler != null) { + // Reset LobHandler holder. + configTimeLobHandlerHolder.set(null); + } + } + } + + /** + * Build a SqlMapClient instance based on the given standard configuration. + *

The default implementation uses the standard iBATIS {@link SqlMapClientBuilder} + * API to build a SqlMapClient instance based on an InputStream (if possible, + * on iBATIS 2.3 and higher) or on a Reader (on iBATIS up to version 2.2). + * @param configLocations the config files to load from + * @param properties the SqlMapClient properties (if any) + * @return the SqlMapClient instance (never null) + * @throws IOException if loading the config file failed + * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient + */ + protected SqlMapClient buildSqlMapClient( + Resource[] configLocations, Resource[] mappingLocations, Properties properties) + throws IOException { + + if (ObjectUtils.isEmpty(configLocations)) { + throw new IllegalArgumentException("At least 1 'configLocation' entry is required"); + } + + SqlMapClient client = null; + SqlMapConfigParser configParser = new SqlMapConfigParser(); + for (int i = 0; i < configLocations.length; i++) { + InputStream is = configLocations[i].getInputStream(); + try { + client = configParser.parse(is, properties); + } + catch (RuntimeException ex) { + throw new NestedIOException("Failed to parse config resource: " + configLocations[i], ex.getCause()); + } + } + + if (mappingLocations != null) { + SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser); + for (int i = 0; i < mappingLocations.length; i++) { + try { + mapParser.parse(mappingLocations[i].getInputStream()); + } + catch (NodeletException ex) { + throw new NestedIOException("Failed to parse mapping resource: " + mappingLocations[i], ex); + } + } + } + + return client; + } + + /** + * Apply the given iBATIS TransactionConfig to the SqlMapClient. + *

The default implementation casts to ExtendedSqlMapClient, retrieves the maximum + * number of concurrent transactions from the SqlMapExecutorDelegate, and sets + * an iBATIS TransactionManager with the given TransactionConfig. + * @param sqlMapClient the SqlMapClient to apply the TransactionConfig to + * @param transactionConfig the iBATIS TransactionConfig to apply + * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient + * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions + * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager + */ + protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) { + if (!(sqlMapClient instanceof ExtendedSqlMapClient)) { + throw new IllegalArgumentException( + "Cannot set TransactionConfig with DataSource for SqlMapClient if not of type " + + "ExtendedSqlMapClient: " + sqlMapClient); + } + ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient; + transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate().getMaxTransactions()); + extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig)); + } + + + public Object getObject() { + return this.sqlMapClient; + } + + public Class getObjectType() { + return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState class). + */ + private static class SqlMapParserFactory { + + public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) { + // Ideally: XmlParserState state = configParser.getState(); + // Should raise an enhancement request with iBATIS... + XmlParserState state = null; + try { + Field stateField = SqlMapConfigParser.class.getDeclaredField("state"); + stateField.setAccessible(true); + state = (XmlParserState) stateField.get(configParser); + } + catch (Exception ex) { + throw new IllegalStateException("iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - " + + "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. " + ex); + } + return new SqlMapParser(state); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientOperations.java new file mode 100644 index 00000000000..06ddb42c790 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientOperations.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2006 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.ibatis; + +import java.util.List; +import java.util.Map; + +import com.ibatis.common.util.PaginatedList; +import com.ibatis.sqlmap.client.event.RowHandler; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that specifies a basic set of iBATIS SqlMapClient operations, + * implemented by {@link SqlMapClientTemplate}. Not often used, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Defines SqlMapClientTemplate's convenience methods that mirror + * the iBATIS {@link com.ibatis.sqlmap.client.SqlMapExecutor}'s execution + * methods. Users are strongly encouraged to read the iBATIS javadocs + * for details on the semantics of those methods. + * + * @author Juergen Hoeller + * @since 24.02.2004 + * @see SqlMapClientTemplate + * @see com.ibatis.sqlmap.client.SqlMapClient + * @see com.ibatis.sqlmap.client.SqlMapExecutor + */ +public interface SqlMapClientOperations { + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForObject(String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Object queryForObject(String statementName) throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForObject(String, Object) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Object queryForObject(String statementName, Object parameterObject) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForObject(String, Object, Object) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Object queryForObject(String statementName, Object parameterObject, Object resultObject) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForList(String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + List queryForList(String statementName) throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForList(String, Object) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + List queryForList(String statementName, Object parameterObject) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForList(String, int, int) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + List queryForList(String statementName, int skipResults, int maxResults) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForList(String, Object, int, int) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + List queryForList(String statementName, Object parameterObject, int skipResults, int maxResults) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryWithRowHandler(String, RowHandler) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + void queryWithRowHandler(String statementName, RowHandler rowHandler) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryWithRowHandler(String, Object, RowHandler) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + void queryWithRowHandler(String statementName, Object parameterObject, RowHandler rowHandler) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForPaginatedList(String, int) + * @deprecated as of iBATIS 2.3.0 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + PaginatedList queryForPaginatedList(String statementName, int pageSize) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForPaginatedList(String, Object, int) + * @deprecated as of iBATIS 2.3.0 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + PaginatedList queryForPaginatedList(String statementName, Object parameterObject, int pageSize) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForMap(String, Object, String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Map queryForMap(String statementName, Object parameterObject, String keyProperty) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#queryForMap(String, Object, String, String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Map queryForMap(String statementName, Object parameterObject, String keyProperty, String valueProperty) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#insert(String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Object insert(String statementName) throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#insert(String, Object) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + Object insert(String statementName, Object parameterObject) throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#update(String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + int update(String statementName) throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#update(String, Object) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + int update(String statementName, Object parameterObject) throws DataAccessException; + + /** + * Convenience method provided by Spring: execute an update operation + * with an automatic check that the update affected the given required + * number of rows. + * @param statementName the name of the mapped statement + * @param parameterObject the parameter object + * @param requiredRowsAffected the number of rows that the update is + * required to affect + * @throws org.springframework.dao.DataAccessException in case of errors + */ + void update(String statementName, Object parameterObject, int requiredRowsAffected) + throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#delete(String) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + int delete(String statementName) throws DataAccessException; + + /** + * @see com.ibatis.sqlmap.client.SqlMapExecutor#delete(String, Object) + * @throws org.springframework.dao.DataAccessException in case of errors + */ + int delete(String statementName, Object parameterObject) throws DataAccessException; + + /** + * Convenience method provided by Spring: execute a delete operation + * with an automatic check that the delete affected the given required + * number of rows. + * @param statementName the name of the mapped statement + * @param parameterObject the parameter object + * @param requiredRowsAffected the number of rows that the delete is + * required to affect + * @throws org.springframework.dao.DataAccessException in case of errors + */ + void delete(String statementName, Object parameterObject, int requiredRowsAffected) + throws DataAccessException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientTemplate.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientTemplate.java new file mode 100644 index 00000000000..27671dfdaa2 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/SqlMapClientTemplate.java @@ -0,0 +1,454 @@ +/* + * Copyright 2002-2007 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.ibatis; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import com.ibatis.common.util.PaginatedList; +import com.ibatis.sqlmap.client.SqlMapClient; +import com.ibatis.sqlmap.client.SqlMapExecutor; +import com.ibatis.sqlmap.client.SqlMapSession; +import com.ibatis.sqlmap.client.event.RowHandler; +import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.JdbcAccessor; +import org.springframework.util.Assert; + +/** + * Helper class that simplifies data access via the iBATIS + * {@link com.ibatis.sqlmap.client.SqlMapClient} API, converting checked + * SQLExceptions into unchecked DataAccessExceptions, following the + * org.springframework.dao exception hierarchy. + * Uses the same {@link org.springframework.jdbc.support.SQLExceptionTranslator} + * mechanism as {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

The main method of this class executes a callback that implements a + * data access action. Furthermore, this class provides numerous convenience + * methods that mirror {@link com.ibatis.sqlmap.client.SqlMapExecutor}'s + * execution methods. + * + *

It is generally recommended to use the convenience methods on this template + * for plain query/insert/update/delete operations. However, for more complex + * operations like batch updates, a custom SqlMapClientCallback must be implemented, + * usually as anonymous inner class. For example: + * + *

+ * getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
+ * 	 public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
+ * 		 executor.startBatch();
+ * 		 executor.update("insertSomething", "myParamValue");
+ * 		 executor.update("insertSomethingElse", "myOtherParamValue");
+ * 		 executor.executeBatch();
+ * 		 return null;
+ * 	 }
+ * });
+ * + * The template needs a SqlMapClient to work on, passed in via the "sqlMapClient" + * property. A Spring context typically uses a {@link SqlMapClientFactoryBean} + * to build the SqlMapClient. The template an additionally be configured with a + * DataSource for fetching Connections, although this is not necessary if a + * DataSource is specified for the SqlMapClient itself (typically through + * SqlMapClientFactoryBean's "dataSource" property). + * + * @author Juergen Hoeller + * @since 24.02.2004 + * @see #execute + * @see #setSqlMapClient + * @see #setDataSource + * @see #setExceptionTranslator + * @see SqlMapClientFactoryBean#setDataSource + * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource + * @see com.ibatis.sqlmap.client.SqlMapExecutor + */ +public class SqlMapClientTemplate extends JdbcAccessor implements SqlMapClientOperations { + + private SqlMapClient sqlMapClient; + + private boolean lazyLoadingAvailable = true; + + + /** + * Create a new SqlMapClientTemplate. + */ + public SqlMapClientTemplate() { + } + + /** + * Create a new SqlMapTemplate. + * @param sqlMapClient iBATIS SqlMapClient that defines the mapped statements + */ + public SqlMapClientTemplate(SqlMapClient sqlMapClient) { + setSqlMapClient(sqlMapClient); + afterPropertiesSet(); + } + + /** + * Create a new SqlMapTemplate. + * @param dataSource JDBC DataSource to obtain connections from + * @param sqlMapClient iBATIS SqlMapClient that defines the mapped statements + */ + public SqlMapClientTemplate(DataSource dataSource, SqlMapClient sqlMapClient) { + setDataSource(dataSource); + setSqlMapClient(sqlMapClient); + afterPropertiesSet(); + } + + + /** + * Set the iBATIS Database Layer SqlMapClient that defines the mapped statements. + */ + public void setSqlMapClient(SqlMapClient sqlMapClient) { + this.sqlMapClient = sqlMapClient; + } + + /** + * Return the iBATIS Database Layer SqlMapClient that this template works with. + */ + public SqlMapClient getSqlMapClient() { + return this.sqlMapClient; + } + + /** + * If no DataSource specified, use SqlMapClient's DataSource. + * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource() + */ + public DataSource getDataSource() { + DataSource ds = super.getDataSource(); + return (ds != null ? ds : this.sqlMapClient.getDataSource()); + } + + public void afterPropertiesSet() { + if (this.sqlMapClient == null) { + throw new IllegalArgumentException("Property 'sqlMapClient' is required"); + } + if (this.sqlMapClient instanceof ExtendedSqlMapClient) { + // Check whether iBATIS lazy loading is available, that is, + // whether a DataSource was specified on the SqlMapClient itself. + this.lazyLoadingAvailable = (((ExtendedSqlMapClient) this.sqlMapClient).getDelegate().getTxManager() != null); + } + super.afterPropertiesSet(); + } + + + /** + * Execute the given data access action on a SqlMapExecutor. + * @param action callback object that specifies the data access action + * @return a result object returned by the action, or null + * @throws DataAccessException in case of SQL Maps errors + */ + public Object execute(SqlMapClientCallback action) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + Assert.notNull(this.sqlMapClient, "No SqlMapClient specified"); + + // We always needs to use a SqlMapSession, as we need to pass a Spring-managed + // Connection (potentially transactional) in. This shouldn't be necessary if + // we run against a TransactionAwareDataSourceProxy underneath, but unfortunately + // we still need it to make iBATIS batch execution work properly: If iBATIS + // doesn't recognize an existing transaction, it automatically executes the + // batch for every single statement... + + SqlMapSession session = this.sqlMapClient.openSession(); + if (logger.isDebugEnabled()) { + logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation"); + } + Connection ibatisCon = null; + + try { + Connection springCon = null; + DataSource dataSource = getDataSource(); + boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy); + + // Obtain JDBC Connection to operate on... + try { + ibatisCon = session.getCurrentConnection(); + if (ibatisCon == null) { + springCon = (transactionAware ? + dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource)); + session.setUserConnection(springCon); + if (logger.isDebugEnabled()) { + logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation"); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation"); + } + } + } + catch (SQLException ex) { + throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); + } + + // Execute given callback... + try { + return action.doInSqlMapClient(session); + } + catch (SQLException ex) { + throw getExceptionTranslator().translate("SqlMapClient operation", null, ex); + } + finally { + try { + if (springCon != null) { + if (transactionAware) { + springCon.close(); + } + else { + DataSourceUtils.doReleaseConnection(springCon, dataSource); + } + } + } + catch (Throwable ex) { + logger.debug("Could not close JDBC Connection", ex); + } + } + + // Processing finished - potentially session still to be closed. + } + finally { + // Only close SqlMapSession if we know we've actually opened it + // at the present level. + if (ibatisCon == null) { + session.close(); + } + } + } + + /** + * Execute the given data access action on a SqlMapExecutor, + * expecting a List result. + * @param action callback object that specifies the data access action + * @return the List result + * @throws DataAccessException in case of SQL Maps errors + */ + public List executeWithListResult(SqlMapClientCallback action) throws DataAccessException { + return (List) execute(action); + } + + /** + * Execute the given data access action on a SqlMapExecutor, + * expecting a Map result. + * @param action callback object that specifies the data access action + * @return the Map result + * @throws DataAccessException in case of SQL Maps errors + */ + public Map executeWithMapResult(SqlMapClientCallback action) throws DataAccessException { + return (Map) execute(action); + } + + + public Object queryForObject(String statementName) throws DataAccessException { + return queryForObject(statementName, null); + } + + public Object queryForObject(final String statementName, final Object parameterObject) + throws DataAccessException { + + return execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForObject(statementName, parameterObject); + } + }); + } + + public Object queryForObject( + final String statementName, final Object parameterObject, final Object resultObject) + throws DataAccessException { + + return execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForObject(statementName, parameterObject, resultObject); + } + }); + } + + public List queryForList(String statementName) throws DataAccessException { + return queryForList(statementName, null); + } + + public List queryForList(final String statementName, final Object parameterObject) + throws DataAccessException { + + return executeWithListResult(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForList(statementName, parameterObject); + } + }); + } + + public List queryForList(String statementName, int skipResults, int maxResults) + throws DataAccessException { + + return queryForList(statementName, null, skipResults, maxResults); + } + + public List queryForList( + final String statementName, final Object parameterObject, final int skipResults, final int maxResults) + throws DataAccessException { + + return executeWithListResult(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForList(statementName, parameterObject, skipResults, maxResults); + } + }); + } + + public void queryWithRowHandler(String statementName, RowHandler rowHandler) + throws DataAccessException { + + queryWithRowHandler(statementName, null, rowHandler); + } + + public void queryWithRowHandler( + final String statementName, final Object parameterObject, final RowHandler rowHandler) + throws DataAccessException { + + execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + executor.queryWithRowHandler(statementName, parameterObject, rowHandler); + return null; + } + }); + } + + /** + * @deprecated as of iBATIS 2.3.0 + */ + public PaginatedList queryForPaginatedList(String statementName, int pageSize) + throws DataAccessException { + + return queryForPaginatedList(statementName, null, pageSize); + } + + /** + * @deprecated as of iBATIS 2.3.0 + */ + public PaginatedList queryForPaginatedList( + final String statementName, final Object parameterObject, final int pageSize) + throws DataAccessException { + + // Throw exception if lazy loading will not work. + if (!this.lazyLoadingAvailable) { + throw new InvalidDataAccessApiUsageException( + "SqlMapClient needs to have DataSource to allow for lazy loading" + + " - specify SqlMapClientFactoryBean's 'dataSource' property"); + } + + return (PaginatedList) execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForPaginatedList(statementName, parameterObject, pageSize); + } + }); + } + + public Map queryForMap( + final String statementName, final Object parameterObject, final String keyProperty) + throws DataAccessException { + + return executeWithMapResult(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForMap(statementName, parameterObject, keyProperty); + } + }); + } + + public Map queryForMap( + final String statementName, final Object parameterObject, final String keyProperty, final String valueProperty) + throws DataAccessException { + + return executeWithMapResult(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.queryForMap(statementName, parameterObject, keyProperty, valueProperty); + } + }); + } + + public Object insert(String statementName) throws DataAccessException { + return insert(statementName, null); + } + + public Object insert(final String statementName, final Object parameterObject) + throws DataAccessException { + + return execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return executor.insert(statementName, parameterObject); + } + }); + } + + public int update(String statementName) throws DataAccessException { + return update(statementName, null); + } + + public int update(final String statementName, final Object parameterObject) + throws DataAccessException { + + Integer result = (Integer) execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return new Integer(executor.update(statementName, parameterObject)); + } + }); + return result.intValue(); + } + + public void update(String statementName, Object parameterObject, int requiredRowsAffected) + throws DataAccessException { + + int actualRowsAffected = update(statementName, parameterObject); + if (actualRowsAffected != requiredRowsAffected) { + throw new JdbcUpdateAffectedIncorrectNumberOfRowsException( + statementName, requiredRowsAffected, actualRowsAffected); + } + } + + public int delete(String statementName) throws DataAccessException { + return delete(statementName, null); + } + + public int delete(final String statementName, final Object parameterObject) + throws DataAccessException { + + Integer result = (Integer) execute(new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { + return new Integer(executor.delete(statementName, parameterObject)); + } + }); + return result.intValue(); + } + + public void delete(String statementName, Object parameterObject, int requiredRowsAffected) + throws DataAccessException { + + int actualRowsAffected = delete(statementName, parameterObject); + if (actualRowsAffected != requiredRowsAffected) { + throw new JdbcUpdateAffectedIncorrectNumberOfRowsException( + statementName, requiredRowsAffected, actualRowsAffected); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/package.html new file mode 100644 index 00000000000..ebae95b87d1 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/package.html @@ -0,0 +1,12 @@ + + + +Package providing integration of +iBATIS Database Layer +with Spring concepts. + +

Contains resource helper classes and template classes for +data access with the iBATIS SqlMapClient API. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/AbstractLobTypeHandler.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/AbstractLobTypeHandler.java new file mode 100644 index 00000000000..53e8922340c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/AbstractLobTypeHandler.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2005 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.ibatis.support; + +import java.io.IOException; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import com.ibatis.sqlmap.engine.type.BaseTypeHandler; + +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.orm.ibatis.SqlMapClientFactoryBean; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Abstract base class for iBATIS TypeHandler implementations that map to LOBs. + * Retrieves the LobHandler to use from SqlMapClientFactoryBean at config time. + * + *

For writing LOBs, an active Spring transaction synchronization is required, + * to be able to register a synchronization that closes the LobCreator. + * + *

Offers template methods for setting parameters and getting result values, + * passing in the LobHandler or LobCreator to use. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.springframework.jdbc.support.lob.LobHandler + * @see org.springframework.jdbc.support.lob.LobCreator + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#setLobHandler + */ +public abstract class AbstractLobTypeHandler extends BaseTypeHandler { + + /** + * Order value for TransactionSynchronization objects that clean up LobCreators. + * Return DataSourceUtils.#CONNECTION_SYNCHRONIZATION_ORDER - 100 to execute + * LobCreator cleanup before JDBC Connection cleanup, if any. + * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER + */ + public static final int LOB_CREATOR_SYNCHRONIZATION_ORDER = + DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 200; + + private LobHandler lobHandler; + + + /** + * Constructor used by iBATIS: fetches config-time LobHandler from + * SqlMapClientFactoryBean. + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#getConfigTimeLobHandler + */ + public AbstractLobTypeHandler() { + this(SqlMapClientFactoryBean.getConfigTimeLobHandler()); + } + + /** + * Constructor used for testing: takes an explicit LobHandler. + */ + protected AbstractLobTypeHandler(LobHandler lobHandler) { + if (lobHandler == null) { + throw new IllegalStateException("No LobHandler found for configuration - " + + "lobHandler property must be set on SqlMapClientFactoryBean"); + } + this.lobHandler = lobHandler; + } + + + /** + * This implementation delegates to setParameterInternal, + * passing in a transaction-synchronized LobCreator for the + * LobHandler of this type. + * @see #setParameterInternal + */ + public final void setParameter(PreparedStatement ps, int i, Object parameter, String jdbcType) + throws SQLException { + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + throw new IllegalStateException("Spring transaction synchronization needs to be active for " + + "setting values in iBATIS TypeHandlers that delegate to a Spring LobHandler"); + } + final LobCreator lobCreator = this.lobHandler.getLobCreator(); + try { + setParameterInternal(ps, i, parameter, jdbcType, lobCreator); + } + catch (IOException ex) { + throw new SQLException("I/O errors during LOB access: " + ex.getMessage()); + } + + TransactionSynchronizationManager.registerSynchronization( + new LobCreatorSynchronization(lobCreator)); + } + + /** + * This implementation delegates to the getResult version + * that takes a column index. + * @see #getResult(java.sql.ResultSet, String) + * @see java.sql.ResultSet#findColumn + */ + public final Object getResult(ResultSet rs, String columnName) throws SQLException { + return getResult(rs, rs.findColumn(columnName)); + } + + /** + * This implementation delegates to getResultInternal, + * passing in the LobHandler of this type. + * @see #getResultInternal + */ + public final Object getResult(ResultSet rs, int columnIndex) throws SQLException { + try { + return getResultInternal(rs, columnIndex, this.lobHandler); + } + catch (IOException ex) { + throw new SQLException( + "I/O errors during LOB access: " + ex.getClass().getName() + ": " + ex.getMessage()); + } + } + + /** + * This implementation always throws a SQLException: + * retrieving LOBs from a CallableStatement is not supported. + */ + public Object getResult(CallableStatement cs, int columnIndex) throws SQLException { + throw new SQLException("Retrieving LOBs from a CallableStatement is not supported"); + } + + + /** + * Template method to set the given value on the given statement. + * @param ps the PreparedStatement to set on + * @param index the statement parameter index + * @param value the parameter value to set + * @param jdbcType the JDBC type of the parameter + * @param lobCreator the LobCreator to use + * @throws SQLException if thrown by JDBC methods + * @throws IOException if thrown by streaming methods + */ + protected abstract void setParameterInternal( + PreparedStatement ps, int index, Object value, String jdbcType, LobCreator lobCreator) + throws SQLException, IOException; + + /** + * Template method to extract a value from the given result set. + * @param rs the ResultSet to extract from + * @param index the index in the ResultSet + * @param lobHandler the LobHandler to use + * @return the extracted value + * @throws SQLException if thrown by JDBC methods + * @throws IOException if thrown by streaming methods + */ + protected abstract Object getResultInternal(ResultSet rs, int index, LobHandler lobHandler) + throws SQLException, IOException; + + + /** + * Callback for resource cleanup at the end of a Spring transaction. + * Invokes LobCreator.close to clean up temporary LOBs that might have been created. + * @see org.springframework.jdbc.support.lob.LobCreator#close + */ + private static class LobCreatorSynchronization extends TransactionSynchronizationAdapter { + + private final LobCreator lobCreator; + + public LobCreatorSynchronization(LobCreator lobCreator) { + this.lobCreator = lobCreator; + } + + public int getOrder() { + return LOB_CREATOR_SYNCHRONIZATION_ORDER; + } + + public void beforeCompletion() { + this.lobCreator.close(); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/BlobByteArrayTypeHandler.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/BlobByteArrayTypeHandler.java new file mode 100644 index 00000000000..589ce14a7ef --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/BlobByteArrayTypeHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2005 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.ibatis.support; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * iBATIS TypeHandler implementation for byte arrays that get mapped to BLOBs. + * Retrieves the LobHandler to use from SqlMapClientFactoryBean at config time. + * + *

Can also be defined in generic iBATIS mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be BLOB: For databases like MySQL and MS SQL Server, any + * large enough binary type will work. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#setLobHandler + */ +public class BlobByteArrayTypeHandler extends AbstractLobTypeHandler { + + /** + * Constructor used by iBATIS: fetches config-time LobHandler from + * SqlMapClientFactoryBean. + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#getConfigTimeLobHandler + */ + public BlobByteArrayTypeHandler() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler. + */ + protected BlobByteArrayTypeHandler(LobHandler lobHandler) { + super(lobHandler); + } + + protected void setParameterInternal( + PreparedStatement ps, int index, Object value, String jdbcType, LobCreator lobCreator) + throws SQLException { + lobCreator.setBlobAsBytes(ps, index, (byte[]) value); + } + + protected Object getResultInternal(ResultSet rs, int index, LobHandler lobHandler) + throws SQLException { + return lobHandler.getBlobAsBytes(rs, index); + } + + public Object valueOf(String s) { + return s.getBytes(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/BlobSerializableTypeHandler.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/BlobSerializableTypeHandler.java new file mode 100644 index 00000000000..f392d521a55 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/BlobSerializableTypeHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2005 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.ibatis.support; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * iBATIS TypeHandler implementation for arbitrary objects that get serialized to BLOBs. + * Retrieves the LobHandler to use from SqlMapClientFactoryBean at config time. + * + *

Can also be defined in generic iBATIS mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be BLOB: For databases like MySQL and MS SQL Server, any + * large enough binary type will work. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#setLobHandler + */ +public class BlobSerializableTypeHandler extends AbstractLobTypeHandler { + + /** + * Constructor used by iBATIS: fetches config-time LobHandler from + * SqlMapClientFactoryBean. + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#getConfigTimeLobHandler + */ + public BlobSerializableTypeHandler() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler. + */ + protected BlobSerializableTypeHandler(LobHandler lobHandler) { + super(lobHandler); + } + + protected void setParameterInternal( + PreparedStatement ps, int index, Object value, String jdbcType, LobCreator lobCreator) + throws SQLException, IOException { + + if (value != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + try { + oos.writeObject(value); + oos.flush(); + lobCreator.setBlobAsBytes(ps, index, baos.toByteArray()); + } + finally { + oos.close(); + } + } + else { + lobCreator.setBlobAsBytes(ps, index, null); + } + } + + protected Object getResultInternal(ResultSet rs, int index, LobHandler lobHandler) + throws SQLException, IOException { + + InputStream is = lobHandler.getBlobAsBinaryStream(rs, index); + if (is != null) { + ObjectInputStream ois = new ObjectInputStream(is); + try { + return ois.readObject(); + } + catch (ClassNotFoundException ex) { + throw new SQLException("Could not deserialize BLOB contents: " + ex.getMessage()); + } + finally { + ois.close(); + } + } + else { + return null; + } + } + + public Object valueOf(String s) { + return s; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/ClobStringTypeHandler.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/ClobStringTypeHandler.java new file mode 100644 index 00000000000..e4fe32c1680 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/ClobStringTypeHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2005 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.ibatis.support; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +/** + * iBATIS TypeHandler implementation for Strings that get mapped to CLOBs. + * Retrieves the LobHandler to use from SqlMapClientFactoryBean at config time. + * + *

Particularly useful for storing Strings with more than 4000 characters in an + * Oracle database (only possible via CLOBs), in combination with OracleLobHandler. + * + *

Can also be defined in generic iBATIS mappings, as DefaultLobCreator will + * work with most JDBC-compliant database drivers. In this case, the field type + * does not have to be BLOB: For databases like MySQL and MS SQL Server, any + * large enough binary type will work. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#setLobHandler + */ +public class ClobStringTypeHandler extends AbstractLobTypeHandler { + + /** + * Constructor used by iBATIS: fetches config-time LobHandler from + * SqlMapClientFactoryBean. + * @see org.springframework.orm.ibatis.SqlMapClientFactoryBean#getConfigTimeLobHandler + */ + public ClobStringTypeHandler() { + super(); + } + + /** + * Constructor used for testing: takes an explicit LobHandler. + */ + protected ClobStringTypeHandler(LobHandler lobHandler) { + super(lobHandler); + } + + protected void setParameterInternal( + PreparedStatement ps, int index, Object value, String jdbcType, LobCreator lobCreator) + throws SQLException { + lobCreator.setClobAsString(ps, index, (String) value); + } + + protected Object getResultInternal(ResultSet rs, int index, LobHandler lobHandler) + throws SQLException { + return lobHandler.getClobAsString(rs, index); + } + + public Object valueOf(String s) { + return s; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/SqlMapClientDaoSupport.java b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/SqlMapClientDaoSupport.java new file mode 100644 index 00000000000..195b129b925 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/SqlMapClientDaoSupport.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2008 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.ibatis.support; + +import javax.sql.DataSource; + +import com.ibatis.sqlmap.client.SqlMapClient; + +import org.springframework.dao.support.DaoSupport; +import org.springframework.orm.ibatis.SqlMapClientTemplate; +import org.springframework.util.Assert; + +/** + * Convenient super class for iBATIS SqlMapClient data access objects. + * Requires a SqlMapClient to be set, providing a SqlMapClientTemplate + * based on it to subclasses. + * + *

Instead of a plain SqlMapClient, you can also pass a preconfigured + * SqlMapClientTemplate instance in. This allows you to share your + * SqlMapClientTemplate configuration for all your DAOs, for example + * a custom SQLExceptionTranslator to use. + * + * @author Juergen Hoeller + * @since 24.02.2004 + * @see #setSqlMapClient + * @see #setSqlMapClientTemplate + * @see org.springframework.orm.ibatis.SqlMapClientTemplate + * @see org.springframework.orm.ibatis.SqlMapClientTemplate#setExceptionTranslator + */ +public abstract class SqlMapClientDaoSupport extends DaoSupport { + + private SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(); + + private boolean externalTemplate = false; + + + /** + * Set the JDBC DataSource to be used by this DAO. + * Not required: The SqlMapClient might carry a shared DataSource. + * @see #setSqlMapClient + */ + public final void setDataSource(DataSource dataSource) { + if (!this.externalTemplate) { + this.sqlMapClientTemplate.setDataSource(dataSource); + } + } + + /** + * Return the JDBC DataSource used by this DAO. + */ + public final DataSource getDataSource() { + return this.sqlMapClientTemplate.getDataSource(); + } + + /** + * Set the iBATIS Database Layer SqlMapClient to work with. + * Either this or a "sqlMapClientTemplate" is required. + * @see #setSqlMapClientTemplate + */ + public final void setSqlMapClient(SqlMapClient sqlMapClient) { + if (!this.externalTemplate) { + this.sqlMapClientTemplate.setSqlMapClient(sqlMapClient); + } + } + + /** + * Return the iBATIS Database Layer SqlMapClient that this template works with. + */ + public final SqlMapClient getSqlMapClient() { + return this.sqlMapClientTemplate.getSqlMapClient(); + } + + /** + * Set the SqlMapClientTemplate for this DAO explicitly, + * as an alternative to specifying a SqlMapClient. + * @see #setSqlMapClient + */ + public final void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) { + Assert.notNull(sqlMapClientTemplate, "SqlMapClientTemplate must not be null"); + this.sqlMapClientTemplate = sqlMapClientTemplate; + this.externalTemplate = true; + } + + /** + * Return the SqlMapClientTemplate for this DAO, + * pre-initialized with the SqlMapClient or set explicitly. + */ + public final SqlMapClientTemplate getSqlMapClientTemplate() { + return this.sqlMapClientTemplate; + } + + protected final void checkDaoConfig() { + if (!this.externalTemplate) { + this.sqlMapClientTemplate.afterPropertiesSet(); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/package.html new file mode 100644 index 00000000000..ad799b74950 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/ibatis/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.orm.ibatis package. +Contains a DAO base class for SqlMapClientTemplate usage. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/DefaultJdoDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/DefaultJdoDialect.java new file mode 100644 index 00000000000..267613d619a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/DefaultJdoDialect.java @@ -0,0 +1,269 @@ +/* + * Copyright 2002-2007 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.jdo; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.Query; +import javax.jdo.Transaction; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * Default implementation of the {@link JdoDialect} interface. + * Updated to build on JDO 2.0 or higher, as of Spring 2.5. + * Used as default dialect by {@link JdoAccessor} and {@link JdoTransactionManager}. + * + *

Simply begins a standard JDO transaction in beginTransaction. + * Returns a handle for a JDO2 DataStoreConnection on getJdbcConnection. + * Calls the corresponding JDO2 PersistenceManager operation on flush + * Ignores a given query timeout in applyQueryTimeout. + * Uses a Spring SQLExceptionTranslator for exception translation, if applicable. + * + *

Note that, even with JDO2, vendor-specific subclasses are still necessary + * for special transaction semantics and more sophisticated exception translation. + * Furthermore, vendor-specific subclasses are encouraged to expose the native JDBC + * Connection on getJdbcConnection, rather than JDO2's wrapper handle. + * + *

This class also implements the PersistenceExceptionTranslator interface, + * as autodetected by Spring's PersistenceExceptionTranslationPostProcessor, + * for AOP-based translation of native exceptions to Spring DataAccessExceptions. + * Hence, the presence of a standard DefaultJdoDialect bean automatically enables + * a PersistenceExceptionTranslationPostProcessor to translate JDO exceptions. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJdbcExceptionTranslator + * @see JdoAccessor#setJdoDialect + * @see JdoTransactionManager#setJdoDialect + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + */ +public class DefaultJdoDialect implements JdoDialect, PersistenceExceptionTranslator { + + protected final Log logger = LogFactory.getLog(getClass()); + + private SQLExceptionTranslator jdbcExceptionTranslator; + + + /** + * Create a new DefaultJdoDialect. + */ + public DefaultJdoDialect() { + } + + /** + * Create a new DefaultJdoDialect. + * @param connectionFactory the connection factory of the JDO PersistenceManagerFactory, + * which is used to initialize the default JDBC exception translator + * @see javax.jdo.PersistenceManagerFactory#getConnectionFactory() + * @see PersistenceManagerFactoryUtils#newJdbcExceptionTranslator(Object) + */ + DefaultJdoDialect(Object connectionFactory) { + this.jdbcExceptionTranslator = PersistenceManagerFactoryUtils.newJdbcExceptionTranslator(connectionFactory); + } + + /** + * Set the JDBC exception translator for this dialect. + *

Applied to any SQLException root cause of a JDOException, if specified. + * The default is to rely on the JDO provider's native exception translation. + * @param jdbcExceptionTranslator exception translator + * @see java.sql.SQLException + * @see javax.jdo.JDOException#getCause() + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + /** + * Return the JDBC exception translator for this dialect, if any. + */ + public SQLExceptionTranslator getJdbcExceptionTranslator() { + return this.jdbcExceptionTranslator; + } + + + //------------------------------------------------------------------------- + // Hooks for transaction management (used by JdoTransactionManager) + //------------------------------------------------------------------------- + + /** + * This implementation invokes the standard JDO Transaction.begin + * method. Throws an InvalidIsolationLevelException if a non-default isolation + * level is set. + * @see javax.jdo.Transaction#begin + * @see org.springframework.transaction.InvalidIsolationLevelException + */ + public Object beginTransaction(Transaction transaction, TransactionDefinition definition) + throws JDOException, SQLException, TransactionException { + + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + throw new InvalidIsolationLevelException( + "Standard JDO does not support custom isolation levels: " + + "use a special JdoDialect implementation for your JDO provider"); + } + transaction.begin(); + return null; + } + + /** + * This implementation does nothing, as the default beginTransaction implementation + * does not require any cleanup. + * @see #beginTransaction + */ + public void cleanupTransaction(Object transactionData) { + } + + /** + * This implementation returns a DataStoreConnectionHandle for JDO2, + * which will also work on JDO1 until actually accessing the JDBC Connection. + *

For pre-JDO2 implementations, override this method to return the + * Connection through the corresponding vendor-specific mechanism, or null + * if the Connection is not retrievable. + *

NOTE: A JDO2 DataStoreConnection is always a wrapper, + * never the native JDBC Connection. If you need access to the native JDBC + * Connection (or the connection pool handle, to be unwrapped via a Spring + * NativeJdbcExtractor), override this method to return the native + * Connection through the corresponding vendor-specific mechanism. + *

A JDO2 DataStoreConnection is only "borrowed" from the PersistenceManager: + * it needs to be returned as early as possible. Effectively, JDO2 requires the + * fetched Connection to be closed before continuing PersistenceManager work. + * For this reason, the exposed ConnectionHandle eagerly releases its JDBC + * Connection at the end of each JDBC data access operation (that is, on + * DataSourceUtils.releaseConnection). + * @see javax.jdo.PersistenceManager#getDataStoreConnection() + * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + public ConnectionHandle getJdbcConnection(PersistenceManager pm, boolean readOnly) + throws JDOException, SQLException { + + return new DataStoreConnectionHandle(pm); + } + + /** + * This implementation does nothing, assuming that the Connection + * will implicitly be closed with the PersistenceManager. + *

If the JDO provider returns a Connection handle that it + * expects the application to close, the dialect needs to invoke + * Connection.close here. + * @see java.sql.Connection#close() + */ + public void releaseJdbcConnection(ConnectionHandle conHandle, PersistenceManager pm) + throws JDOException, SQLException { + } + + /** + * This implementation delegates to JDO 2.0's flush method. + *

To be overridden for pre-JDO2 implementations, using the corresponding + * vendor-specific mechanism there. + * @see javax.jdo.PersistenceManager#flush() + */ + public void flush(PersistenceManager pm) throws JDOException { + pm.flush(); + } + + /** + * This implementation logs a warning that it cannot apply a query timeout. + */ + public void applyQueryTimeout(Query query, int remainingTimeInSeconds) throws JDOException { + logger.info("DefaultJdoDialect does not support query timeouts: ignoring remaining transaction time"); + } + + + //----------------------------------------------------------------------------------- + // Hook for exception translation (used by JdoTransactionManager and JdoTemplate) + //----------------------------------------------------------------------------------- + + /** + * Implementation of the PersistenceExceptionTranslator interface, + * as autodetected by Spring's PersistenceExceptionTranslationPostProcessor. + *

Converts the exception if it is a JDOException, using this JdoDialect. + * Else returns null to indicate an unknown exception. + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * @see #translateException + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof JDOException) { + return translateException((JDOException) ex); + } + return null; + } + + /** + * This implementation delegates to PersistenceManagerFactoryUtils. + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ + public DataAccessException translateException(JDOException ex) { + if (getJdbcExceptionTranslator() != null && ex.getCause() instanceof SQLException) { + return getJdbcExceptionTranslator().translate("JDO operation: " + ex.getMessage(), + extractSqlStringFromException(ex), (SQLException) ex.getCause()); + } + return PersistenceManagerFactoryUtils.convertJdoAccessException(ex); + } + + /** + * Template method for extracting a SQL String from the given exception. + *

Default implementation always returns null. Can be overridden in + * subclasses to extract SQL Strings for vendor-specific exception classes. + * @param ex the JDOException, containing a SQLException + * @return the SQL String, or null if none found + */ + protected String extractSqlStringFromException(JDOException ex) { + return null; + } + + + /** + * ConnectionHandle implementation that fetches a new JDO2 DataStoreConnection + * for every getConnection call and closes the Connection on + * releaseConnection. This is necessary because JDO2 requires the + * fetched Connection to be closed before continuing PersistenceManager work. + * @see javax.jdo.PersistenceManager#getDataStoreConnection() + */ + private static class DataStoreConnectionHandle implements ConnectionHandle { + + private final PersistenceManager persistenceManager; + + public DataStoreConnectionHandle(PersistenceManager persistenceManager) { + this.persistenceManager = persistenceManager; + } + + public Connection getConnection() { + return (Connection) this.persistenceManager.getDataStoreConnection(); + } + + public void releaseConnection(Connection con) { + JdbcUtils.closeConnection(con); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoAccessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoAccessor.java new file mode 100644 index 00000000000..cd7b17a583a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoAccessor.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2006 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.jdo; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; + +/** + * Base class for JdoTemplate and JdoInterceptor, defining common + * properties such as PersistenceManagerFactory and flushing behavior. + * + *

Note: With JDO, modifications to persistent objects are just possible + * within a transaction (in contrast to Hibernate). Therefore, eager flushing + * will just get applied when in a transaction. Furthermore, there is no + * explicit notion of flushing never, as this would not imply a performance + * gain due to JDO's field interception mechanism (which doesn't involve + * the overhead of snapshot comparisons). + * + *

Eager flushing is just available for specific JDO providers. + * You need to a corresponding JdoDialect to make eager flushing work. + * + *

Not intended to be used directly. See JdoTemplate and JdoInterceptor. + * + * @author Juergen Hoeller + * @since 02.11.2003 + * @see JdoTemplate + * @see JdoInterceptor + * @see #setFlushEager + */ +public abstract class JdoAccessor implements InitializingBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private PersistenceManagerFactory persistenceManagerFactory; + + private JdoDialect jdoDialect; + + private boolean flushEager = false; + + + /** + * Set the JDO PersistenceManagerFactory that should be used to create + * PersistenceManagers. + */ + public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) { + this.persistenceManagerFactory = pmf; + } + + /** + * Return the JDO PersistenceManagerFactory that should be used to create + * PersistenceManagers. + */ + public PersistenceManagerFactory getPersistenceManagerFactory() { + return persistenceManagerFactory; + } + + /** + * Set the JDO dialect to use for this accessor. + *

The dialect object can be used to retrieve the underlying JDBC + * connection and to eagerly flush changes to the database. + *

Default is a DefaultJdoDialect based on the PersistenceManagerFactory's + * underlying DataSource, if any. + */ + public void setJdoDialect(JdoDialect jdoDialect) { + this.jdoDialect = jdoDialect; + } + + /** + * Return the JDO dialect to use for this accessor. + *

Creates a default one for the specified PersistenceManagerFactory if none set. + */ + public JdoDialect getJdoDialect() { + if (this.jdoDialect == null) { + this.jdoDialect = new DefaultJdoDialect(); + } + return this.jdoDialect; + } + + /** + * Set if this accessor should flush changes to the database eagerly. + *

Eager flushing leads to immediate synchronization with the database, + * even if in a transaction. This causes inconsistencies to show up and throw + * a respective exception immediately, and JDBC access code that participates + * in the same transaction will see the changes as the database is already + * aware of them then. But the drawbacks are: + *

+ */ + public void setFlushEager(boolean flushEager) { + this.flushEager = flushEager; + } + + /** + * Return if this accessor should flush changes to the database eagerly. + */ + public boolean isFlushEager() { + return flushEager; + } + + /** + * Eagerly initialize the JDO dialect, creating a default one + * for the specified PersistenceManagerFactory if none set. + */ + public void afterPropertiesSet() { + if (getPersistenceManagerFactory() == null) { + throw new IllegalArgumentException("persistenceManagerFactory is required"); + } + // Build default JdoDialect if none explicitly specified. + if (this.jdoDialect == null) { + this.jdoDialect = new DefaultJdoDialect(getPersistenceManagerFactory().getConnectionFactory()); + } + } + + + /** + * Flush the given JDO persistence manager if necessary. + * @param pm the current JDO PersistenceManager + * @param existingTransaction if executing within an existing transaction + * (within an existing JDO PersistenceManager that won't be closed immediately) + * @throws JDOException in case of JDO flushing errors + */ + protected void flushIfNecessary(PersistenceManager pm, boolean existingTransaction) throws JDOException { + if (isFlushEager()) { + logger.debug("Eagerly flushing JDO persistence manager"); + getJdoDialect().flush(pm); + } + } + + /** + * Convert the given JDOException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Default implementation delegates to the JdoDialect. + * May be overridden in subclasses. + * @param ex JDOException that occured + * @return the corresponding DataAccessException instance + * @see JdoDialect#translateException + */ + public DataAccessException convertJdoAccessException(JDOException ex) { + return getJdoDialect().translateException(ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoCallback.java new file mode 100644 index 00000000000..ebcb2e72012 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoCallback.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 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.jdo; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; + +/** + * Callback interface for JDO code. To be used with {@link JdoTemplate}'s + * execution methods, often as anonymous classes within a method implementation. + * A typical implementation will call PersistenceManager CRUD to perform + * some operations on persistent objects. + * + *

Note that JDO works on bytecode-modified Java objects, to be able to + * perform dirty detection on each modification of a persistent instance field. + * In contrast to Hibernate, using returned objects outside of an active + * PersistenceManager poses a problem: To be able to read and modify fields + * e.g. in a web GUI, one has to explicitly make the instances "transient". + * Reassociation with a new PersistenceManager, e.g. for updates when coming + * back from the GUI, isn't possible, as the JDO instances have lost their + * identity when turned transient. This means that either value objects have + * to be used as parameters, or the contents of the outside-modified instance + * have to be copied to a freshly loaded active instance on reassociation. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see JdoTemplate + * @see JdoTransactionManager + */ +public interface JdoCallback { + + /** + * Gets called by JdoTemplate.execute with an active JDO + * PersistenceManager. Does not need to care about activating + * or closing the PersistenceManager, or handling transactions. + * + *

Note that JDO callback code will not flush any modifications to the + * database if not executed within a transaction. Thus, you need to make + * sure that JdoTransactionManager has initiated a JDO transaction when + * the callback gets called, at least if you want to write to the database. + * + *

Allows for returning a result object created within the callback, + * i.e. a domain object or a collection of domain objects. + * A thrown custom RuntimeException is treated as an application exception: + * It gets propagated to the caller of the template. + * + * @param pm active PersistenceManager + * @return a result object, or null if none + * @throws JDOException if thrown by the JDO API + * @see JdoTemplate#execute + * @see JdoTemplate#executeFind + */ + Object doInJdo(PersistenceManager pm) throws JDOException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoDialect.java new file mode 100644 index 00000000000..0246694be88 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoDialect.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2007 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.jdo; + +import java.sql.SQLException; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.Query; +import javax.jdo.Transaction; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * SPI strategy that allows for customizing integration with a specific JDO provider, + * in particular regarding transaction management and exception translation. To be + * implemented for specific JDO providers such as JPOX, Kodo, Lido, Versant Open Access. + * + *

JDO 2.0 defines standard ways for most of the functionality covered here. + * Hence, Spring's {@link DefaultJdoDialect} uses the corresponding JDO 2.0 methods + * by default, to be overridden in a vendor-specific fashion if necessary. + * Vendor-specific subclasses of {@link DefaultJdoDialect} are still required for special + * transaction semantics and more sophisticated exception translation (if needed). + * + *

In general, it is recommended to derive from {@link DefaultJdoDialect} instead + * of implementing this interface directly. This allows for inheriting common + * behavior (present and future) from {@link DefaultJdoDialect}, only overriding + * specific hooks to plug in concrete vendor-specific behavior. + * + * @author Juergen Hoeller + * @since 02.11.2003 + * @see JdoTransactionManager#setJdoDialect + * @see JdoAccessor#setJdoDialect + * @see DefaultJdoDialect + */ +public interface JdoDialect { + + //------------------------------------------------------------------------- + // Hooks for transaction management (used by JdoTransactionManager) + //------------------------------------------------------------------------- + + /** + * Begin the given JDO transaction, applying the semantics specified by the + * given Spring transaction definition (in particular, an isolation level + * and a timeout). Invoked by JdoTransactionManager on transaction begin. + *

An implementation can configure the JDO Transaction object and then + * invoke begin, or invoke a special begin method that takes, + * for example, an isolation level. + *

An implementation can also apply read-only flag and isolation level to the + * underlying JDBC Connection before beginning the transaction. In that case, + * a transaction data object can be returned that holds the previous isolation + * level (and possibly other data), to be reset in cleanupTransaction. + *

Implementations can also use the Spring transaction name, as exposed by the + * passed-in TransactionDefinition, to optimize for specific data access use cases + * (effectively using the current transaction name as use case identifier). + * @param transaction the JDO transaction to begin + * @param definition the Spring transaction definition that defines semantics + * @return an arbitrary object that holds transaction data, if any + * (to be passed into cleanupTransaction) + * @throws JDOException if thrown by JDO methods + * @throws SQLException if thrown by JDBC methods + * @throws TransactionException in case of invalid arguments + * @see #cleanupTransaction + * @see javax.jdo.Transaction#begin + * @see org.springframework.jdbc.datasource.DataSourceUtils#prepareConnectionForTransaction + */ + Object beginTransaction(Transaction transaction, TransactionDefinition definition) + throws JDOException, SQLException, TransactionException; + + /** + * Clean up the transaction via the given transaction data. + * Invoked by JdoTransactionManager on transaction cleanup. + *

An implementation can, for example, reset read-only flag and + * isolation level of the underlying JDBC Connection. Furthermore, + * an exposed data access use case can be reset here. + * @param transactionData arbitrary object that holds transaction data, if any + * (as returned by beginTransaction) + * @see #beginTransaction + * @see org.springframework.jdbc.datasource.DataSourceUtils#resetConnectionAfterTransaction + */ + void cleanupTransaction(Object transactionData); + + /** + * Retrieve the JDBC Connection that the given JDO PersistenceManager uses underneath, + * if accessing a relational database. This method will just get invoked if actually + * needing access to the underlying JDBC Connection, usually within an active JDO + * transaction (for example, by JdoTransactionManager). The returned handle will + * be passed into the releaseJdbcConnection method when not needed anymore. + *

Implementations are encouraged to return an unwrapped Connection object, i.e. + * the Connection as they got it from the connection pool. This makes it easier for + * application code to get at the underlying native JDBC Connection, like an + * OracleConnection, which is sometimes necessary for LOB handling etc. We assume + * that calling code knows how to properly handle the returned Connection object. + *

In a simple case where the returned Connection will be auto-closed with the + * PersistenceManager or can be released via the Connection object itself, an + * implementation can return a SimpleConnectionHandle that just contains the + * Connection. If some other object is needed in releaseJdbcConnection, + * an implementation should use a special handle that references that other object. + * @param pm the current JDO PersistenceManager + * @param readOnly whether the Connection is only needed for read-only purposes + * @return a handle for the JDBC Connection, to be passed into + * releaseJdbcConnection, or null + * if no JDBC Connection can be retrieved + * @throws JDOException if thrown by JDO methods + * @throws SQLException if thrown by JDBC methods + * @see #releaseJdbcConnection + * @see org.springframework.jdbc.datasource.ConnectionHandle#getConnection + * @see org.springframework.jdbc.datasource.SimpleConnectionHandle + * @see JdoTransactionManager#setDataSource + * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor + */ + ConnectionHandle getJdbcConnection(PersistenceManager pm, boolean readOnly) + throws JDOException, SQLException; + + /** + * Release the given JDBC Connection, which has originally been retrieved + * via getJdbcConnection. This should be invoked in any case, + * to allow for proper release of the retrieved Connection handle. + *

An implementation might simply do nothing, if the Connection returned + * by getJdbcConnection will be implicitly closed when the JDO + * transaction completes or when the PersistenceManager is closed. + * @param conHandle the JDBC Connection handle to release + * @param pm the current JDO PersistenceManager + * @throws JDOException if thrown by JDO methods + * @throws SQLException if thrown by JDBC methods + * @see #getJdbcConnection + */ + void releaseJdbcConnection(ConnectionHandle conHandle, PersistenceManager pm) + throws JDOException, SQLException; + + /** + * Flush the given PersistenceManager, i.e. flush all changes (that have been + * applied to persistent objects) to the underlying database. This method will + * just get invoked when eager flushing is actually necessary, for example when + * JDBC access code needs to see changes within the same transaction. + * @param pm the current JDO PersistenceManager + * @throws JDOException in case of errors + * @see JdoAccessor#setFlushEager + */ + void flush(PersistenceManager pm) throws JDOException; + + /** + * Apply the given timeout to the given JDO query object. + *

Invoked with the remaining time of a specified transaction timeout, if any. + * @param query the JDO query object to apply the timeout to + * @param timeout the timeout value to apply + * @throws JDOException if thrown by JDO methods + * @see JdoTemplate#prepareQuery + */ + void applyQueryTimeout(Query query, int timeout) throws JDOException; + + + //----------------------------------------------------------------------------------- + // Hook for exception translation (used by JdoTransactionManager and JdoTemplate) + //----------------------------------------------------------------------------------- + + /** + * Translate the given JDOException to a corresponding exception from Spring's + * generic DataAccessException hierarchy. An implementation should apply + * PersistenceManagerFactoryUtils' standard exception translation if can't do + * anything more specific. + *

Of particular importance is the correct translation to + * DataIntegrityViolationException, for example on constraint violation. + * Unfortunately, standard JDO does not allow for portable detection of this. + *

Can use a SQLExceptionTranslator for translating underlying SQLExceptions + * in a database-specific fashion. + * @param ex the JDOException thrown + * @return the corresponding DataAccessException (must not be null) + * @see JdoAccessor#convertJdoAccessException + * @see JdoTransactionManager#convertJdoAccessException + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + * @see org.springframework.dao.DataIntegrityViolationException + * @see org.springframework.jdbc.support.SQLExceptionTranslator + */ + DataAccessException translateException(JDOException ex); + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoInterceptor.java new file mode 100644 index 00000000000..8bc86f5fc43 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoInterceptor.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2006 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.jdo; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * This interceptor binds a new JDO PersistenceManager to the thread before a method + * call, closing and removing it afterwards in case of any method outcome. + * If there already is a pre-bound PersistenceManager (e.g. from JdoTransactionManager, + * or from a surrounding JDO-intercepted method), the interceptor simply participates in it. + * + *

Application code must retrieve a JDO PersistenceManager via the + * PersistenceManagerFactoryUtils.getPersistenceManager method, + * to be able to detect a thread-bound PersistenceManager. It is preferable to use + * getPersistenceManager with allowCreate=false, if the code relies on + * the interceptor to provide proper PersistenceManager handling. Typically, the code + * will look like as follows: + * + *

+ * public void doSomeDataAccessAction() {
+ *   PersistenceManager pm = PersistenceManagerFactoryUtils.getPersistenceManager(this.pmf, false);
+ *   ...
+ * }
+ * + *

Note that this interceptor automatically translates JDOExceptions, via + * delegating to the PersistenceManagerFactoryUtils.convertJdoAccessException + * method that converts them to exceptions that are compatible with the + * org.springframework.dao exception hierarchy (like JdoTemplate does). + * This can be turned off if the raw exceptions are preferred. + * + *

This class can be considered a declarative alternative to JdoTemplate's + * callback approach. The advantages are: + *

+ * + *

The drawback is the dependency on interceptor configuration. However, note + * that this interceptor is usually not necessary in scenarios where the + * data access code always executes within transactions. A transaction will always + * have a thread-bound PersistenceManager in the first place, so adding this interceptor + * to the configuration just adds value when fine-tuning PersistenceManager settings + * like the flush mode - or when relying on exception translation. + * + * @author Juergen Hoeller + * @since 13.06.2003 + * @see PersistenceManagerFactoryUtils#getPersistenceManager + * @see JdoTransactionManager + * @see JdoTemplate + */ +public class JdoInterceptor extends JdoAccessor implements MethodInterceptor { + + private boolean exceptionConversionEnabled = true; + + + /** + * Set whether to convert any JDOException raised to a Spring DataAccessException, + * compatible with the org.springframework.dao exception hierarchy. + *

Default is "true". Turn this flag off to let the caller receive raw exceptions + * as-is, without any wrapping. + * @see org.springframework.dao.DataAccessException + */ + public void setExceptionConversionEnabled(boolean exceptionConversionEnabled) { + this.exceptionConversionEnabled = exceptionConversionEnabled; + } + + + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + boolean existingTransaction = false; + PersistenceManager pm = PersistenceManagerFactoryUtils.getPersistenceManager(getPersistenceManagerFactory(), true); + if (TransactionSynchronizationManager.hasResource(getPersistenceManagerFactory())) { + logger.debug("Found thread-bound PersistenceManager for JDO interceptor"); + existingTransaction = true; + } + else { + logger.debug("Using new PersistenceManager for JDO interceptor"); + TransactionSynchronizationManager.bindResource(getPersistenceManagerFactory(), new PersistenceManagerHolder(pm)); + } + try { + Object retVal = methodInvocation.proceed(); + flushIfNecessary(pm, existingTransaction); + return retVal; + } + catch (JDOException ex) { + if (this.exceptionConversionEnabled) { + throw convertJdoAccessException(ex); + } + else { + throw ex; + } + } + finally { + if (existingTransaction) { + logger.debug("Not closing pre-bound JDO PersistenceManager after interceptor"); + } + else { + TransactionSynchronizationManager.unbindResource(getPersistenceManagerFactory()); + PersistenceManagerFactoryUtils.releasePersistenceManager(pm, getPersistenceManagerFactory()); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoObjectRetrievalFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoObjectRetrievalFailureException.java new file mode 100644 index 00000000000..76b040d7e2c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoObjectRetrievalFailureException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2006 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.jdo; + +import javax.jdo.JDOHelper; +import javax.jdo.JDOObjectNotFoundException; + +import org.springframework.orm.ObjectRetrievalFailureException; + +/** + * JDO-specific subclass of ObjectRetrievalFailureException. + * Converts JDO's JDOObjectNotFoundException. + * + * @author Juergen Hoeller + * @since 1.1 + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ +public class JdoObjectRetrievalFailureException extends ObjectRetrievalFailureException { + + public JdoObjectRetrievalFailureException(JDOObjectNotFoundException ex) { + // Extract information about the failed object from the JDOException, if available. + super((ex.getFailedObject() != null ? ex.getFailedObject().getClass() : null), + (ex.getFailedObject() != null ? JDOHelper.getObjectId(ex.getFailedObject()) : null), + ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoOperations.java new file mode 100644 index 00000000000..7704bda1e88 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoOperations.java @@ -0,0 +1,442 @@ +/* + * Copyright 2002-2007 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.jdo; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that specifies a basic set of JDO operations, + * implemented by {@link JdoTemplate}. Not often used, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Defines JdoTemplate's data access methods that mirror + * various JDO {@link javax.jdo.PersistenceManager} methods. Users are + * strongly encouraged to read the JDO PersistenceManager + * javadocs for details on the semantics of those methods. + * + *

Note that lazy loading will just work with an open JDO + * PersistenceManager, either within a managed transaction or within + * {@link org.springframework.orm.jdo.support.OpenPersistenceManagerInViewFilter}/ + * {@link org.springframework.orm.jdo.support.OpenPersistenceManagerInViewInterceptor}. + * Furthermore, some operations just make sense within transactions, + * for example: evict, evictAll, flush. + * + *

Updated to build on JDO 2.0 or higher, as of Spring 2.5. + * + * @author Juergen Hoeller + * @since 1.1 + * @see JdoTemplate + * @see javax.jdo.PersistenceManager + * @see JdoTransactionManager + * @see JdoDialect + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewFilter + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewInterceptor + */ +public interface JdoOperations { + + /** + * Execute the action specified by the given action object within a + * PersistenceManager. Application exceptions thrown by the action object + * get propagated to the caller (can only be unchecked). JDO exceptions + * are transformed into appropriate DAO ones. Allows for returning a + * result object, i.e. a domain object or a collection of domain objects. + *

Note: Callback code is not supposed to handle transactions itself! + * Use an appropriate transaction manager like JdoTransactionManager. + * @param action callback object that specifies the JDO action + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see JdoTransactionManager + * @see org.springframework.dao + * @see org.springframework.transaction + * @see javax.jdo.PersistenceManager + */ + Object execute(JdoCallback action) throws DataAccessException; + + /** + * Execute the specified action assuming that the result object is a + * Collection. This is a convenience method for executing JDO queries + * within an action. + * @param action callback object that specifies the JDO action + * @return a Collection result returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of JDO errors + */ + Collection executeFind(JdoCallback action) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for load, save, delete + //------------------------------------------------------------------------- + + /** + * Return the persistent instance with the given JDO object id, + * throwing an exception if not found. + *

A JDO object id identifies both the persistent class and the id + * within the namespace of that class. + * @param objectId a JDO object id of the persistent instance + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#getObjectById(Object, boolean) + */ + Object getObjectById(Object objectId) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given id value, throwing an exception if not found. + *

The given id value is typically just unique within the namespace + * of the persistent class. Its toString value must correspond to the + * toString value of the corresponding JDO object id. + *

Usually, the passed-in value will have originated from the primary + * key field of a persistent object that uses JDO's application identity. + * @param entityClass a persistent class + * @param idValue an id value of the persistent instance + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#getObjectById(Object, boolean) + * @see javax.jdo.PersistenceManager#getObjectById(Class, Object) + */ + Object getObjectById(Class entityClass, Object idValue) throws DataAccessException; + + /** + * Remove the given object from the PersistenceManager cache. + * @param entity the persistent instance to evict + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#evict(Object) + */ + void evict(Object entity) throws DataAccessException; + + /** + * Remove all given objects from the PersistenceManager cache. + * @param entities the persistent instances to evict + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#evictAll(java.util.Collection) + */ + void evictAll(Collection entities) throws DataAccessException; + + /** + * Remove all objects from the PersistenceManager cache. + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#evictAll() + */ + void evictAll() throws DataAccessException; + + /** + * Re-read the state of the given persistent instance. + * @param entity the persistent instance to re-read + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#refresh(Object) + */ + void refresh(Object entity) throws DataAccessException; + + /** + * Re-read the state of all given persistent instances. + * @param entities the persistent instances to re-read + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#refreshAll(java.util.Collection) + */ + void refreshAll(Collection entities) throws DataAccessException; + + /** + * Re-read the state of all persistent instances. + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#refreshAll() + */ + void refreshAll() throws DataAccessException; + + /** + * Make the given transient instance persistent. + * Attach the given entity if the instance is detached. + * @param entity the transient instance to make persistent + * @return the persistent instance + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#makePersistent(Object) + */ + Object makePersistent(Object entity) throws DataAccessException; + + /** + * Make the given transient instances persistent. + * Attach the given entities if the instances are detached. + * @param entities the transient instances to make persistent + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#makePersistentAll(java.util.Collection) + */ + Collection makePersistentAll(Collection entities) throws DataAccessException; + + /** + * Delete the given persistent instance. + * @param entity the persistent instance to delete + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#deletePersistent(Object) + */ + void deletePersistent(Object entity) throws DataAccessException; + + /** + * Delete all given persistent instances. + *

This can be combined with any of the find methods to delete by query + * in two lines of code. + * @param entities the persistent instances to delete + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#deletePersistentAll(java.util.Collection) + */ + void deletePersistentAll(Collection entities) throws DataAccessException; + + /** + * Detach a copy of the given persistent instance from the current JDO transaction, + * for use outside a JDO transaction (for example, as web form object). + * @param entity the persistent instance to detach + * @return the corresponding detached instance + * @see javax.jdo.PersistenceManager#detachCopy(Object) + */ + Object detachCopy(Object entity); + + /** + * Detach copies of the given persistent instances from the current JDO transaction, + * for use outside a JDO transaction (for example, as web form objects). + * @param entities the persistent instances to detach + * @return the corresponding detached instances + * @see javax.jdo.PersistenceManager#detachCopyAll(Collection) + */ + Collection detachCopyAll(Collection entities); + + /** + * Reattach the given detached instance (for example, a web form object) with + * the current JDO transaction, merging its changes into the current persistence + * instance that represents the corresponding entity. + *

Note that as of JDO 2.0 final, this operation is equivalent to a + * makePersistent call, with the latter method returning the + * persistence instance. + * @param detachedEntity the detached instance to attach + * @return the corresponding persistent instance + * @deprecated in favor of {@link #makePersistent(Object)}. + * To be removed in Spring 3.0. + */ + Object attachCopy(Object detachedEntity); + + /** + * Reattach the given detached instances (for example, web form objects) with + * the current JDO transaction, merging their changes into the current persistence + * instances that represent the corresponding entities. + *

Note that as of JDO 2.0 final, this operation is equivalent to a + * makePersistentAll call, with the latter method returning the + * persistence instance. + * @param detachedEntities the detached instances to reattach + * @return the corresponding persistent instances + * @deprecated in favor of {@link #makePersistentAll(java.util.Collection)}. + * To be removed in Spring 3.0. + */ + Collection attachCopyAll(Collection detachedEntities); + + /** + * Flush all transactional modifications to the database. + *

Only invoke this for selective eager flushing, for example when JDBC code + * needs to see certain changes within the same transaction. Else, it's preferable + * to rely on auto-flushing at transaction completion. + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#flush() + * @see JdoDialect#flush(javax.jdo.PersistenceManager) + */ + void flush() throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods + //------------------------------------------------------------------------- + + /** + * Find all persistent instances of the given class. + * @param entityClass a persistent class + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class) + */ + Collection find(Class entityClass) throws DataAccessException; + + /** + * Find all persistent instances of the given class that match the given + * JDOQL filter. + * @param entityClass a persistent class + * @param filter the JDOQL filter to match (or null if none) + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class, String) + */ + Collection find(Class entityClass, String filter) throws DataAccessException; + + /** + * Find all persistent instances of the given class that match the given + * JDOQL filter, with the given result ordering. + * @param entityClass a persistent class + * @param filter the JDOQL filter to match (or null if none) + * @param ordering the ordering of the result (or null if none) + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class, String) + * @see javax.jdo.Query#setOrdering + */ + Collection find(Class entityClass, String filter, String ordering) throws DataAccessException; + + /** + * Find all persistent instances of the given class that match the given + * JDOQL filter, using the given parameter declarations and parameter values. + * @param entityClass a persistent class + * @param filter the JDOQL filter to match + * @param parameters the JDOQL parameter declarations + * @param values the corresponding parameter values + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class, String) + * @see javax.jdo.Query#declareParameters + * @see javax.jdo.Query#executeWithArray + */ + Collection find(Class entityClass, String filter, String parameters, Object[] values) + throws DataAccessException; + + /** + * Find all persistent instances of the given class that match the given + * JDOQL filter, using the given parameter declarations and parameter values, + * with the given result ordering. + * @param entityClass a persistent class + * @param filter the JDOQL filter to match + * @param parameters the JDOQL parameter declarations + * @param values the corresponding parameter values + * @param ordering the ordering of the result (or null if none) + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class, String) + * @see javax.jdo.Query#declareParameters + * @see javax.jdo.Query#executeWithArray + * @see javax.jdo.Query#setOrdering + */ + Collection find(Class entityClass, String filter, String parameters, Object[] values, String ordering) + throws DataAccessException; + + /** + * Find all persistent instances of the given class that match the given + * JDOQL filter, using the given parameter declarations and parameter values. + * @param entityClass a persistent class + * @param filter the JDOQL filter to match + * @param parameters the JDOQL parameter declarations + * @param values a Map with parameter names as keys and parameter values + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class, String) + * @see javax.jdo.Query#declareParameters + * @see javax.jdo.Query#executeWithMap + */ + Collection find(Class entityClass, String filter, String parameters, Map values) + throws DataAccessException; + + /** + * Find all persistent instances of the given class that match the given + * JDOQL filter, using the given parameter declarations and parameter values, + * with the given result ordering. + * @param entityClass a persistent class + * @param filter the JDOQL filter to match + * @param parameters the JDOQL parameter declarations + * @param values a Map with parameter names as keys and parameter values + * @param ordering the ordering of the result (or null if none) + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(Class, String) + * @see javax.jdo.Query#declareParameters + * @see javax.jdo.Query#executeWithMap + * @see javax.jdo.Query#setOrdering + */ + Collection find(Class entityClass, String filter, String parameters, Map values, String ordering) + throws DataAccessException; + + /** + * Find persistent instances through the given query object + * in the specified query language. + * @param language the query language (javax.jdo.Query#JDOQL + * or javax.jdo.Query#SQL, for example) + * @param queryObject the query object for the specified language + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(String, Object) + * @see javax.jdo.Query#JDOQL + * @see javax.jdo.Query#SQL + */ + Collection find(String language, Object queryObject) throws DataAccessException; + + /** + * Find persistent instances through the given single-string JDOQL query. + * @param queryString the single-string JDOQL query + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(String) + */ + Collection find(String queryString) throws DataAccessException; + + /** + * Find persistent instances through the given single-string JDOQL query. + * @param queryString the single-string JDOQL query + * @param values the corresponding parameter values + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(String) + */ + Collection find(String queryString, Object[] values) throws DataAccessException; + + /** + * Find persistent instances through the given single-string JDOQL query. + * @param queryString the single-string JDOQL query + * @param values a Map with parameter names as keys and parameter values + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newQuery(String) + */ + Collection find(String queryString, Map values) throws DataAccessException; + + /** + * Find persistent instances through the given named query. + * @param entityClass a persistent class + * @param queryName the name of the query + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newNamedQuery(Class, String) + */ + Collection findByNamedQuery(Class entityClass, String queryName) throws DataAccessException; + + /** + * Find persistent instances through the given named query. + * @param entityClass a persistent class + * @param queryName the name of the query + * @param values the corresponding parameter values + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newNamedQuery(Class, String) + */ + Collection findByNamedQuery(Class entityClass, String queryName, Object[] values) throws DataAccessException; + + /** + * Find persistent instances through the given named query. + * @param entityClass a persistent class + * @param queryName the name of the query + * @param values a Map with parameter names as keys and parameter values + * @return the persistent instances + * @throws org.springframework.dao.DataAccessException in case of JDO errors + * @see javax.jdo.PersistenceManager#newNamedQuery(Class, String) + */ + Collection findByNamedQuery(Class entityClass, String queryName, Map values) throws DataAccessException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoOptimisticLockingFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoOptimisticLockingFailureException.java new file mode 100644 index 00000000000..721ccd22567 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoOptimisticLockingFailureException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2006 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.jdo; + +import javax.jdo.JDOHelper; +import javax.jdo.JDOOptimisticVerificationException; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +/** + * JDO-specific subclass of ObjectOptimisticLockingFailureException. + * Converts JDO's JDOOptimisticVerificationException. + * + * @author Juergen Hoeller + * @since 1.1 + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ +public class JdoOptimisticLockingFailureException extends ObjectOptimisticLockingFailureException { + + public JdoOptimisticLockingFailureException(JDOOptimisticVerificationException ex) { + // Extract information about the failed object from the JDOException, if available. + super((ex.getFailedObject() != null ? ex.getFailedObject().getClass() : null), + (ex.getFailedObject() != null ? JDOHelper.getObjectId(ex.getFailedObject()) : null), + ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoResourceFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoResourceFailureException.java new file mode 100644 index 00000000000..c34591573f1 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoResourceFailureException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2005 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.jdo; + +import javax.jdo.JDODataStoreException; +import javax.jdo.JDOFatalDataStoreException; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * JDO-specific subclass of DataAccessResourceFailureException. + * Converts JDO's JDODataStoreException and JDOFatalDataStoreException. + * + * @author Juergen Hoeller + * @since 1.1 + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ +public class JdoResourceFailureException extends DataAccessResourceFailureException { + + public JdoResourceFailureException(JDODataStoreException ex) { + super(ex.getMessage(), ex); + } + + public JdoResourceFailureException(JDOFatalDataStoreException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoSystemException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoSystemException.java new file mode 100644 index 00000000000..088b80a0b37 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoSystemException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2005 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.jdo; + +import javax.jdo.JDOException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * JDO-specific subclass of UncategorizedDataAccessException, + * for JDO system errors that do not match any concrete + * org.springframework.dao exceptions. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ +public class JdoSystemException extends UncategorizedDataAccessException { + + public JdoSystemException(JDOException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoTemplate.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoTemplate.java new file mode 100644 index 00000000000..7f4190c6f31 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoTemplate.java @@ -0,0 +1,621 @@ +/* + * Copyright 2002-2008 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.jdo; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Map; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; +import javax.jdo.Query; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Helper class that simplifies JDO data access code, and converts + * JDOExceptions into Spring DataAccessExceptions, following the + * org.springframework.dao exception hierarchy. + * + *

The central method is execute, supporting JDO access code + * implementing the {@link JdoCallback} interface. It provides JDO PersistenceManager + * handling such that neither the JdoCallback implementation nor the calling + * code needs to explicitly care about retrieving/closing PersistenceManagers, + * or handling JDO lifecycle exceptions. + * + *

Typically used to implement data access or business logic services that + * use JDO within their implementation but are JDO-agnostic in their interface. + * The latter or code calling the latter only have to deal with business + * objects, query objects, and org.springframework.dao exceptions. + * + *

Can be used within a service implementation via direct instantiation + * with a PersistenceManagerFactory reference, or get prepared in an + * application context and given to services as bean reference. + * Note: The PersistenceManagerFactory 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. + * + *

This class can be considered as direct alternative to working with the + * raw JDO PersistenceManager API (through + * PersistenceManagerFactoryUtils.getPersistenceManager()). + * The major advantage is its automatic conversion to DataAccessExceptions, the + * major disadvantage that no checked application exceptions can get thrown from + * within data access code. Corresponding checks and the actual throwing of such + * exceptions can often be deferred to after callback execution, though. + * + *

{@link LocalPersistenceManagerFactoryBean} is the preferred way of obtaining + * a reference to a specific PersistenceManagerFactory, at least in a non-EJB + * environment. The Spring application context will manage its lifecycle, + * initializing and shutting down the factory as part of the application. + * + *

Note that lazy loading will just work with an open JDO PersistenceManager, + * either within a Spring-driven transaction (with JdoTransactionManager or + * JtaTransactionManager) or within OpenPersistenceManagerInViewFilter/Interceptor. + * Furthermore, some operations just make sense within transactions, + * for example: evict, evictAll, flush. + * + *

NOTE: This class requires JDO 2.0 or higher, as of Spring 2.5. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see #setPersistenceManagerFactory + * @see JdoCallback + * @see javax.jdo.PersistenceManager + * @see LocalPersistenceManagerFactoryBean + * @see JdoTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewFilter + * @see org.springframework.orm.jdo.support.OpenPersistenceManagerInViewInterceptor + */ +public class JdoTemplate extends JdoAccessor implements JdoOperations { + + private boolean allowCreate = true; + + private boolean exposeNativePersistenceManager = false; + + + /** + * Create a new JdoTemplate instance. + */ + public JdoTemplate() { + } + + /** + * Create a new JdoTemplate instance. + * @param pmf PersistenceManagerFactory to create PersistenceManagers + */ + public JdoTemplate(PersistenceManagerFactory pmf) { + setPersistenceManagerFactory(pmf); + afterPropertiesSet(); + } + + /** + * Create a new JdoTemplate instance. + * @param pmf PersistenceManagerFactory to create PersistenceManagers + * @param allowCreate if a non-transactional PersistenceManager should be created + * when no transactional PersistenceManager can be found for the current thread + */ + public JdoTemplate(PersistenceManagerFactory pmf, boolean allowCreate) { + setPersistenceManagerFactory(pmf); + setAllowCreate(allowCreate); + afterPropertiesSet(); + } + + /** + * Set if a new PersistenceManager should be created when no transactional + * PersistenceManager can be found for the current thread. + *

JdoTemplate is aware of a corresponding PersistenceManager bound to the + * current thread, for example when using JdoTransactionManager. + * If allowCreate is true, a new non-transactional PersistenceManager will be + * created if none found, which needs to be closed at the end of the operation. + * If false, an IllegalStateException will get thrown in this case. + * @see PersistenceManagerFactoryUtils#getPersistenceManager(javax.jdo.PersistenceManagerFactory, boolean) + */ + public void setAllowCreate(boolean allowCreate) { + this.allowCreate = allowCreate; + } + + /** + * Return if a new PersistenceManager should be created if no thread-bound found. + */ + public boolean isAllowCreate() { + return this.allowCreate; + } + + /** + * Set whether to expose the native JDO PersistenceManager to JdoCallback + * code. Default is "false": a PersistenceManager proxy will be returned, + * suppressing close calls and automatically applying transaction + * timeouts (if any). + *

As there is often a need to cast to a provider-specific PersistenceManager + * class in DAOs that use provider-specific functionality, the exposed proxy + * implements all interfaces implemented by the original PersistenceManager. + * If this is not sufficient, turn this flag to "true". + * @see JdoCallback + * @see javax.jdo.PersistenceManager + * @see #prepareQuery + */ + public void setExposeNativePersistenceManager(boolean exposeNativePersistenceManager) { + this.exposeNativePersistenceManager = exposeNativePersistenceManager; + } + + /** + * Return whether to expose the native JDO PersistenceManager to JdoCallback + * code, or rather a PersistenceManager proxy. + */ + public boolean isExposeNativePersistenceManager() { + return this.exposeNativePersistenceManager; + } + + + public Object execute(JdoCallback action) throws DataAccessException { + return execute(action, isExposeNativePersistenceManager()); + } + + public Collection executeFind(JdoCallback action) throws DataAccessException { + Object result = execute(action, isExposeNativePersistenceManager()); + if (result != null && !(result instanceof Collection)) { + throw new InvalidDataAccessApiUsageException( + "Result object returned from JdoCallback isn't a Collection: [" + result + "]"); + } + return (Collection) result; + } + + /** + * Execute the action specified by the given action object within a + * PersistenceManager. + * @param action callback object that specifies the JDO action + * @param exposeNativePersistenceManager whether to expose the native + * JDO persistence manager to callback code + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of JDO errors + */ + public Object execute(JdoCallback action, boolean exposeNativePersistenceManager) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + PersistenceManager pm = PersistenceManagerFactoryUtils.getPersistenceManager( + getPersistenceManagerFactory(), isAllowCreate()); + boolean existingTransaction = + TransactionSynchronizationManager.hasResource(getPersistenceManagerFactory()); + try { + PersistenceManager pmToExpose = (exposeNativePersistenceManager ? pm : createPersistenceManagerProxy(pm)); + Object result = action.doInJdo(pmToExpose); + flushIfNecessary(pm, existingTransaction); + return postProcessResult(result, pm, existingTransaction); + } + catch (JDOException ex) { + throw convertJdoAccessException(ex); + } + catch (RuntimeException ex) { + // callback code threw application exception + throw ex; + } + finally { + PersistenceManagerFactoryUtils.releasePersistenceManager(pm, getPersistenceManagerFactory()); + } + } + + /** + * Create a close-suppressing proxy for the given JDO PersistenceManager. + * Called by the execute method. + *

The proxy also prepares returned JDO Query objects. + * @param pm the JDO PersistenceManager to create a proxy for + * @return the PersistenceManager proxy, implementing all interfaces + * implemented by the passed-in PersistenceManager object (that is, + * also implementing all provider-specific extension interfaces) + * @see javax.jdo.PersistenceManager#close() + * @see #execute(JdoCallback, boolean) + * @see #prepareQuery + */ + protected PersistenceManager createPersistenceManagerProxy(PersistenceManager pm) { + Class[] ifcs = ClassUtils.getAllInterfacesForClass(pm.getClass(), getClass().getClassLoader()); + return (PersistenceManager) Proxy.newProxyInstance( + pm.getClass().getClassLoader(), ifcs, new CloseSuppressingInvocationHandler(pm)); + } + + /** + * Post-process the given result object, which might be a Collection. + * Called by the execute method. + *

Default implementation always returns the passed-in Object as-is. + * Subclasses might override this to automatically detach result + * collections or even single result objects. + * @param pm the current JDO PersistenceManager + * @param result the result object (might be a Collection) + * @param existingTransaction if executing within an existing transaction + * (within an existing JDO PersistenceManager that won't be closed immediately) + * @return the post-processed result object (can be simply be the passed-in object) + * @see #execute(JdoCallback, boolean) + */ + protected Object postProcessResult(Object result, PersistenceManager pm, boolean existingTransaction) { + return result; + } + + + //------------------------------------------------------------------------- + // Convenience methods for load, save, delete + //------------------------------------------------------------------------- + + public Object getObjectById(final Object objectId) throws DataAccessException { + return execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + return pm.getObjectById(objectId, true); + } + }, true); + } + + public Object getObjectById(final Class entityClass, final Object idValue) throws DataAccessException { + return execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + return pm.getObjectById(entityClass, idValue); + } + }, true); + } + + public void evict(final Object entity) throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.evict(entity); + return null; + } + }, true); + } + + public void evictAll(final Collection entities) throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.evictAll(entities); + return null; + } + }, true); + } + + public void evictAll() throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.evictAll(); + return null; + } + }, true); + } + + public void refresh(final Object entity) throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.refresh(entity); + return null; + } + }, true); + } + + public void refreshAll(final Collection entities) throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.refreshAll(entities); + return null; + } + }, true); + } + + public void refreshAll() throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.refreshAll(); + return null; + } + }, true); + } + + public Object makePersistent(final Object entity) throws DataAccessException { + return execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + return pm.makePersistent(entity); + } + }, true); + } + + public Collection makePersistentAll(final Collection entities) throws DataAccessException { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + return pm.makePersistentAll(entities); + } + }, true); + } + + public void deletePersistent(final Object entity) throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.deletePersistent(entity); + return null; + } + }, true); + } + + public void deletePersistentAll(final Collection entities) throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + pm.deletePersistentAll(entities); + return null; + } + }, true); + } + + public Object detachCopy(final Object entity) { + return execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + return pm.detachCopy(entity); + } + }, true); + } + + public Collection detachCopyAll(final Collection entities) { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + return pm.detachCopyAll(entities); + } + }, true); + } + + /** + * @deprecated in favor of {@link #makePersistent(Object)}. + * To be removed in Spring 3.0. + */ + public Object attachCopy(Object detachedEntity) { + return makePersistent(detachedEntity); + } + + /** + * @deprecated in favor of {@link #makePersistentAll(java.util.Collection)}. + * To be removed in Spring 3.0. + */ + public Collection attachCopyAll(Collection detachedEntities) { + return makePersistentAll(detachedEntities); + } + + public void flush() throws DataAccessException { + execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + getJdoDialect().flush(pm); + return null; + } + }, true); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods + //------------------------------------------------------------------------- + + public Collection find(Class entityClass) throws DataAccessException { + return find(entityClass, null, null); + } + + public Collection find(Class entityClass, String filter) throws DataAccessException { + return find(entityClass, filter, null); + } + + public Collection find(final Class entityClass, final String filter, final String ordering) + throws DataAccessException { + + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = (filter != null ? pm.newQuery(entityClass, filter) : pm.newQuery(entityClass)); + prepareQuery(query); + if (ordering != null) { + query.setOrdering(ordering); + } + return query.execute(); + } + }, true); + } + + public Collection find(Class entityClass, String filter, String parameters, Object[] values) + throws DataAccessException { + + return find(entityClass, filter, parameters, values, null); + } + + public Collection find( + final Class entityClass, final String filter, final String parameters, final Object[] values, + final String ordering) throws DataAccessException { + + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newQuery(entityClass, filter); + prepareQuery(query); + query.declareParameters(parameters); + if (ordering != null) { + query.setOrdering(ordering); + } + return query.executeWithArray(values); + } + }, true); + } + + public Collection find(Class entityClass, String filter, String parameters, Map values) + throws DataAccessException { + + return find(entityClass, filter, parameters, values, null); + } + + public Collection find( + final Class entityClass, final String filter, final String parameters, final Map values, + final String ordering) throws DataAccessException { + + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newQuery(entityClass, filter); + prepareQuery(query); + query.declareParameters(parameters); + if (ordering != null) { + query.setOrdering(ordering); + } + return query.executeWithMap(values); + } + }, true); + } + + public Collection find(final String language, final Object queryObject) throws DataAccessException { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newQuery(language, queryObject); + prepareQuery(query); + return query.execute(); + } + }, true); + } + + public Collection find(final String queryString) throws DataAccessException { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newQuery(queryString); + prepareQuery(query); + return query.execute(); + } + }, true); + } + + public Collection find(final String queryString, final Object[] values) throws DataAccessException { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newQuery(queryString); + prepareQuery(query); + return query.executeWithArray(values); + } + }, true); + } + + public Collection find(final String queryString, final Map values) throws DataAccessException { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newQuery(queryString); + prepareQuery(query); + return query.executeWithMap(values); + } + }, true); + } + + public Collection findByNamedQuery(final Class entityClass, final String queryName) throws DataAccessException { + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newNamedQuery(entityClass, queryName); + prepareQuery(query); + return query.execute(); + } + }, true); + } + + public Collection findByNamedQuery(final Class entityClass, final String queryName, final Object[] values) + throws DataAccessException { + + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newNamedQuery(entityClass, queryName); + prepareQuery(query); + return query.executeWithArray(values); + } + }, true); + } + + public Collection findByNamedQuery(final Class entityClass, final String queryName, final Map values) + throws DataAccessException { + + return (Collection) execute(new JdoCallback() { + public Object doInJdo(PersistenceManager pm) throws JDOException { + Query query = pm.newNamedQuery(entityClass, queryName); + prepareQuery(query); + return query.executeWithMap(values); + } + }, true); + } + + + /** + * Prepare the given JDO query object. To be used within a JdoCallback. + * Applies a transaction timeout, if any. If you don't use such timeouts, + * the call is a no-op. + *

In general, prefer a proxied PersistenceManager instead, which will + * automatically apply the transaction timeout (through the use of a special + * PersistenceManager proxy). You need to set the "exposeNativePersistenceManager" + * property to "false" to activate this. Note that you won't be able to cast + * to a provider-specific JDO PersistenceManager class anymore then. + * @param query the JDO query object + * @throws JDOException if the query could not be properly prepared + * @see JdoCallback#doInJdo + * @see PersistenceManagerFactoryUtils#applyTransactionTimeout + * @see #setExposeNativePersistenceManager + */ + public void prepareQuery(Query query) throws JDOException { + PersistenceManagerFactoryUtils.applyTransactionTimeout( + query, getPersistenceManagerFactory(), getJdoDialect()); + } + + + /** + * Invocation handler that suppresses close calls on JDO PersistenceManagers. + * Also prepares returned Query objects. + * @see javax.jdo.PersistenceManager#close() + */ + private class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final PersistenceManager target; + + public CloseSuppressingInvocationHandler(PersistenceManager target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on PersistenceManager interface (or provider-specific extension) coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of PersistenceManager proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return null; + } + + // Invoke method on target PersistenceManager. + try { + Object retVal = method.invoke(this.target, args); + + // If return value is a JDO Query object, apply transaction timeout. + if (retVal instanceof Query) { + prepareQuery(((Query) retVal)); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoTransactionManager.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoTransactionManager.java new file mode 100644 index 00000000000..b6402b2e9c8 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoTransactionManager.java @@ -0,0 +1,587 @@ +/* + * Copyright 2002-2008 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.jdo; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; +import javax.jdo.Transaction; +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} implementation + * for a single JDO {@link javax.jdo.PersistenceManagerFactory}. Binds a JDO + * PersistenceManager from the specified factory to the thread, potentially allowing + * for one thread-bound PersistenceManager per factory. + * {@link PersistenceManagerFactoryUtils} and {@link JdoTemplate} are aware of + * thread-bound persistence managers and participate in such transactions automatically. + * Using either of those (or going through a {@link TransactionAwarePersistenceManagerFactoryProxy} + * is required for JDO access code supporting this transaction management mechanism. + * + *

This transaction manager is appropriate for applications that use a single + * JDO PersistenceManagerFactory for transactional data access. JTA (usually through + * {@link org.springframework.transaction.jta.JtaTransactionManager}) is necessary + * for accessing multiple transactional resources within the same transaction. + * Note that you need to configure your JDO provider accordingly in order to make + * it participate in JTA transactions. + * + *

This transaction manager also supports direct DataSource access within a + * transaction (i.e. plain JDBC code working with the same DataSource). + * This allows for mixing services which access JDO and services which use plain + * JDBC (without being aware of JDO)! Application code needs to stick to the + * same simple Connection lookup pattern as with + * {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} + * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection} + * or going through a + * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}). + * + *

Note: To be able to register a DataSource's Connection for plain JDBC code, + * this instance needs to be aware of the DataSource ({@link #setDataSource}). + * The given DataSource should obviously match the one used by the given + * PersistenceManagerFactory. This transaction manager will autodetect the DataSource + * that acts as "connectionFactory" of the PersistenceManagerFactory, so you usually + * don't need to explicitly specify the "dataSource" property. + * + *

On JDBC 3.0, this transaction manager supports nested transactions via JDBC 3.0 + * Savepoints. The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} + * flag defaults to "false", though, as nested transactions will just apply to the + * JDBC Connection, not to the JDO PersistenceManager and its cached objects. + * You can manually set the flag to "true" if you want to use nested transactions + * for JDBC access code which participates in JDO transactions (provided that your + * JDBC driver supports Savepoints). Note that JDO itself does not support + * nested transactions! Hence, do not expect JDO access code to semantically + * participate in a nested transaction. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see #setPersistenceManagerFactory + * @see #setDataSource + * @see javax.jdo.PersistenceManagerFactory#getConnectionFactory + * @see LocalPersistenceManagerFactoryBean + * @see PersistenceManagerFactoryUtils#getPersistenceManager + * @see PersistenceManagerFactoryUtils#releasePersistenceManager + * @see JdoTemplate + * @see TransactionAwarePersistenceManagerFactoryProxy + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class JdoTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private PersistenceManagerFactory persistenceManagerFactory; + + private DataSource dataSource; + + private boolean autodetectDataSource = true; + + private JdoDialect jdoDialect; + + + /** + * Create a new JdoTransactionManager instance. + * A PersistenceManagerFactory has to be set to be able to use it. + * @see #setPersistenceManagerFactory + */ + public JdoTransactionManager() { + } + + /** + * Create a new JdoTransactionManager instance. + * @param pmf PersistenceManagerFactory to manage transactions for + */ + public JdoTransactionManager(PersistenceManagerFactory pmf) { + this.persistenceManagerFactory = pmf; + afterPropertiesSet(); + } + + + /** + * Set the PersistenceManagerFactory that this instance should manage transactions for. + *

The PersistenceManagerFactory specified here should be the target + * PersistenceManagerFactory to manage transactions for, not a + * TransactionAwarePersistenceManagerFactoryProxy. Only data access + * code may work with TransactionAwarePersistenceManagerFactoryProxy, while the + * transaction manager needs to work on the underlying target PersistenceManagerFactory. + * @see TransactionAwarePersistenceManagerFactoryProxy + */ + public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) { + this.persistenceManagerFactory = pmf; + } + + /** + * Return the PersistenceManagerFactory that this instance should manage transactions for. + */ + public PersistenceManagerFactory getPersistenceManagerFactory() { + return this.persistenceManagerFactory; + } + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + * The DataSource should match the one used by the JDO PersistenceManagerFactory: + * for example, you could specify the same JNDI DataSource for both. + *

If the PersistenceManagerFactory uses a DataSource as connection factory, + * the DataSource will be autodetected: You can still explictly specify the + * DataSource, but you don't need to in this case. + *

A transactional JDBC Connection for this DataSource will be provided to + * application code accessing this DataSource directly via DataSourceUtils + * or JdbcTemplate. The Connection will be taken from the JDO PersistenceManager. + *

Note that you need to use a JDO dialect for a specific JDO provider to + * allow for exposing JDO transactions as JDBC transactions. + *

The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + * @see #setAutodetectDataSource + * @see #setJdoDialect + * @see javax.jdo.PersistenceManagerFactory#getConnectionFactory + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.DataSourceUtils + * @see org.springframework.jdbc.core.JdbcTemplate + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set whether to autodetect a JDBC DataSource used by the JDO PersistenceManagerFactory, + * as returned by the getConnectionFactory() method. Default is "true". + *

Can be turned off to deliberately ignore an available DataSource, + * to not expose JDO transactions as JDBC transactions for that DataSource. + * @see #setDataSource + * @see javax.jdo.PersistenceManagerFactory#getConnectionFactory + */ + public void setAutodetectDataSource(boolean autodetectDataSource) { + this.autodetectDataSource = autodetectDataSource; + } + + /** + * Set the JDO dialect to use for this transaction manager. + *

The dialect object can be used to retrieve the underlying JDBC connection + * and thus allows for exposing JDO transactions as JDBC transactions. + * @see JdoDialect#getJdbcConnection + */ + public void setJdoDialect(JdoDialect jdoDialect) { + this.jdoDialect = jdoDialect; + } + + /** + * Return the JDO dialect to use for this transaction manager. + *

Creates a default one for the specified PersistenceManagerFactory if none set. + */ + public JdoDialect getJdoDialect() { + if (this.jdoDialect == null) { + this.jdoDialect = new DefaultJdoDialect(); + } + return this.jdoDialect; + } + + /** + * Eagerly initialize the JDO dialect, creating a default one + * for the specified PersistenceManagerFactory if none set. + * Auto-detect the PersistenceManagerFactory's DataSource, if any. + */ + public void afterPropertiesSet() { + if (getPersistenceManagerFactory() == null) { + throw new IllegalArgumentException("Property 'persistenceManagerFactory' is required"); + } + // Build default JdoDialect if none explicitly specified. + if (this.jdoDialect == null) { + this.jdoDialect = new DefaultJdoDialect(getPersistenceManagerFactory().getConnectionFactory()); + } + + // Check for DataSource as connection factory. + if (this.autodetectDataSource && getDataSource() == null) { + Object pmfcf = getPersistenceManagerFactory().getConnectionFactory(); + if (pmfcf instanceof DataSource) { + // Use the PersistenceManagerFactory's DataSource for exposing transactions to JDBC code. + this.dataSource = (DataSource) pmfcf; + if (logger.isInfoEnabled()) { + logger.info("Using DataSource [" + this.dataSource + + "] of JDO PersistenceManagerFactory for JdoTransactionManager"); + } + } + } + } + + + public Object getResourceFactory() { + return getPersistenceManagerFactory(); + } + + protected Object doGetTransaction() { + JdoTransactionObject txObject = new JdoTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + + PersistenceManagerHolder pmHolder = (PersistenceManagerHolder) + TransactionSynchronizationManager.getResource(getPersistenceManagerFactory()); + if (pmHolder != null) { + if (logger.isDebugEnabled()) { + logger.debug("Found thread-bound PersistenceManager [" + + pmHolder.getPersistenceManager() + "] for JDO transaction"); + } + txObject.setPersistenceManagerHolder(pmHolder, false); + } + + if (getDataSource() != null) { + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.getResource(getDataSource()); + txObject.setConnectionHolder(conHolder); + } + + return txObject; + } + + protected boolean isExistingTransaction(Object transaction) { + return ((JdoTransactionObject) transaction).hasTransaction(); + } + + protected void doBegin(Object transaction, TransactionDefinition definition) { + JdoTransactionObject txObject = (JdoTransactionObject) transaction; + + if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + throw new IllegalTransactionStateException( + "Pre-bound JDBC Connection found! JdoTransactionManager does not support " + + "running within DataSourceTransactionManager if told to manage the DataSource itself. " + + "It is recommended to use a single JdoTransactionManager for all transactions " + + "on a single DataSource, no matter whether JDO or JDBC access."); + } + + PersistenceManager pm = null; + + try { + if (txObject.getPersistenceManagerHolder() == null || + txObject.getPersistenceManagerHolder().isSynchronizedWithTransaction()) { + PersistenceManager newPm = getPersistenceManagerFactory().getPersistenceManager(); + if (logger.isDebugEnabled()) { + logger.debug("Opened new PersistenceManager [" + newPm + "] for JDO transaction"); + } + txObject.setPersistenceManagerHolder(new PersistenceManagerHolder(newPm), true); + } + + pm = txObject.getPersistenceManagerHolder().getPersistenceManager(); + + // Delegate to JdoDialect for actual transaction begin. + Object transactionData = getJdoDialect().beginTransaction(pm.currentTransaction(), definition); + txObject.setTransactionData(transactionData); + + // Register transaction timeout. + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getPersistenceManagerHolder().setTimeoutInSeconds(timeout); + } + + // Register the JDO PersistenceManager's JDBC Connection for the DataSource, if set. + if (getDataSource() != null) { + ConnectionHandle conHandle = getJdoDialect().getJdbcConnection(pm, definition.isReadOnly()); + if (conHandle != null) { + ConnectionHolder conHolder = new ConnectionHolder(conHandle); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + conHolder.setTimeoutInSeconds(timeout); + } + if (logger.isDebugEnabled()) { + logger.debug("Exposing JDO transaction as JDBC transaction [" + conHolder.getConnectionHandle() + "]"); + } + TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); + txObject.setConnectionHolder(conHolder); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Not exposing JDO transaction [" + pm + "] as JDBC transaction because JdoDialect [" + + getJdoDialect() + "] does not support JDBC Connection retrieval"); + } + } + } + + // Bind the persistence manager holder to the thread. + if (txObject.isNewPersistenceManagerHolder()) { + TransactionSynchronizationManager.bindResource( + getPersistenceManagerFactory(), txObject.getPersistenceManagerHolder()); + } + txObject.getPersistenceManagerHolder().setSynchronizedWithTransaction(true); + } + + catch (TransactionException ex) { + closePersistenceManagerAfterFailedBegin(txObject); + throw ex; + } + catch (Exception ex) { + closePersistenceManagerAfterFailedBegin(txObject); + throw new CannotCreateTransactionException("Could not open JDO PersistenceManager for transaction", ex); + } + } + + /** + * Close the current transaction's EntityManager. + * Called after a transaction begin attempt failed. + * @param txObject the current transaction + */ + protected void closePersistenceManagerAfterFailedBegin(JdoTransactionObject txObject) { + if (txObject.isNewPersistenceManagerHolder()) { + PersistenceManager pm = txObject.getPersistenceManagerHolder().getPersistenceManager(); + try { + if (pm.currentTransaction().isActive()) { + pm.currentTransaction().rollback(); + } + } + catch (Throwable ex) { + logger.debug("Could not rollback PersistenceManager after failed transaction begin", ex); + } + finally { + PersistenceManagerFactoryUtils.releasePersistenceManager(pm, getPersistenceManagerFactory()); + } + } + } + + protected Object doSuspend(Object transaction) { + JdoTransactionObject txObject = (JdoTransactionObject) transaction; + txObject.setPersistenceManagerHolder(null, false); + PersistenceManagerHolder persistenceManagerHolder = (PersistenceManagerHolder) + TransactionSynchronizationManager.unbindResource(getPersistenceManagerFactory()); + txObject.setConnectionHolder(null); + ConnectionHolder connectionHolder = null; + if (getDataSource() != null && TransactionSynchronizationManager.hasResource(getDataSource())) { + connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(getDataSource()); + } + return new SuspendedResourcesHolder(persistenceManagerHolder, connectionHolder); + } + + protected void doResume(Object transaction, Object suspendedResources) { + SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources; + TransactionSynchronizationManager.bindResource( + getPersistenceManagerFactory(), resourcesHolder.getPersistenceManagerHolder()); + if (getDataSource() != null && resourcesHolder.getConnectionHolder() != null) { + TransactionSynchronizationManager.bindResource(getDataSource(), resourcesHolder.getConnectionHolder()); + } + } + + /** + * This implementation returns "true": a JDO2 commit will properly handle + * transactions that have been marked rollback-only at a global level. + */ + protected boolean shouldCommitOnGlobalRollbackOnly() { + return true; + } + + protected void doCommit(DefaultTransactionStatus status) { + JdoTransactionObject txObject = (JdoTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Committing JDO transaction on PersistenceManager [" + + txObject.getPersistenceManagerHolder().getPersistenceManager() + "]"); + } + try { + Transaction tx = txObject.getPersistenceManagerHolder().getPersistenceManager().currentTransaction(); + tx.commit(); + } + catch (JDOException ex) { + // Assumably failed to flush changes to database. + throw convertJdoAccessException(ex); + } + } + + protected void doRollback(DefaultTransactionStatus status) { + JdoTransactionObject txObject = (JdoTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Rolling back JDO transaction on PersistenceManager [" + + txObject.getPersistenceManagerHolder().getPersistenceManager() + "]"); + } + try { + Transaction tx = txObject.getPersistenceManagerHolder().getPersistenceManager().currentTransaction(); + if (tx.isActive()) { + tx.rollback(); + } + } + catch (JDOException ex) { + throw new TransactionSystemException("Could not roll back JDO transaction", ex); + } + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + JdoTransactionObject txObject = (JdoTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDO transaction on PersistenceManager [" + + txObject.getPersistenceManagerHolder().getPersistenceManager() + "] rollback-only"); + } + txObject.setRollbackOnly(); + } + + protected void doCleanupAfterCompletion(Object transaction) { + JdoTransactionObject txObject = (JdoTransactionObject) transaction; + + // Remove the persistence manager holder from the thread. + if (txObject.isNewPersistenceManagerHolder()) { + TransactionSynchronizationManager.unbindResource(getPersistenceManagerFactory()); + } + txObject.getPersistenceManagerHolder().clear(); + + // Remove the JDBC connection holder from the thread, if exposed. + if (txObject.hasConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(getDataSource()); + try { + getJdoDialect().releaseJdbcConnection(txObject.getConnectionHolder().getConnectionHandle(), + txObject.getPersistenceManagerHolder().getPersistenceManager()); + } + catch (Throwable ex) { + // Just log it, to keep a transaction-related exception. + logger.debug("Could not release JDBC connection after transaction", ex); + } + } + + getJdoDialect().cleanupTransaction(txObject.getTransactionData()); + + if (txObject.isNewPersistenceManagerHolder()) { + PersistenceManager pm = txObject.getPersistenceManagerHolder().getPersistenceManager(); + if (logger.isDebugEnabled()) { + logger.debug("Closing JDO PersistenceManager [" + pm + "] after transaction"); + } + PersistenceManagerFactoryUtils.releasePersistenceManager(pm, getPersistenceManagerFactory()); + } + else { + logger.debug("Not closing pre-bound JDO PersistenceManager after transaction"); + } + } + + /** + * Convert the given JDOException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Default implementation delegates to the JdoDialect. + * May be overridden in subclasses. + * @param ex JDOException that occured + * @return the corresponding DataAccessException instance + * @see JdoDialect#translateException + */ + protected DataAccessException convertJdoAccessException(JDOException ex) { + return getJdoDialect().translateException(ex); + } + + + /** + * JDO transaction object, representing a PersistenceManagerHolder. + * Used as transaction object by JdoTransactionManager. + */ + private static class JdoTransactionObject extends JdbcTransactionObjectSupport { + + private PersistenceManagerHolder persistenceManagerHolder; + + private boolean newPersistenceManagerHolder; + + private Object transactionData; + + public void setPersistenceManagerHolder( + PersistenceManagerHolder persistenceManagerHolder, boolean newPersistenceManagerHolder) { + this.persistenceManagerHolder = persistenceManagerHolder; + this.newPersistenceManagerHolder = newPersistenceManagerHolder; + } + + public PersistenceManagerHolder getPersistenceManagerHolder() { + return persistenceManagerHolder; + } + + public boolean isNewPersistenceManagerHolder() { + return newPersistenceManagerHolder; + } + + public boolean hasTransaction() { + return (this.persistenceManagerHolder != null && this.persistenceManagerHolder.isTransactionActive()); + } + + public void setTransactionData(Object transactionData) { + this.transactionData = transactionData; + this.persistenceManagerHolder.setTransactionActive(true); + } + + public Object getTransactionData() { + return transactionData; + } + + public void setRollbackOnly() { + Transaction tx = this.persistenceManagerHolder.getPersistenceManager().currentTransaction(); + if (tx.isActive()) { + tx.setRollbackOnly(); + } + if (hasConnectionHolder()) { + getConnectionHolder().setRollbackOnly(); + } + } + + public boolean isRollbackOnly() { + Transaction tx = this.persistenceManagerHolder.getPersistenceManager().currentTransaction(); + return tx.getRollbackOnly(); + } + } + + + /** + * Holder for suspended resources. + * Used internally by doSuspend and doResume. + */ + private static class SuspendedResourcesHolder { + + private final PersistenceManagerHolder persistenceManagerHolder; + + private final ConnectionHolder connectionHolder; + + private SuspendedResourcesHolder(PersistenceManagerHolder pmHolder, ConnectionHolder conHolder) { + this.persistenceManagerHolder = pmHolder; + this.connectionHolder = conHolder; + } + + private PersistenceManagerHolder getPersistenceManagerHolder() { + return this.persistenceManagerHolder; + } + + private ConnectionHolder getConnectionHolder() { + return this.connectionHolder; + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoUsageException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoUsageException.java new file mode 100644 index 00000000000..b4c44419106 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/JdoUsageException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2005 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.jdo; + +import javax.jdo.JDOFatalUserException; +import javax.jdo.JDOUserException; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * JDO-specific subclass of InvalidDataAccessApiUsageException. + * Converts JDO's JDOUserException and JDOFatalUserException. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ +public class JdoUsageException extends InvalidDataAccessApiUsageException { + + public JdoUsageException(JDOUserException ex) { + super(ex.getMessage(), ex); + } + + public JdoUsageException(JDOFatalUserException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/LocalPersistenceManagerFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/LocalPersistenceManagerFactoryBean.java new file mode 100644 index 00000000000..172b3c83808 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/LocalPersistenceManagerFactoryBean.java @@ -0,0 +1,322 @@ +/* + * Copyright 2002-2007 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.jdo; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.jdo.JDOException; +import javax.jdo.JDOHelper; +import javax.jdo.PersistenceManagerFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.util.CollectionUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a + * JDO {@link javax.jdo.PersistenceManagerFactory}. This is the usual way to + * set up a shared JDO PersistenceManagerFactory in a Spring application context; + * the PersistenceManagerFactory can then be passed to JDO-based DAOs via + * dependency injection. Note that switching to a JNDI lookup or to a bean-style + * PersistenceManagerFactory instance is just a matter of configuration! + * + *

Configuration settings can either be read from a properties file, + * specified as "configLocation", or locally specified. Properties + * specified as "jdoProperties" here will override any settings in a file. + * On JDO 2.1, you may alternatively specify a "persistenceManagerFactoryName", + * referring to a PMF definition in "META-INF/jdoconfig.xml" + * (see {@link #setPersistenceManagerFactoryName}). + * + *

NOTE: This class requires JDO 2.0 or higher, as of Spring 2.5. + * + *

This class also implements the + * {@link org.springframework.dao.support.PersistenceExceptionTranslator} + * interface, as autodetected by Spring's + * {@link org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor}, + * for AOP-based translation of native exceptions to Spring DataAccessExceptions. + * Hence, the presence of a LocalPersistenceManagerFactoryBean automatically enables + * a PersistenceExceptionTranslationPostProcessor to translate JDO exceptions. + * + *

Alternative: Configuration of a PersistenceManagerFactory provider bean + * + *

As alternative to the properties-driven approach that this FactoryBean offers + * (which is analogous to using the standard JDOHelper class with a Properties + * object that is populated with standard JDO properties), you can set up an + * instance of your PersistenceManagerFactory implementation class directly. + * + *

Like a DataSource, a PersistenceManagerFactory is encouraged to + * support bean-style configuration, which makes it very easy to set up as + * Spring-managed bean. The implementation class becomes the bean class; + * the remaining properties are applied as bean properties (starting with + * lower-case characters, in contrast to the corresponding JDO properties). + * + *

For example, in case of JPOX: + * + *

+ * <bean id="persistenceManagerFactory" class="org.jpox.PersistenceManagerFactoryImpl" destroy-method="close">
+ *   <property name="connectionFactory" ref="dataSource"/>
+ *   <property name="nontransactionalRead" value="true"/>
+ * </bean>
+ * 
+ * + *

Note that such direct setup of a PersistenceManagerFactory implementation + * is the only way to pass an external connection factory (i.e. a JDBC DataSource) + * into a JDO PersistenceManagerFactory. With the standard properties-driven approach, + * you can only use an internal connection pool or a JNDI DataSource. + * + *

The close() method is standardized in JDO; don't forget to + * specify it as "destroy-method" for any PersistenceManagerFactory instance. + * Note that this FactoryBean will automatically invoke close() for + * the PersistenceManagerFactory that it creates, without any special configuration. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see JdoTemplate#setPersistenceManagerFactory + * @see JdoTransactionManager#setPersistenceManagerFactory + * @see org.springframework.jndi.JndiObjectFactoryBean + * @see javax.jdo.JDOHelper#getPersistenceManagerFactory + * @see javax.jdo.PersistenceManagerFactory#setConnectionFactory + * @see javax.jdo.PersistenceManagerFactory#close() + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + */ +public class LocalPersistenceManagerFactoryBean + implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean, PersistenceExceptionTranslator { + + protected final Log logger = LogFactory.getLog(getClass()); + + private String persistenceManagerFactoryName; + + private Resource configLocation; + + private final Map jdoPropertyMap = new HashMap(); + + private ClassLoader beanClassLoader; + + private PersistenceManagerFactory persistenceManagerFactory; + + private JdoDialect jdoDialect; + + + /** + * Specify the name of the desired PersistenceManagerFactory. + *

This may either be a properties resource in the classpath if such a resource exists + * (JDO 2.0), or a PMF definition with that name from "META-INF/jdoconfig.xml" (JDO 2.1), + * or a JPA EntityManagerFactory cast to a PersistenceManagerFactory based on the + * persistence-unit name from "META-INF/persistence.xml" (JDO 2.1 / JPA 1.0). + *

Default is none: Either 'persistenceManagerFactoryName' or 'configLocation' + * or 'jdoProperties' needs to be specified. + * @see #setConfigLocation + * @see #setJdoProperties + */ + public void setPersistenceManagerFactoryName(String persistenceManagerFactoryName) { + this.persistenceManagerFactoryName = persistenceManagerFactoryName; + } + + /** + * Set the location of the JDO properties config file, for example + * as classpath resource "classpath:kodo.properties". + *

Note: Can be omitted when all necessary properties are + * specified locally via this bean. + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + /** + * Set JDO properties, such as"javax.jdo.PersistenceManagerFactoryClass". + *

Can be used to override values in a JDO properties config file, + * or to specify all necessary properties locally. + *

Can be populated with a String "value" (parsed via PropertiesEditor) + * or a "props" element in XML bean definitions. + */ + public void setJdoProperties(Properties jdoProperties) { + CollectionUtils.mergePropertiesIntoMap(jdoProperties, this.jdoPropertyMap); + } + + /** + * Specify JDO properties as a Map, to be passed into + * JDOHelper.getPersistenceManagerFactory (if any). + *

Can be populated with a "map" or "props" element in XML bean definitions. + * @see javax.jdo.JDOHelper#getPersistenceManagerFactory(java.util.Map) + */ + public void setJdoPropertyMap(Map jdoProperties) { + if (jdoProperties != null) { + this.jdoPropertyMap.putAll(jdoProperties); + } + } + + /** + * Allow Map access to the JDO properties to be passed to the JDOHelper, + * with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via + * "jdoPropertyMap[myKey]". + */ + public Map getJdoPropertyMap() { + return this.jdoPropertyMap; + } + /** + * Set the JDO dialect to use for the PersistenceExceptionTranslator + * functionality of this factory. + *

Default is a DefaultJdoDialect based on the PersistenceManagerFactory's + * underlying DataSource, if any. + * @see JdoDialect#translateException + * @see #translateExceptionIfPossible + * @see org.springframework.dao.support.PersistenceExceptionTranslator + */ + public void setJdoDialect(JdoDialect jdoDialect) { + this.jdoDialect = jdoDialect; + } + + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + /** + * Initialize the PersistenceManagerFactory for the given location. + * @throws IllegalArgumentException in case of illegal property values + * @throws IOException if the properties could not be loaded from the given location + * @throws JDOException in case of JDO initialization errors + */ + public void afterPropertiesSet() throws IllegalArgumentException, IOException, JDOException { + if (this.persistenceManagerFactoryName != null) { + if (this.configLocation != null || !this.jdoPropertyMap.isEmpty()) { + throw new IllegalStateException("'configLocation'/'jdoProperties' not supported in " + + "combination with 'persistenceManagerFactoryName' - specify one or the other, not both"); + } + if (logger.isInfoEnabled()) { + logger.info("Building new JDO PersistenceManagerFactory for name '" + + this.persistenceManagerFactoryName + "'"); + } + this.persistenceManagerFactory = newPersistenceManagerFactory(this.persistenceManagerFactoryName); + } + + else { + Map mergedProps = new HashMap(); + + if (this.configLocation != null) { + if (logger.isInfoEnabled()) { + logger.info("Loading JDO config from [" + this.configLocation + "]"); + } + mergedProps.putAll(PropertiesLoaderUtils.loadProperties(this.configLocation)); + } + + mergedProps.putAll(this.jdoPropertyMap); + + // Build PersistenceManagerFactory instance. + logger.info("Building new JDO PersistenceManagerFactory"); + this.persistenceManagerFactory = newPersistenceManagerFactory(mergedProps); + } + + // Build default JdoDialect if none explicitly specified. + if (this.jdoDialect == null) { + this.jdoDialect = new DefaultJdoDialect(this.persistenceManagerFactory.getConnectionFactory()); + } + } + + /** + * Subclasses can override this to perform custom initialization of the + * PersistenceManagerFactory instance, creating it for the specified name. + *

The default implementation invokes JDOHelper's + * getPersistenceManagerFactory(String) method. + * A custom implementation could prepare the instance in a specific way, + * or use a custom PersistenceManagerFactory implementation. + * @param name the name of the desired PersistenceManagerFactory + * @return the PersistenceManagerFactory instance + * @see javax.jdo.JDOHelper#getPersistenceManagerFactory(String) + */ + protected PersistenceManagerFactory newPersistenceManagerFactory(String name) { + return JDOHelper.getPersistenceManagerFactory(name, this.beanClassLoader); + } + + /** + * Subclasses can override this to perform custom initialization of the + * PersistenceManagerFactory instance, creating it via the given Properties + * that got prepared by this LocalPersistenceManagerFactoryBean. + *

The default implementation invokes JDOHelper's + * getPersistenceManagerFactory(Map) method. + * A custom implementation could prepare the instance in a specific way, + * or use a custom PersistenceManagerFactory implementation. + * @param props the merged properties prepared by this LocalPersistenceManagerFactoryBean + * @return the PersistenceManagerFactory instance + * @see javax.jdo.JDOHelper#getPersistenceManagerFactory(java.util.Map) + */ + protected PersistenceManagerFactory newPersistenceManagerFactory(Map props) { + return JDOHelper.getPersistenceManagerFactory(props, this.beanClassLoader); + } + + + /** + * Return the singleton PersistenceManagerFactory. + */ + public Object getObject() { + return this.persistenceManagerFactory; + } + + public Class getObjectType() { + return (this.persistenceManagerFactory != null ? + this.persistenceManagerFactory.getClass() : PersistenceManagerFactory.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Implementation of the PersistenceExceptionTranslator interface, + * as autodetected by Spring's PersistenceExceptionTranslationPostProcessor. + *

Converts the exception if it is a JDOException, preferably using a specified + * JdoDialect. Else returns null to indicate an unknown exception. + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * @see JdoDialect#translateException + * @see PersistenceManagerFactoryUtils#convertJdoAccessException + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof JDOException) { + if (this.jdoDialect != null) { + return this.jdoDialect.translateException((JDOException) ex); + } + else { + return PersistenceManagerFactoryUtils.convertJdoAccessException((JDOException) ex); + } + } + return null; + } + + + /** + * Close the PersistenceManagerFactory on bean factory shutdown. + */ + public void destroy() { + logger.info("Closing JDO PersistenceManagerFactory"); + this.persistenceManagerFactory.close(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/PersistenceManagerFactoryUtils.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/PersistenceManagerFactoryUtils.java new file mode 100644 index 00000000000..8d3a7ca4fe0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/PersistenceManagerFactoryUtils.java @@ -0,0 +1,321 @@ +/* + * Copyright 2002-2008 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.jdo; + +import javax.jdo.JDODataStoreException; +import javax.jdo.JDOException; +import javax.jdo.JDOFatalDataStoreException; +import javax.jdo.JDOFatalUserException; +import javax.jdo.JDOObjectNotFoundException; +import javax.jdo.JDOOptimisticVerificationException; +import javax.jdo.JDOUserException; +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; +import javax.jdo.Query; +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; +import org.springframework.transaction.support.ResourceHolder; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class featuring methods for JDO PersistenceManager handling, + * allowing for reuse of PersistenceManager instances within transactions. + * Also provides support for exception translation. + * + *

Used internally by {@link JdoTemplate}, {@link JdoInterceptor} and + * {@link JdoTransactionManager}. Can also be used directly in application code. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see JdoTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public abstract class PersistenceManagerFactoryUtils { + + /** + * Order value for TransactionSynchronization objects that clean up JDO + * PersistenceManagers. Return DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100 + * to execute PersistenceManager cleanup before JDBC Connection cleanup, if any. + * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER + */ + public static final int PERSISTENCE_MANAGER_SYNCHRONIZATION_ORDER = + DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100; + + private static final Log logger = LogFactory.getLog(PersistenceManagerFactoryUtils.class); + + + /** + * Create an appropriate SQLExceptionTranslator for the given PersistenceManagerFactory. + *

If a DataSource is found, creates a SQLErrorCodeSQLExceptionTranslator for the + * DataSource; else, falls back to a SQLStateSQLExceptionTranslator. + * @param connectionFactory the connection factory of the PersistenceManagerFactory + * (may be null) + * @return the SQLExceptionTranslator (never null) + * @see javax.jdo.PersistenceManagerFactory#getConnectionFactory() + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + static SQLExceptionTranslator newJdbcExceptionTranslator(Object connectionFactory) { + // Check for PersistenceManagerFactory's DataSource. + if (connectionFactory instanceof DataSource) { + return new SQLErrorCodeSQLExceptionTranslator((DataSource) connectionFactory); + } + else { + return new SQLStateSQLExceptionTranslator(); + } + } + + /** + * Obtain a JDO PersistenceManager via the given factory. Is aware of a + * corresponding PersistenceManager bound to the current thread, + * for example when using JdoTransactionManager. Will create a new + * PersistenceManager else, if "allowCreate" is true. + * @param pmf PersistenceManagerFactory to create the PersistenceManager with + * @param allowCreate if a non-transactional PersistenceManager should be created + * when no transactional PersistenceManager can be found for the current thread + * @return the PersistenceManager + * @throws DataAccessResourceFailureException if the PersistenceManager couldn't be obtained + * @throws IllegalStateException if no thread-bound PersistenceManager found and + * "allowCreate" is false + * @see JdoTransactionManager + */ + public static PersistenceManager getPersistenceManager(PersistenceManagerFactory pmf, boolean allowCreate) + throws DataAccessResourceFailureException, IllegalStateException { + + try { + return doGetPersistenceManager(pmf, allowCreate); + } + catch (JDOException ex) { + throw new DataAccessResourceFailureException("Could not obtain JDO PersistenceManager", ex); + } + } + + /** + * Obtain a JDO PersistenceManager via the given factory. Is aware of a + * corresponding PersistenceManager bound to the current thread, + * for example when using JdoTransactionManager. Will create a new + * PersistenceManager else, if "allowCreate" is true. + *

Same as getPersistenceManager, but throwing the original JDOException. + * @param pmf PersistenceManagerFactory to create the PersistenceManager with + * @param allowCreate if a non-transactional PersistenceManager should be created + * when no transactional PersistenceManager can be found for the current thread + * @return the PersistenceManager + * @throws JDOException if the PersistenceManager couldn't be created + * @throws IllegalStateException if no thread-bound PersistenceManager found and + * "allowCreate" is false + * @see #getPersistenceManager(javax.jdo.PersistenceManagerFactory, boolean) + * @see JdoTransactionManager + */ + public static PersistenceManager doGetPersistenceManager(PersistenceManagerFactory pmf, boolean allowCreate) + throws JDOException, IllegalStateException { + + Assert.notNull(pmf, "No PersistenceManagerFactory specified"); + + PersistenceManagerHolder pmHolder = + (PersistenceManagerHolder) TransactionSynchronizationManager.getResource(pmf); + if (pmHolder != null) { + if (!pmHolder.isSynchronizedWithTransaction() && + TransactionSynchronizationManager.isSynchronizationActive()) { + pmHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.registerSynchronization( + new PersistenceManagerSynchronization(pmHolder, pmf, false)); + } + return pmHolder.getPersistenceManager(); + } + + if (!allowCreate && !TransactionSynchronizationManager.isSynchronizationActive()) { + throw new IllegalStateException("No JDO PersistenceManager bound to thread, " + + "and configuration does not allow creation of non-transactional one here"); + } + + logger.debug("Opening JDO PersistenceManager"); + PersistenceManager pm = pmf.getPersistenceManager(); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + logger.debug("Registering transaction synchronization for JDO PersistenceManager"); + // Use same PersistenceManager for further JDO actions within the transaction. + // Thread object will get removed by synchronization at transaction completion. + pmHolder = new PersistenceManagerHolder(pm); + pmHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.registerSynchronization( + new PersistenceManagerSynchronization(pmHolder, pmf, true)); + TransactionSynchronizationManager.bindResource(pmf, pmHolder); + } + + return pm; + } + + /** + * Return whether the given JDO PersistenceManager is transactional, that is, + * bound to the current thread by Spring's transaction facilities. + * @param pm the JDO PersistenceManager to check + * @param pmf JDO PersistenceManagerFactory that the PersistenceManager + * was created with (can be null) + * @return whether the PersistenceManager is transactional + */ + public static boolean isPersistenceManagerTransactional( + PersistenceManager pm, PersistenceManagerFactory pmf) { + + if (pmf == null) { + return false; + } + PersistenceManagerHolder pmHolder = + (PersistenceManagerHolder) TransactionSynchronizationManager.getResource(pmf); + return (pmHolder != null && pm == pmHolder.getPersistenceManager()); + } + + /** + * Apply the current transaction timeout, if any, to the given JDO Query object. + * @param query the JDO Query object + * @param pmf JDO PersistenceManagerFactory that the Query was created for + * @param jdoDialect the JdoDialect to use for applying a query timeout + * (must not be null) + * @throws JDOException if thrown by JDO methods + * @see JdoDialect#applyQueryTimeout + */ + public static void applyTransactionTimeout( + Query query, PersistenceManagerFactory pmf, JdoDialect jdoDialect) throws JDOException { + + Assert.notNull(query, "No Query object specified"); + PersistenceManagerHolder pmHolder = + (PersistenceManagerHolder) TransactionSynchronizationManager.getResource(pmf); + if (pmHolder != null && pmHolder.hasTimeout()) { + jdoDialect.applyQueryTimeout(query, pmHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Convert the given JDOException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

The most important cases like object not found or optimistic locking + * failure are covered here. For more fine-granular conversion, JdoAccessor and + * JdoTransactionManager support sophisticated translation of exceptions via a + * JdoDialect. + * @param ex JDOException that occured + * @return the corresponding DataAccessException instance + * @see JdoAccessor#convertJdoAccessException + * @see JdoTransactionManager#convertJdoAccessException + * @see JdoDialect#translateException + */ + public static DataAccessException convertJdoAccessException(JDOException ex) { + if (ex instanceof JDOObjectNotFoundException) { + throw new JdoObjectRetrievalFailureException((JDOObjectNotFoundException) ex); + } + if (ex instanceof JDOOptimisticVerificationException) { + throw new JdoOptimisticLockingFailureException((JDOOptimisticVerificationException) ex); + } + if (ex instanceof JDODataStoreException) { + return new JdoResourceFailureException((JDODataStoreException) ex); + } + if (ex instanceof JDOFatalDataStoreException) { + return new JdoResourceFailureException((JDOFatalDataStoreException) ex); + } + if (ex instanceof JDOUserException) { + return new JdoUsageException((JDOUserException) ex); + } + if (ex instanceof JDOFatalUserException) { + return new JdoUsageException((JDOFatalUserException) ex); + } + // fallback + return new JdoSystemException(ex); + } + + /** + * Close the given PersistenceManager, created via the given factory, + * if it is not managed externally (i.e. not bound to the thread). + * @param pm PersistenceManager to close + * @param pmf PersistenceManagerFactory that the PersistenceManager was created with + * (can be null) + */ + public static void releasePersistenceManager(PersistenceManager pm, PersistenceManagerFactory pmf) { + try { + doReleasePersistenceManager(pm, pmf); + } + catch (JDOException ex) { + logger.debug("Could not close JDO PersistenceManager", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing JDO PersistenceManager", ex); + } + } + + /** + * Actually release a PersistenceManager for the given factory. + * Same as releasePersistenceManager, but throwing the original JDOException. + * @param pm PersistenceManager to close + * @param pmf PersistenceManagerFactory that the PersistenceManager was created with + * (can be null) + * @throws JDOException if thrown by JDO methods + */ + public static void doReleasePersistenceManager(PersistenceManager pm, PersistenceManagerFactory pmf) + throws JDOException { + + if (pm == null) { + return; + } + // Only release non-transactional PersistenceManagers. + if (!isPersistenceManagerTransactional(pm, pmf)) { + logger.debug("Closing JDO PersistenceManager"); + pm.close(); + } + } + + + /** + * Callback for resource cleanup at the end of a non-JDO transaction + * (e.g. when participating in a JtaTransactionManager transaction). + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + private static class PersistenceManagerSynchronization extends ResourceHolderSynchronization + implements Ordered { + + private final boolean newPersistenceManager; + + public PersistenceManagerSynchronization( + PersistenceManagerHolder pmHolder, PersistenceManagerFactory pmf, boolean newPersistenceManager) { + super(pmHolder, pmf); + this.newPersistenceManager = newPersistenceManager; + } + + public int getOrder() { + return PERSISTENCE_MANAGER_SYNCHRONIZATION_ORDER; + } + + protected boolean shouldUnbindAtCompletion() { + return this.newPersistenceManager; + } + + protected void releaseResource(ResourceHolder resourceHolder, Object resourceKey) { + releasePersistenceManager(((PersistenceManagerHolder) resourceHolder).getPersistenceManager(), + (PersistenceManagerFactory) resourceKey); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/PersistenceManagerHolder.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/PersistenceManagerHolder.java new file mode 100644 index 00000000000..c261cb176d5 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/PersistenceManagerHolder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2007 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.jdo; + +import javax.jdo.PersistenceManager; + +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Holder wrapping a JDO PersistenceManager. + * JdoTransactionManager binds instances of this class + * to the thread, for a given PersistenceManagerFactory. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 03.06.2003 + * @see JdoTransactionManager + * @see PersistenceManagerFactoryUtils + */ +public class PersistenceManagerHolder extends ResourceHolderSupport { + + private final PersistenceManager persistenceManager; + + private boolean transactionActive; + + + public PersistenceManagerHolder(PersistenceManager persistenceManager) { + Assert.notNull(persistenceManager, "PersistenceManager must not be null"); + this.persistenceManager = persistenceManager; + } + + + public PersistenceManager getPersistenceManager() { + return this.persistenceManager; + } + + protected void setTransactionActive(boolean transactionActive) { + this.transactionActive = transactionActive; + } + + protected boolean isTransactionActive() { + return this.transactionActive; + } + + public void clear() { + super.clear(); + this.transactionActive = false; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/TransactionAwarePersistenceManagerFactoryProxy.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/TransactionAwarePersistenceManagerFactoryProxy.java new file mode 100644 index 00000000000..753436c4edc --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/TransactionAwarePersistenceManagerFactoryProxy.java @@ -0,0 +1,222 @@ +/* + * Copyright 2002-2008 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.jdo; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Proxy for a target JDO {@link javax.jdo.PersistenceManagerFactory}, + * returning the current thread-bound PersistenceManager (the Spring-managed + * transactional PersistenceManager or a the single OpenPersistenceManagerInView + * PersistenceManager) on getPersistenceManager(), if any. + * + *

Essentially, getPersistenceManager() calls get seamlessly + * forwarded to {@link PersistenceManagerFactoryUtils#getPersistenceManager}. + * Furthermore, PersistenceManager.close calls get forwarded to + * {@link PersistenceManagerFactoryUtils#releasePersistenceManager}. + * + *

The main advantage of this proxy is that it allows DAOs to work with a + * plain JDO PersistenceManagerFactory reference, while still participating in + * Spring's (or a J2EE server's) resource and transaction management. DAOs will + * only rely on the JDO API in such a scenario, without any Spring dependencies. + * + *

Note that the behavior of this proxy matches the behavior that the JDO spec + * defines for a PersistenceManagerFactory as exposed by a JCA connector, when + * deployed in a J2EE server. Hence, DAOs could seamlessly switch between a JNDI + * PersistenceManagerFactory and this proxy for a local PersistenceManagerFactory, + * receiving the reference through Dependency Injection. This will work without + * any Spring API dependencies in the DAO code! + * + *

It is usually preferable to write your JDO-based DAOs with Spring's + * {@link JdoTemplate}, offering benefits such as consistent data access + * exceptions instead of JDOExceptions at the DAO layer. However, Spring's + * resource and transaction management (and Dependency Injection) will work + * for DAOs written against the plain JDO API as well. + * + *

Of course, you can still access the target PersistenceManagerFactory + * even when your DAOs go through this proxy, by defining a bean reference + * that points directly at your target PersistenceManagerFactory bean. + * + * @author Juergen Hoeller + * @since 1.2 + * @see javax.jdo.PersistenceManagerFactory#getPersistenceManager() + * @see javax.jdo.PersistenceManager#close() + * @see PersistenceManagerFactoryUtils#getPersistenceManager + * @see PersistenceManagerFactoryUtils#releasePersistenceManager + */ +public class TransactionAwarePersistenceManagerFactoryProxy implements FactoryBean { + + private PersistenceManagerFactory target; + + private boolean allowCreate = true; + + private PersistenceManagerFactory proxy; + + + /** + * Set the target JDO PersistenceManagerFactory that this proxy should + * delegate to. This should be the raw PersistenceManagerFactory, as + * accessed by JdoTransactionManager. + * @see org.springframework.orm.jdo.JdoTransactionManager + */ + public void setTargetPersistenceManagerFactory(PersistenceManagerFactory target) { + Assert.notNull(target, "Target PersistenceManagerFactory must not be null"); + this.target = target; + Class[] ifcs = ClassUtils.getAllInterfacesForClass(target.getClass(), getClass().getClassLoader()); + this.proxy = (PersistenceManagerFactory) Proxy.newProxyInstance( + target.getClass().getClassLoader(), ifcs, new TransactionAwareFactoryInvocationHandler()); + } + + /** + * Return the target JDO PersistenceManagerFactory that this proxy delegates to. + */ + public PersistenceManagerFactory getTargetPersistenceManagerFactory() { + return this.target; + } + + /** + * Set whether the PersistenceManagerFactory proxy is allowed to create + * a non-transactional PersistenceManager when no transactional + * PersistenceManager can be found for the current thread. + *

Default is "true". Can be turned off to enforce access to + * transactional PersistenceManagers, which safely allows for DAOs + * written to get a PersistenceManager without explicit closing + * (i.e. a PersistenceManagerFactory.getPersistenceManager() + * call without corresponding PersistenceManager.close() call). + * @see PersistenceManagerFactoryUtils#getPersistenceManager(javax.jdo.PersistenceManagerFactory, boolean) + */ + public void setAllowCreate(boolean allowCreate) { + this.allowCreate = allowCreate; + } + + /** + * Return whether the PersistenceManagerFactory proxy is allowed to create + * a non-transactional PersistenceManager when no transactional + * PersistenceManager can be found for the current thread. + */ + protected boolean isAllowCreate() { + return this.allowCreate; + } + + + public Object getObject() { + return this.proxy; + } + + public Class getObjectType() { + return PersistenceManagerFactory.class; + } + + public boolean isSingleton() { + return true; + } + + + /** + * Invocation handler that delegates getPersistenceManager calls on the + * PersistenceManagerFactory proxy to PersistenceManagerFactoryUtils + * for being aware of thread-bound transactions. + */ + private class TransactionAwareFactoryInvocationHandler implements InvocationHandler { + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on PersistenceManagerFactory interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of PersistenceManagerFactory proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("getPersistenceManager")) { + PersistenceManagerFactory target = getTargetPersistenceManagerFactory(); + PersistenceManager pm = + PersistenceManagerFactoryUtils.doGetPersistenceManager(target, isAllowCreate()); + Class[] ifcs = ClassUtils.getAllInterfacesForClass(pm.getClass(), getClass().getClassLoader()); + return (PersistenceManager) Proxy.newProxyInstance( + pm.getClass().getClassLoader(), ifcs, new TransactionAwareInvocationHandler(pm, target)); + } + + // Invoke method on target PersistenceManagerFactory. + try { + return method.invoke(getTargetPersistenceManagerFactory(), args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + + /** + * Invocation handler that delegates close calls on PersistenceManagers to + * PersistenceManagerFactoryUtils for being aware of thread-bound transactions. + */ + private static class TransactionAwareInvocationHandler implements InvocationHandler { + + private final PersistenceManager target; + + private final PersistenceManagerFactory persistenceManagerFactory; + + public TransactionAwareInvocationHandler(PersistenceManager target, PersistenceManagerFactory pmf) { + this.target = target; + this.persistenceManagerFactory = pmf; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on PersistenceManager interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of PersistenceManager proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("close")) { + // Handle close method: only close if not within a transaction. + if (this.persistenceManagerFactory != null) { + PersistenceManagerFactoryUtils.doReleasePersistenceManager( + this.target, this.persistenceManagerFactory); + } + return null; + } + + // Invoke method on target PersistenceManager. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/package.html new file mode 100644 index 00000000000..69de760e087 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/package.html @@ -0,0 +1,9 @@ + + + +Package providing integration of JDO (Java Date Objects) with Spring concepts. +Contains PersistenceManagerFactory helper classes, a template plus callback for JDO +access, and an implementation of Spring's transaction SPI for local JDO transactions. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/JdoDaoSupport.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/JdoDaoSupport.java new file mode 100644 index 00000000000..61d88d9e115 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/JdoDaoSupport.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2008 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.jdo.support; + +import javax.jdo.JDOException; +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.support.DaoSupport; +import org.springframework.orm.jdo.JdoTemplate; +import org.springframework.orm.jdo.PersistenceManagerFactoryUtils; + +/** + * Convenient super class for JDO data access objects. + * + *

Requires a PersistenceManagerFactory to be set, providing a JdoTemplate + * based on it to subclasses. Can alternatively be initialized directly with a + * JdoTemplate, to reuse the latter's settings such as the PersistenceManagerFactory, + * JdoDialect, flush mode, etc. + * + *

This base class is mainly intended for JdoTemplate usage but can also + * be used when working with PersistenceManagerFactoryUtils directly, for example + * in combination with JdoInterceptor-managed PersistenceManagers. Convenience + * getPersistenceManager and releasePersistenceManager + * methods are provided for that usage style. + * + *

This class will create its own JdoTemplate if only a PersistenceManagerFactory + * is passed in. The "allowCreate" flag on that JdoTemplate will be "true" by default. + * A custom JdoTemplate instance can be used through overriding createJdoTemplate. + * + * @author Juergen Hoeller + * @since 28.07.2003 + * @see #setPersistenceManagerFactory + * @see #setJdoTemplate + * @see #createJdoTemplate + * @see #getPersistenceManager + * @see #releasePersistenceManager + * @see org.springframework.orm.jdo.JdoTemplate + * @see org.springframework.orm.jdo.JdoInterceptor + */ +public abstract class JdoDaoSupport extends DaoSupport { + + private JdoTemplate jdoTemplate; + + + /** + * Set the JDO PersistenceManagerFactory to be used by this DAO. + * Will automatically create a JdoTemplate for the given PersistenceManagerFactory. + * @see #createJdoTemplate + * @see #setJdoTemplate + */ + public final void setPersistenceManagerFactory(PersistenceManagerFactory persistenceManagerFactory) { + if (this.jdoTemplate == null || persistenceManagerFactory != this.jdoTemplate.getPersistenceManagerFactory()) { + this.jdoTemplate = createJdoTemplate(persistenceManagerFactory); + } + } + + /** + * Create a JdoTemplate for the given PersistenceManagerFactory. + * Only invoked if populating the DAO with a PersistenceManagerFactory reference! + *

Can be overridden in subclasses to provide a JdoTemplate instance + * with different configuration, or a custom JdoTemplate subclass. + * @param persistenceManagerFactory the JDO PersistenceManagerFactoryto create a JdoTemplate for + * @return the new JdoTemplate instance + * @see #setPersistenceManagerFactory + */ + protected JdoTemplate createJdoTemplate(PersistenceManagerFactory persistenceManagerFactory) { + return new JdoTemplate(persistenceManagerFactory); + } + + /** + * Return the JDO PersistenceManagerFactory used by this DAO. + */ + public final PersistenceManagerFactory getPersistenceManagerFactory() { + return (this.jdoTemplate != null ? this.jdoTemplate.getPersistenceManagerFactory() : null); + } + + /** + * Set the JdoTemplate for this DAO explicitly, + * as an alternative to specifying a PersistenceManagerFactory. + * @see #setPersistenceManagerFactory + */ + public final void setJdoTemplate(JdoTemplate jdoTemplate) { + this.jdoTemplate = jdoTemplate; + } + + /** + * Return the JdoTemplate for this DAO, pre-initialized + * with the PersistenceManagerFactory or set explicitly. + */ + public final JdoTemplate getJdoTemplate() { + return jdoTemplate; + } + + protected final void checkDaoConfig() { + if (this.jdoTemplate == null) { + throw new IllegalArgumentException("persistenceManagerFactory or jdoTemplate is required"); + } + } + + + /** + * Get a JDO PersistenceManager, either from the current transaction or + * a new one. The latter is only allowed if the "allowCreate" setting + * of this bean's JdoTemplate is true. + * @return the JDO PersistenceManager + * @throws DataAccessResourceFailureException if the PersistenceManager couldn't be created + * @throws IllegalStateException if no thread-bound PersistenceManager found and allowCreate false + * @see org.springframework.orm.jdo.PersistenceManagerFactoryUtils#getPersistenceManager + */ + protected final PersistenceManager getPersistenceManager() { + return getPersistenceManager(this.jdoTemplate.isAllowCreate()); + } + + /** + * Get a JDO PersistenceManager, either from the current transaction or + * a new one. The latter is only allowed if "allowCreate" is true. + * @param allowCreate if a non-transactional PersistenceManager should be created + * when no transactional PersistenceManager can be found for the current thread + * @return the JDO PersistenceManager + * @throws DataAccessResourceFailureException if the PersistenceManager couldn't be created + * @throws IllegalStateException if no thread-bound PersistenceManager found and allowCreate false + * @see org.springframework.orm.jdo.PersistenceManagerFactoryUtils#getPersistenceManager + */ + protected final PersistenceManager getPersistenceManager(boolean allowCreate) + throws DataAccessResourceFailureException, IllegalStateException { + + return PersistenceManagerFactoryUtils.getPersistenceManager(getPersistenceManagerFactory(), allowCreate); + } + + /** + * Convert the given JDOException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Delegates to the convertJdoAccessException method of this DAO's JdoTemplate. + * @param ex JDOException that occured + * @return the corresponding DataAccessException instance + * @see #setJdoTemplate + * @see org.springframework.orm.jdo.JdoTemplate#convertJdoAccessException + */ + protected final DataAccessException convertJdoAccessException(JDOException ex) { + return this.jdoTemplate.convertJdoAccessException(ex); + } + + /** + * Close the given JDO PersistenceManager, created via this DAO's + * PersistenceManagerFactory, if it isn't bound to the thread. + * @param pm PersistenceManager to close + * @see org.springframework.orm.jdo.PersistenceManagerFactoryUtils#releasePersistenceManager + */ + protected final void releasePersistenceManager(PersistenceManager pm) { + PersistenceManagerFactoryUtils.releasePersistenceManager(pm, getPersistenceManagerFactory()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/OpenPersistenceManagerInViewFilter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/OpenPersistenceManagerInViewFilter.java new file mode 100644 index 00000000000..e4343a67fd1 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/OpenPersistenceManagerInViewFilter.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2007 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.jdo.support; + +import java.io.IOException; + +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.orm.jdo.PersistenceManagerFactoryUtils; +import org.springframework.orm.jdo.PersistenceManagerHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Servlet 2.3 Filter that binds a JDO PersistenceManager to the thread for the + * entire processing of the request. Intended for the "Open PersistenceManager in + * View" pattern, i.e. to allow for lazy loading in web views despite the + * original transactions already being completed. + * + *

This filter makes JDO PersistenceManagers available via the current thread, + * which will be autodetected by transaction managers. It is suitable for service + * layer transactions via {@link org.springframework.orm.jdo.JdoTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager} as well + * as for non-transactional read-only execution. + * + *

Looks up the PersistenceManagerFactory in Spring's root web application context. + * Supports a "persistenceManagerFactoryBeanName" filter init-param in web.xml; + * the default bean name is "persistenceManagerFactory". Looks up the PersistenceManagerFactory + * on each request, to avoid initialization order issues (when using ContextLoaderServlet, + * the root application context will get initialized after this filter). + * + * @author Juergen Hoeller + * @since 1.1 + * @see OpenPersistenceManagerInViewInterceptor + * @see org.springframework.orm.jdo.JdoInterceptor + * @see org.springframework.orm.jdo.JdoTransactionManager + * @see org.springframework.orm.jdo.PersistenceManagerFactoryUtils#getPersistenceManager + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class OpenPersistenceManagerInViewFilter extends OncePerRequestFilter { + + public static final String DEFAULT_PERSISTENCE_MANAGER_FACTORY_BEAN_NAME = "persistenceManagerFactory"; + + private String persistenceManagerFactoryBeanName = DEFAULT_PERSISTENCE_MANAGER_FACTORY_BEAN_NAME; + + + /** + * Set the bean name of the PersistenceManagerFactory to fetch from Spring's + * root application context. Default is "persistenceManagerFactory". + * @see #DEFAULT_PERSISTENCE_MANAGER_FACTORY_BEAN_NAME + */ + public void setPersistenceManagerFactoryBeanName(String persistenceManagerFactoryBeanName) { + this.persistenceManagerFactoryBeanName = persistenceManagerFactoryBeanName; + } + + /** + * Return the bean name of the PersistenceManagerFactory to fetch from Spring's + * root application context. + */ + protected String getPersistenceManagerFactoryBeanName() { + return this.persistenceManagerFactoryBeanName; + } + + + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + PersistenceManagerFactory pmf = lookupPersistenceManagerFactory(request); + boolean participate = false; + + if (TransactionSynchronizationManager.hasResource(pmf)) { + // Do not modify the PersistenceManager: just set the participate flag. + participate = true; + } + else { + logger.debug("Opening JDO PersistenceManager in OpenPersistenceManagerInViewFilter"); + PersistenceManager pm = PersistenceManagerFactoryUtils.getPersistenceManager(pmf, true); + TransactionSynchronizationManager.bindResource(pmf, new PersistenceManagerHolder(pm)); + } + + try { + filterChain.doFilter(request, response); + } + + finally { + if (!participate) { + PersistenceManagerHolder pmHolder = (PersistenceManagerHolder) + TransactionSynchronizationManager.unbindResource(pmf); + logger.debug("Closing JDO PersistenceManager in OpenPersistenceManagerInViewFilter"); + PersistenceManagerFactoryUtils.releasePersistenceManager(pmHolder.getPersistenceManager(), pmf); + } + } + } + + /** + * Look up the PersistenceManagerFactory that this filter should use, + * taking the current HTTP request as argument. + *

Default implementation delegates to the lookupPersistenceManagerFactory + * without arguments. + * @return the PersistenceManagerFactory to use + * @see #lookupPersistenceManagerFactory() + */ + protected PersistenceManagerFactory lookupPersistenceManagerFactory(HttpServletRequest request) { + return lookupPersistenceManagerFactory(); + } + + /** + * Look up the PersistenceManagerFactory that this filter should use. + * The default implementation looks for a bean with the specified name + * in Spring's root application context. + * @return the PersistenceManagerFactory to use + * @see #getPersistenceManagerFactoryBeanName + */ + protected PersistenceManagerFactory lookupPersistenceManagerFactory() { + if (logger.isDebugEnabled()) { + logger.debug("Using PersistenceManagerFactory '" + getPersistenceManagerFactoryBeanName() + + "' for OpenPersistenceManagerInViewFilter"); + } + WebApplicationContext wac = + WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + return (PersistenceManagerFactory) + wac.getBean(getPersistenceManagerFactoryBeanName(), PersistenceManagerFactory.class); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/OpenPersistenceManagerInViewInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/OpenPersistenceManagerInViewInterceptor.java new file mode 100644 index 00000000000..91c40af077f --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/OpenPersistenceManagerInViewInterceptor.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2007 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.jdo.support; + +import javax.jdo.PersistenceManager; +import javax.jdo.PersistenceManagerFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.orm.jdo.PersistenceManagerFactoryUtils; +import org.springframework.orm.jdo.PersistenceManagerHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.ui.ModelMap; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.context.request.WebRequestInterceptor; + +/** + * Spring web request interceptor that binds a JDO PersistenceManager to the + * thread for the entire processing of the request. Intended for the "Open + * PersistenceManager in View" pattern, i.e. to allow for lazy loading in + * web views despite the original transactions already being completed. + * + *

This interceptor makes JDO PersistenceManagers available via the current thread, + * which will be autodetected by transaction managers. It is suitable for service + * layer transactions via {@link org.springframework.orm.jdo.JdoTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager} as well + * as for non-transactional read-only execution. + * + *

In contrast to {@link OpenPersistenceManagerInViewFilter}, this interceptor + * is set up in a Spring application context and can thus take advantage of + * bean wiring. It inherits common JDO configuration properties from + * {@link org.springframework.orm.jdo.JdoAccessor}, to be configured in a + * bean definition. + * + * @author Juergen Hoeller + * @since 1.1 + * @see OpenPersistenceManagerInViewFilter + * @see org.springframework.orm.jdo.JdoInterceptor + * @see org.springframework.orm.jdo.JdoTransactionManager + * @see org.springframework.orm.jdo.PersistenceManagerFactoryUtils#getPersistenceManager + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class OpenPersistenceManagerInViewInterceptor implements WebRequestInterceptor { + + /** + * Suffix that gets appended to the PersistenceManagerFactory toString + * representation for the "participate in existing persistence manager + * handling" request attribute. + * @see #getParticipateAttributeName + */ + public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private PersistenceManagerFactory persistenceManagerFactory; + + + /** + * Set the JDO PersistenceManagerFactory that should be used to create + * PersistenceManagers. + */ + public void setPersistenceManagerFactory(PersistenceManagerFactory pmf) { + this.persistenceManagerFactory = pmf; + } + + /** + * Return the JDO PersistenceManagerFactory that should be used to create + * PersistenceManagers. + */ + public PersistenceManagerFactory getPersistenceManagerFactory() { + return persistenceManagerFactory; + } + + + public void preHandle(WebRequest request) throws DataAccessException { + if (TransactionSynchronizationManager.hasResource(getPersistenceManagerFactory())) { + // Do not modify the PersistenceManager: just mark the request accordingly. + String participateAttributeName = getParticipateAttributeName(); + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + int newCount = (count != null) ? count.intValue() + 1 : 1; + request.setAttribute(getParticipateAttributeName(), new Integer(newCount), WebRequest.SCOPE_REQUEST); + } + else { + logger.debug("Opening JDO PersistenceManager in OpenPersistenceManagerInViewInterceptor"); + PersistenceManager pm = + PersistenceManagerFactoryUtils.getPersistenceManager(getPersistenceManagerFactory(), true); + TransactionSynchronizationManager.bindResource( + getPersistenceManagerFactory(), new PersistenceManagerHolder(pm)); + } + } + + public void postHandle(WebRequest request, ModelMap model) { + } + + public void afterCompletion(WebRequest request, Exception ex) throws DataAccessException { + String participateAttributeName = getParticipateAttributeName(); + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + if (count != null) { + // Do not modify the PersistenceManager: just clear the marker. + if (count.intValue() > 1) { + request.setAttribute(participateAttributeName, new Integer(count.intValue() - 1), WebRequest.SCOPE_REQUEST); + } + else { + request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + } + } + else { + PersistenceManagerHolder pmHolder = (PersistenceManagerHolder) + TransactionSynchronizationManager.unbindResource(getPersistenceManagerFactory()); + logger.debug("Closing JDO PersistenceManager in OpenPersistenceManagerInViewInterceptor"); + PersistenceManagerFactoryUtils.releasePersistenceManager( + pmHolder.getPersistenceManager(), getPersistenceManagerFactory()); + } + } + + /** + * Return the name of the request attribute that identifies that a request is + * already filtered. Default implementation takes the toString representation + * of the PersistenceManagerFactory instance and appends ".FILTERED". + * @see #PARTICIPATE_SUFFIX + */ + protected String getParticipateAttributeName() { + return getPersistenceManagerFactory().toString() + PARTICIPATE_SUFFIX; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/package.html new file mode 100644 index 00000000000..a220f24ff73 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jdo/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.orm.jdo package. +Contains a DAO base class for JdoTemplate usage. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java new file mode 100644 index 00000000000..0945d2587f0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -0,0 +1,448 @@ +/* + * Copyright 2002-2008 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.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Abstract {@link org.springframework.beans.factory.FactoryBean} that + * creates a local JPA {@link javax.persistence.EntityManagerFactory} + * instance within a Spring application context. + * + *

Encapsulates the common functionality between the different JPA + * bootstrap contracts (standalone as well as container). + * + *

Implements support for standard JPA configuration as well as + * Spring's {@link JpaVendorAdapter} abstraction, and controls the + * EntityManagerFactory's lifecycle. + * + *

This class also implements the + * {@link org.springframework.dao.support.PersistenceExceptionTranslator} + * interface, as autodetected by Spring's + * {@link org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor}, + * for AOP-based translation of native exceptions to Spring DataAccessExceptions. + * Hence, the presence of e.g. LocalEntityManagerFactoryBean automatically enables + * a PersistenceExceptionTranslationPostProcessor to translate JPA exceptions. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see LocalEntityManagerFactoryBean + * @see LocalContainerEntityManagerFactoryBean + */ +public abstract class AbstractEntityManagerFactoryBean implements + FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean, + EntityManagerFactoryInfo, PersistenceExceptionTranslator { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private PersistenceProvider persistenceProvider; + + private String persistenceUnitName; + + private final Map jpaPropertyMap = new HashMap(); + + private Class entityManagerFactoryInterface; + + private Class entityManagerInterface; + + private JpaDialect jpaDialect; + + private JpaVendorAdapter jpaVendorAdapter; + + private ClassLoader beanClassLoader = getClass().getClassLoader(); + + /** Raw EntityManagerFactory as returned by the PersistenceProvider */ + public EntityManagerFactory nativeEntityManagerFactory; + + private EntityManagerFactory entityManagerFactory; + + + /** + * Set the PersistenceProvider implementation class to use for creating the + * EntityManagerFactory. If not specified, the persistence provider will be + * taken from the JpaVendorAdapter (if any) or retrieved through scanning + * (as far as possible). + * @see JpaVendorAdapter#getPersistenceProvider() + * @see javax.persistence.spi.PersistenceProvider + * @see javax.persistence.Persistence + */ + public void setPersistenceProviderClass(Class persistenceProviderClass) { + Assert.isAssignable(PersistenceProvider.class, persistenceProviderClass); + this.persistenceProvider = (PersistenceProvider) BeanUtils.instantiateClass(persistenceProviderClass); + } + + /** + * Set the PersistenceProvider instance to use for creating the + * EntityManagerFactory. If not specified, the persistence provider + * will be taken from the JpaVendorAdapter (if any) or determined + * by the persistence unit deployment descriptor (as far as possible). + * @see JpaVendorAdapter#getPersistenceProvider() + * @see javax.persistence.spi.PersistenceProvider + * @see javax.persistence.Persistence + */ + public void setPersistenceProvider(PersistenceProvider persistenceProvider) { + this.persistenceProvider = persistenceProvider; + } + + public PersistenceProvider getPersistenceProvider() { + return this.persistenceProvider; + } + + /** + * Specify the name of the EntityManagerFactory configuration. + *

Default is none, indicating the default EntityManagerFactory + * configuration. The persistence provider will throw an exception if + * ambiguous EntityManager configurations are found. + * @see javax.persistence.Persistence#createEntityManagerFactory(String) + */ + public void setPersistenceUnitName(String persistenceUnitName) { + this.persistenceUnitName = persistenceUnitName; + } + + public String getPersistenceUnitName() { + return this.persistenceUnitName; + } + + /** + * Specify JPA properties, to be passed into + * Persistence.createEntityManagerFactory (if any). + *

Can be populated with a String "value" (parsed via PropertiesEditor) or a + * "props" element in XML bean definitions. + * @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map) + * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map) + */ + public void setJpaProperties(Properties jpaProperties) { + CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.jpaPropertyMap); + } + + /** + * Specify JPA properties as a Map, to be passed into + * Persistence.createEntityManagerFactory (if any). + *

Can be populated with a "map" or "props" element in XML bean definitions. + * @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map) + * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map) + */ + public void setJpaPropertyMap(Map jpaProperties) { + if (jpaProperties != null) { + this.jpaPropertyMap.putAll(jpaProperties); + } + } + + /** + * Allow Map access to the JPA properties to be passed to the persistence + * provider, with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via + * "jpaPropertyMap[myKey]". + */ + public Map getJpaPropertyMap() { + return this.jpaPropertyMap; + } + + /** + * Specify the (potentially vendor-specific) EntityManagerFactory interface + * that this EntityManagerFactory proxy is supposed to implement. + *

The default will be taken from the specific JpaVendorAdapter, if any, + * or set to the standard javax.persistence.EntityManagerFactory + * interface else. + * @see JpaVendorAdapter#getEntityManagerFactoryInterface() + */ + public void setEntityManagerFactoryInterface(Class emfInterface) { + Assert.isAssignable(EntityManagerFactory.class, emfInterface); + this.entityManagerFactoryInterface = emfInterface; + } + + /** + * Specify the (potentially vendor-specific) EntityManager interface + * that this factory's EntityManagers are supposed to implement. + *

The default will be taken from the specific JpaVendorAdapter, if any, + * or set to the standard javax.persistence.EntityManager + * interface else. + * @see JpaVendorAdapter#getEntityManagerInterface() + * @see EntityManagerFactoryInfo#getEntityManagerInterface() + */ + public void setEntityManagerInterface(Class emInterface) { + Assert.isAssignable(EntityManager.class, emInterface); + this.entityManagerInterface = emInterface; + } + + public Class getEntityManagerInterface() { + return this.entityManagerInterface; + } + + /** + * Specify the vendor-specific JpaDialect implementation to associate with + * this EntityManagerFactory. This will be exposed through the + * EntityManagerFactoryInfo interface, to be picked up as default dialect by + * accessors that intend to use JpaDialect functionality. + * @see EntityManagerFactoryInfo#getJpaDialect() + */ + public void setJpaDialect(JpaDialect jpaDialect) { + this.jpaDialect = jpaDialect; + } + + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + /** + * Specify the JpaVendorAdapter implementation for the desired JPA provider, + * if any. This will initialize appropriate defaults for the given provider, + * such as persistence provider class and JpaDialect, unless locally + * overridden in this FactoryBean. + */ + public void setJpaVendorAdapter(JpaVendorAdapter jpaVendorAdapter) { + this.jpaVendorAdapter = jpaVendorAdapter; + } + + /** + * Return the JpaVendorAdapter implementation for this + * EntityManagerFactory, or null if not known. + */ + public JpaVendorAdapter getJpaVendorAdapter() { + return this.jpaVendorAdapter; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public ClassLoader getBeanClassLoader() { + return this.beanClassLoader; + } + + + public final void afterPropertiesSet() throws PersistenceException { + if (this.jpaVendorAdapter != null) { + if (this.persistenceProvider == null) { + this.persistenceProvider = this.jpaVendorAdapter.getPersistenceProvider(); + } + Map vendorPropertyMap = this.jpaVendorAdapter.getJpaPropertyMap(); + if (vendorPropertyMap != null) { + for (Iterator it = vendorPropertyMap.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + if (!this.jpaPropertyMap.containsKey(entry.getKey())) { + this.jpaPropertyMap.put(entry.getKey(), entry.getValue()); + } + } + } + if (this.entityManagerFactoryInterface == null) { + this.entityManagerFactoryInterface = this.jpaVendorAdapter.getEntityManagerFactoryInterface(); + if (!ClassUtils.isVisible(this.entityManagerFactoryInterface, this.beanClassLoader)) { + this.entityManagerFactoryInterface = EntityManagerFactory.class; + } + } + if (this.entityManagerInterface == null) { + this.entityManagerInterface = this.jpaVendorAdapter.getEntityManagerInterface(); + if (!ClassUtils.isVisible(this.entityManagerInterface, this.beanClassLoader)) { + this.entityManagerInterface = EntityManager.class; + } + } + if (this.jpaDialect == null) { + this.jpaDialect = this.jpaVendorAdapter.getJpaDialect(); + } + } + + this.nativeEntityManagerFactory = createNativeEntityManagerFactory(); + if (this.nativeEntityManagerFactory == null) { + throw new IllegalStateException( + "JPA PersistenceProvider returned null EntityManagerFactory - check your JPA provider setup!"); + } + if (this.jpaVendorAdapter != null) { + this.jpaVendorAdapter.postProcessEntityManagerFactory(this.nativeEntityManagerFactory); + } + + // Wrap the EntityManagerFactory in a factory implementing all its interfaces. + // This allows interception of createEntityManager methods to return an + // application-managed EntityManager proxy that automatically joins + // existing transactions. + this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory); + } + + /** + * Create a proxy of the given EntityManagerFactory. We do this to be able + * to return transaction-aware proxies for application-managed + * EntityManagers, and to introduce the NamedEntityManagerFactory interface + * @param emf EntityManagerFactory as returned by the persistence provider + * @return proxy entity manager + */ + protected EntityManagerFactory createEntityManagerFactoryProxy(EntityManagerFactory emf) { + Set ifcs = new LinkedHashSet(); + if (this.entityManagerFactoryInterface != null) { + ifcs.add(this.entityManagerFactoryInterface); + } + else { + ifcs.addAll(ClassUtils.getAllInterfacesForClassAsSet(emf.getClass(), this.beanClassLoader)); + } + ifcs.add(EntityManagerFactoryInfo.class); + EntityManagerFactoryPlusOperations plusOperations = null; + if (getJpaDialect() != null && getJpaDialect().supportsEntityManagerFactoryPlusOperations()) { + plusOperations = getJpaDialect().getEntityManagerFactoryPlusOperations(emf); + ifcs.add(EntityManagerFactoryPlusOperations.class); + } + return (EntityManagerFactory) Proxy.newProxyInstance( + this.beanClassLoader, ifcs.toArray(new Class[ifcs.size()]), + new ManagedEntityManagerFactoryInvocationHandler(emf, this, plusOperations)); + } + + /** + * Subclasses must implement this method to create the EntityManagerFactory + * that will be returned by the getObject() method. + * @return EntityManagerFactory instance returned by this FactoryBean + * @throws PersistenceException if the EntityManager cannot be created + */ + protected abstract EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException; + + + /** + * Implementation of the PersistenceExceptionTranslator interface, as + * autodetected by Spring's PersistenceExceptionTranslationPostProcessor. + *

Uses the dialect's conversion if possible; otherwise falls back to + * standard JPA exception conversion. + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * @see JpaDialect#translateExceptionIfPossible + * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return (this.jpaDialect != null ? this.jpaDialect.translateExceptionIfPossible(ex) : + EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex)); + } + + public EntityManagerFactory getNativeEntityManagerFactory() { + return this.nativeEntityManagerFactory; + } + + public PersistenceUnitInfo getPersistenceUnitInfo() { + return null; + } + + public DataSource getDataSource() { + return null; + } + + + /** + * Return the singleton EntityManagerFactory. + */ + public EntityManagerFactory getObject() { + return this.entityManagerFactory; + } + + public Class getObjectType() { + return (this.entityManagerFactory != null ? this.entityManagerFactory.getClass() : EntityManagerFactory.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Close the EntityManagerFactory on bean factory shutdown. + */ + public void destroy() { + if (logger.isInfoEnabled()) { + logger.info("Closing JPA EntityManagerFactory for persistence unit '" + getPersistenceUnitName() + "'"); + } + this.entityManagerFactory.close(); + } + + + /** + * Dynamic proxy invocation handler proxying an EntityManagerFactory to + * return a proxy EntityManager if necessary from createEntityManager() + * methods. + */ + private static class ManagedEntityManagerFactoryInvocationHandler implements InvocationHandler { + + private final EntityManagerFactory targetEntityManagerFactory; + + private final EntityManagerFactoryInfo entityManagerFactoryInfo; + + private final EntityManagerFactoryPlusOperations entityManagerFactoryPlusOperations; + + public ManagedEntityManagerFactoryInvocationHandler(EntityManagerFactory targetEmf, + EntityManagerFactoryInfo emfInfo, EntityManagerFactoryPlusOperations entityManagerFactoryPlusOperations) { + + this.targetEntityManagerFactory = targetEmf; + this.entityManagerFactoryInfo = emfInfo; + this.entityManagerFactoryPlusOperations = entityManagerFactoryPlusOperations; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of EntityManagerFactory proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getDeclaringClass().isAssignableFrom(EntityManagerFactoryInfo.class)) { + return method.invoke(this.entityManagerFactoryInfo, args); + } + else if (method.getDeclaringClass().equals(EntityManagerFactoryPlusOperations.class)) { + return method.invoke(this.entityManagerFactoryPlusOperations, args); + } + + Object retVal = method.invoke(this.targetEntityManagerFactory, args); + if (retVal instanceof EntityManager) { + EntityManager rawEntityManager = (EntityManager) retVal; + retVal = ExtendedEntityManagerCreator.createApplicationManagedEntityManager( + rawEntityManager, this.entityManagerFactoryInfo); + } + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java new file mode 100644 index 00000000000..6e3aab31551 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2007 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.io.Serializable; +import java.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * Default implementation of the {@link JpaDialect} interface. + * Used as default dialect by {@link JpaAccessor} and {@link JpaTransactionManager}. + * + *

Simply begins a standard JPA transaction in {@link #beginTransaction} + * and performs standard exception translation through {@link EntityManagerFactoryUtils}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see JpaAccessor#setJpaDialect + * @see JpaTransactionManager#setJpaDialect + */ +public class DefaultJpaDialect implements JpaDialect, Serializable { + + //------------------------------------------------------------------------- + // Hooks for transaction management (used by JpaTransactionManager) + //------------------------------------------------------------------------- + + /** + * This implementation invokes the standard JPA Transaction.begin + * method. Throws an InvalidIsolationLevelException if a non-default isolation + * level is set. + *

This implementation does not return any transaction data Object, since there + * is no state to be kept for a standard JPA transaction. Hence, subclasses do not + * have to care about the return value (null) of this implementation + * and are free to return their own transaction data Object. + * @see javax.persistence.EntityTransaction#begin + * @see org.springframework.transaction.InvalidIsolationLevelException + * @see #cleanupTransaction + */ + public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + throws PersistenceException, SQLException, TransactionException { + + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + throw new InvalidIsolationLevelException( + "Standard JPA does not support custom isolation levels - " + + "use a special JpaDialect for your JPA implementation"); + } + entityManager.getTransaction().begin(); + return null; + } + + public Object prepareTransaction(EntityManager entityManager, boolean readOnly, String name) + throws PersistenceException { + + return null; + } + + /** + * This implementation does nothing, since the default beginTransaction + * implementation does not require any cleanup. + * @see #beginTransaction + */ + public void cleanupTransaction(Object transactionData) { + } + + /** + * This implementation always returns null, + * indicating that no JDBC Connection can be provided. + */ + public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) + throws PersistenceException, SQLException { + + return null; + } + + /** + * This implementation does nothing, assuming that the Connection + * will implicitly be closed with the EntityManager. + *

If the JPA implementation returns a Connection handle that it expects + * the application to close after use, the dialect implementation needs to invoke + * Connection.close() (or some other method with similar effect) here. + * @see java.sql.Connection#close() + */ + public void releaseJdbcConnection(ConnectionHandle conHandle, EntityManager em) + throws PersistenceException, SQLException { + } + + + //----------------------------------------------------------------------------------- + // Hook for exception translation (used by JpaTransactionManager and JpaTemplate) + //----------------------------------------------------------------------------------- + + /** + * This implementation delegates to EntityManagerFactoryUtils. + * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex); + } + + + public boolean supportsEntityManagerFactoryPlusOperations() { + return false; + } + + public boolean supportsEntityManagerPlusOperations() { + return false; + } + + public EntityManagerFactoryPlusOperations getEntityManagerFactoryPlusOperations(EntityManagerFactory rawEntityManager) { + throw new UnsupportedOperationException(getClass().getName() + " does not support EntityManagerFactoryPlusOperations"); + } + + public EntityManagerPlusOperations getEntityManagerPlusOperations(EntityManager rawEntityManager) { + throw new UnsupportedOperationException(getClass().getName() + " does not support EntityManagerPlusOperations"); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java new file mode 100644 index 00000000000..a21cfaf514e --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2008 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.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Base class for any class that needs to access an EntityManagerFactory, + * usually in order to obtain an EntityManager. Defines common properties. + * + *

Not intended to be used directly. See {@link JpaAccessor}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see JpaAccessor + * @see EntityManagerFactoryUtils + */ +public abstract class EntityManagerFactoryAccessor { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private EntityManagerFactory entityManagerFactory; + + private final Map jpaPropertyMap = new HashMap(); + + + /** + * Set the JPA EntityManagerFactory that should be used to create + * EntityManagers. + * @see javax.persistence.EntityManagerFactory#createEntityManager() + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + public void setEntityManagerFactory(EntityManagerFactory emf) { + this.entityManagerFactory = emf; + } + + /** + * Return the JPA EntityManagerFactory that should be used to create + * EntityManagers. + */ + public EntityManagerFactory getEntityManagerFactory() { + return this.entityManagerFactory; + } + + /** + * Specify JPA properties, to be passed into + * EntityManagerFactory.createEntityManager(Map) (if any). + *

Can be populated with a String "value" (parsed via PropertiesEditor) + * or a "props" element in XML bean definitions. + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + public void setJpaProperties(Properties jpaProperties) { + CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.jpaPropertyMap); + } + + /** + * Specify JPA properties as a Map, to be passed into + * EntityManagerFactory.createEntityManager(Map) (if any). + *

Can be populated with a "map" or "props" element in XML bean definitions. + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + public void setJpaPropertyMap(Map jpaProperties) { + if (jpaProperties != null) { + this.jpaPropertyMap.putAll(jpaProperties); + } + } + + /** + * Allow Map access to the JPA properties to be passed to the persistence + * provider, with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via "jpaPropertyMap[myKey]". + */ + public Map getJpaPropertyMap() { + return this.jpaPropertyMap; + } + + + /** + * Obtain a new EntityManager from this accessor's EntityManagerFactory. + *

Can be overridden in subclasses to create specific EntityManager variants. + * @return a new EntityManager + * @throws IllegalStateException if this accessor is not configured with an EntityManagerFactory + * @see javax.persistence.EntityManagerFactory#createEntityManager() + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + protected EntityManager createEntityManager() throws IllegalStateException { + EntityManagerFactory emf = getEntityManagerFactory(); + Assert.state(emf != null, "No EntityManagerFactory specified"); + Map properties = getJpaPropertyMap(); + return (!CollectionUtils.isEmpty(properties) ? emf.createEntityManager(properties) : emf.createEntityManager()); + } + + /** + * Obtain the transactional EntityManager for this accessor's EntityManagerFactory, if any. + * @return the transactional EntityManager, or null if none + * @throws IllegalStateException if this accessor is not configured with an EntityManagerFactory + * @see EntityManagerFactoryUtils#getTransactionalEntityManager(javax.persistence.EntityManagerFactory) + * @see EntityManagerFactoryUtils#getTransactionalEntityManager(javax.persistence.EntityManagerFactory, java.util.Map) + */ + protected EntityManager getTransactionalEntityManager() throws IllegalStateException{ + EntityManagerFactory emf = getEntityManagerFactory(); + Assert.state(emf != null, "No EntityManagerFactory specified"); + return EntityManagerFactoryUtils.getTransactionalEntityManager(emf, getJpaPropertyMap()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java new file mode 100644 index 00000000000..83cf10d169f --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2008 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 javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.sql.DataSource; + +/** + * Metadata interface for a Spring-managed JPA {@link EntityManagerFactory}. + * + *

This facility can be obtained from Spring-managed EntityManagerFactory + * proxies through casting the EntityManagerFactory handle to this interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +public interface EntityManagerFactoryInfo { + + /** + * Return the raw underlying EntityManagerFactory. + * @return the unadorned EntityManagerFactory (never null) + */ + EntityManagerFactory getNativeEntityManagerFactory(); + + /** + * Return the underlying PersistenceProvider that the underlying + * EntityManagerFactory was created with. + * @return the PersistenceProvider used to create this EntityManagerFactory, + * or null if the standard JPA provider autodetection process + * was used to configure the EntityManagerFactory + */ + PersistenceProvider getPersistenceProvider(); + + /** + * Return the PersistenceUnitInfo used to create this + * EntityManagerFactory, if the in-container API was used. + * @return the PersistenceUnitInfo used to create this EntityManagerFactory, + * or null if the in-container contract was not used to + * configure the EntityManagerFactory + */ + PersistenceUnitInfo getPersistenceUnitInfo(); + + /** + * Return the name of the persistence unit used to create this + * EntityManagerFactory, or null if + * it is an unnamed default. If getPersistenceUnitInfo() + * returns non-null, the return type of getPersistenceUnitName() + * must be equal to the value returned by + * PersistenceUnitInfo.getPersistenceUnitName(). + * @see #getPersistenceUnitInfo() + * @see javax.persistence.spi.PersistenceUnitInfo#getPersistenceUnitName() + */ + String getPersistenceUnitName(); + + /** + * Return the JDBC DataSource that this EntityManagerFactory + * obtains its JDBC Connections from. + * @return the JDBC DataSource, or null if not known + */ + DataSource getDataSource(); + + /** + * Return the (potentially vendor-specific) EntityManager interface + * that this factory's EntityManagers will implement. + *

A null return value suggests that autodetection is supposed + * to happen: either based on a target EntityManager instance + * or simply defaulting to javax.persistence.EntityManager. + */ + Class getEntityManagerInterface(); + + /** + * Return the vendor-specific JpaDialect implementation for this + * EntityManagerFactory, or null if not known. + */ + JpaDialect getJpaDialect(); + + /** + * Return the ClassLoader that the application's beans are loaded with. + *

Proxies will be generated in this ClassLoader. + */ + ClassLoader getBeanClassLoader(); + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryPlus.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryPlus.java new file mode 100644 index 00000000000..9af3079dfed --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryPlus.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2006 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 javax.persistence.EntityManagerFactory; + +/** + * Extension of the standard JPA EntityManagerFactory interface, linking in + * Spring's EntityManagerFactoryPlusOperations interface which defines + * additional operations (beyond JPA 1.0) in a vendor-independent fashion. + * + * @author Rod Johnson + * @since 2.0 + * @see javax.persistence.EntityManager + */ +public interface EntityManagerFactoryPlus extends EntityManagerFactory, EntityManagerFactoryPlusOperations { + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryPlusOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryPlusOperations.java new file mode 100644 index 00000000000..554fbdf9837 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryPlusOperations.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 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; + +/** + * Interface that defines common operations beyond the standard + * JPA EntityManagerFactory interface, in a vendor-independent fashion. + * To be adapted to specific JPA providers through a JpaDialect. + * + *

As of Spring 2.0, this interface does not define any operations yet. + * The pass-through mechanism to the underlying JpaDialect is already in + * place. Concrete operations will be added in the Spring 2.5 timeframe. + * + * @author Rod Johnson + * @since 2.0 + * @see JpaDialect#getEntityManagerPlusOperations + * @see javax.persistence.EntityManagerFactory + */ +public interface EntityManagerFactoryPlusOperations { + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java new file mode 100644 index 00000000000..0141505727b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java @@ -0,0 +1,367 @@ +/* + * Copyright 2002-2008 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.util.Map; + +import javax.persistence.EntityExistsException; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityNotFoundException; +import javax.persistence.NoResultException; +import javax.persistence.NonUniqueResultException; +import javax.persistence.OptimisticLockException; +import javax.persistence.PersistenceException; +import javax.persistence.TransactionRequiredException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.ResourceHolder; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Helper class featuring methods for JPA EntityManager handling, + * allowing for reuse of EntityManager instances within transactions. + * Also provides support for exception translation. + * + *

Mainly intended for internal use within the framework. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class EntityManagerFactoryUtils { + + /** + * Order value for TransactionSynchronization objects that clean up JPA + * EntityManagers. Return DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100 + * to execute EntityManager cleanup before JDBC Connection cleanup, if any. + * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER + */ + public static final int ENTITY_MANAGER_SYNCHRONIZATION_ORDER = + DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100; + + private static final Log logger = LogFactory.getLog(EntityManagerFactoryUtils.class); + + + /** + * Find an EntityManagerFactory with the given name in the given + * Spring application context (represented as ListableBeanFactory). + *

The specified unit name will be matched against the configured + * peristence unit, provided that a discovered EntityManagerFactory + * implements the {@link EntityManagerFactoryInfo} interface. If not, + * the persistence unit name will be matched against the Spring bean name, + * assuming that the EntityManagerFactory bean names follow that convention. + * @param beanFactory the ListableBeanFactory to search + * @param unitName the name of the persistence unit (never empty) + * @return the EntityManagerFactory + * @throws NoSuchBeanDefinitionException if there is no such EntityManagerFactory in the context + * @see EntityManagerFactoryInfo#getPersistenceUnitName() + */ + public static EntityManagerFactory findEntityManagerFactory( + ListableBeanFactory beanFactory, String unitName) throws NoSuchBeanDefinitionException { + + Assert.notNull(beanFactory, "ListableBeanFactory must not be null"); + Assert.hasLength(unitName, "Unit name must not be empty"); + + // See whether we can find an EntityManagerFactory with matching persistence unit name. + String[] candidateNames = + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, EntityManagerFactory.class); + for (String candidateName : candidateNames) { + EntityManagerFactory emf = (EntityManagerFactory) beanFactory.getBean(candidateName); + if (emf instanceof EntityManagerFactoryInfo) { + if (unitName.equals(((EntityManagerFactoryInfo) emf).getPersistenceUnitName())) { + return emf; + } + } + } + // No matching persistence unit found - simply take the EntityManagerFactory + // with the persistence unit name as bean name (by convention). + return (EntityManagerFactory) beanFactory.getBean(unitName, EntityManagerFactory.class); + } + + /** + * Obtain a JPA EntityManager from the given factory. Is aware of a + * corresponding EntityManager bound to the current thread, + * for example when using JpaTransactionManager. + *

Note: Will return null if no thread-bound EntityManager found! + * @param emf EntityManagerFactory to create the EntityManager with + * @return the EntityManager, or null if none found + * @throws DataAccessResourceFailureException if the EntityManager couldn't be obtained + * @see JpaTransactionManager + */ + public static EntityManager getTransactionalEntityManager(EntityManagerFactory emf) + throws DataAccessResourceFailureException { + + return getTransactionalEntityManager(emf, null); + } + + /** + * Obtain a JPA EntityManager from the given factory. Is aware of a + * corresponding EntityManager bound to the current thread, + * for example when using JpaTransactionManager. + *

Note: Will return null if no thread-bound EntityManager found! + * @param emf EntityManagerFactory to create the EntityManager with + * @param properties the properties to be passed into the createEntityManager + * call (may be null) + * @return the EntityManager, or null if none found + * @throws DataAccessResourceFailureException if the EntityManager couldn't be obtained + * @see JpaTransactionManager + */ + public static EntityManager getTransactionalEntityManager(EntityManagerFactory emf, Map properties) + throws DataAccessResourceFailureException { + try { + return doGetTransactionalEntityManager(emf, properties); + } + catch (PersistenceException ex) { + throw new DataAccessResourceFailureException("Could not obtain JPA EntityManager", ex); + } + } + + /** + * Obtain a JPA EntityManager from the given factory. Is aware of a + * corresponding EntityManager bound to the current thread, + * for example when using JpaTransactionManager. + *

Same as getEntityManager, but throwing the original PersistenceException. + * @param emf EntityManagerFactory to create the EntityManager with + * @param properties the properties to be passed into the createEntityManager + * call (may be null) + * @return the EntityManager, or null if none found + * @throws javax.persistence.PersistenceException if the EntityManager couldn't be created + * @see #getTransactionalEntityManager(javax.persistence.EntityManagerFactory) + * @see JpaTransactionManager + */ + public static EntityManager doGetTransactionalEntityManager( + EntityManagerFactory emf, Map properties) throws PersistenceException { + + Assert.notNull(emf, "No EntityManagerFactory specified"); + + EntityManagerHolder emHolder = + (EntityManagerHolder) TransactionSynchronizationManager.getResource(emf); + if (emHolder != null) { + if (!emHolder.isSynchronizedWithTransaction() && + TransactionSynchronizationManager.isSynchronizationActive()) { + // Try to explicitly synchronize the EntityManager itself + // with an ongoing JTA transaction, if any. + try { + emHolder.getEntityManager().joinTransaction(); + } + catch (TransactionRequiredException ex) { + logger.debug("Could not join JTA transaction because none was active", ex); + } + Object transactionData = prepareTransaction(emHolder.getEntityManager(), emf); + TransactionSynchronizationManager.registerSynchronization( + new EntityManagerSynchronization(emHolder, emf, transactionData, false)); + emHolder.setSynchronizedWithTransaction(true); + } + return emHolder.getEntityManager(); + } + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + // Indicate that we can't obtain a transactional EntityManager. + return null; + } + + // Create a new EntityManager for use within the current transaction. + logger.debug("Opening JPA EntityManager"); + EntityManager em = + (!CollectionUtils.isEmpty(properties) ? emf.createEntityManager(properties) : emf.createEntityManager()); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + logger.debug("Registering transaction synchronization for JPA EntityManager"); + // Use same EntityManager for further JPA actions within the transaction. + // Thread object will get removed by synchronization at transaction completion. + emHolder = new EntityManagerHolder(em); + Object transactionData = prepareTransaction(em, emf); + TransactionSynchronizationManager.registerSynchronization( + new EntityManagerSynchronization(emHolder, emf, transactionData, true)); + emHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(emf, emHolder); + } + + return em; + } + + /** + * Prepare a transaction on the given EntityManager, if possible. + * @param em the EntityManager to prepare + * @param emf the EntityManagerFactory that the EntityManager has been created with + * @return an arbitrary object that holds transaction data, if any + * (to be passed into cleanupTransaction) + * @see JpaDialect#prepareTransaction + */ + private static Object prepareTransaction(EntityManager em, EntityManagerFactory emf) { + if (emf instanceof EntityManagerFactoryInfo) { + EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) emf; + JpaDialect jpaDialect = emfInfo.getJpaDialect(); + if (jpaDialect != null) { + return jpaDialect.prepareTransaction(em, + TransactionSynchronizationManager.isCurrentTransactionReadOnly(), + TransactionSynchronizationManager.getCurrentTransactionName()); + } + } + return null; + } + + /** + * Prepare a transaction on the given EntityManager, if possible. + * @param transactionData arbitrary object that holds transaction data, if any + * (as returned by prepareTransaction) + * @param emf the EntityManagerFactory that the EntityManager has been created with + * @see JpaDialect#cleanupTransaction + */ + private static void cleanupTransaction(Object transactionData, EntityManagerFactory emf) { + if (emf instanceof EntityManagerFactoryInfo) { + EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) emf; + JpaDialect jpaDialect = emfInfo.getJpaDialect(); + if (jpaDialect != null) { + jpaDialect.cleanupTransaction(transactionData); + } + } + } + + /** + * Convert the given runtime exception to an appropriate exception from the + * org.springframework.dao hierarchy. + * Return null if no translation is appropriate: any other exception may + * have resulted from user code, and should not be translated. + *

The most important cases like object not found or optimistic locking + * failure are covered here. For more fine-granular conversion, JpaAccessor and + * JpaTransactionManager support sophisticated translation of exceptions via a + * JpaDialect. + * @param ex runtime exception that occured + * @return the corresponding DataAccessException instance, + * or null if the exception should not be translated + */ + public static DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex) { + // Following the JPA specification, a persistence provider can also + // throw these two exceptions, besides PersistenceException. + if (ex instanceof IllegalStateException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof IllegalArgumentException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + + // Check for well-known PersistenceException subclasses. + if (ex instanceof EntityNotFoundException) { + return new JpaObjectRetrievalFailureException((EntityNotFoundException) ex); + } + if (ex instanceof NoResultException) { + return new EmptyResultDataAccessException(ex.getMessage(), 1); + } + if (ex instanceof NonUniqueResultException) { + return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1); + } + if (ex instanceof OptimisticLockException) { + return new JpaOptimisticLockingFailureException((OptimisticLockException) ex); + } + if (ex instanceof EntityExistsException) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + if (ex instanceof TransactionRequiredException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + + // If we have another kind of PersistenceException, throw it. + if (ex instanceof PersistenceException) { + return new JpaSystemException((PersistenceException) ex); + } + + // If we get here, we have an exception that resulted from user code, + // rather than the persistence provider, so we return null to indicate + // that translation should not occur. + return null; + } + + /** + * Close the given JPA EntityManager, + * catching and logging any cleanup exceptions thrown. + * @param em the JPA EntityManager to close (may be null) + * @see javax.persistence.EntityManager#close() + */ + public static void closeEntityManager(EntityManager em) { + if (em != null) { + logger.debug("Closing JPA EntityManager"); + try { + em.close(); + } + catch (PersistenceException ex) { + logger.debug("Could not close JPA EntityManager", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing JPA EntityManager", ex); + } + } + } + + + /** + * Callback for resource cleanup at the end of a non-JPA transaction + * (e.g. when participating in a JtaTransactionManager transaction). + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + private static class EntityManagerSynchronization extends ResourceHolderSynchronization implements Ordered { + + private final Object transactionData; + + private final boolean newEntityManager; + + public EntityManagerSynchronization( + EntityManagerHolder emHolder, EntityManagerFactory emf, Object transactionData, boolean newEntityManager) { + super(emHolder, emf); + this.transactionData = transactionData; + this.newEntityManager = newEntityManager; + } + + public int getOrder() { + return ENTITY_MANAGER_SYNCHRONIZATION_ORDER; + } + + protected boolean shouldUnbindAtCompletion() { + return this.newEntityManager; + } + + protected void releaseResource(ResourceHolder resourceHolder, Object resourceKey) { + closeEntityManager(((EntityManagerHolder) resourceHolder).getEntityManager()); + } + + protected void cleanupResource(ResourceHolder resourceHolder, Object resourceKey, boolean committed) { + if (!committed) { + // Clear all pending inserts/updates/deletes in the EntityManager. + // Necessary for pre-bound EntityManagers, to avoid inconsistent state. + ((EntityManagerHolder) resourceHolder).getEntityManager().clear(); + } + cleanupTransaction(this.transactionData, (EntityManagerFactory) resourceKey); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java new file mode 100644 index 00000000000..a17f686902e --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2007 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 javax.persistence.EntityManager; + +import org.springframework.transaction.SavepointManager; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Holder wrapping a JPA EntityManager. + * JpaTransactionManager binds instances of this class to the thread, + * for a given EntityManagerFactory. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 2.0 + * @see JpaTransactionManager + * @see EntityManagerFactoryUtils + */ +public class EntityManagerHolder extends ResourceHolderSupport { + + private final EntityManager entityManager; + + private boolean transactionActive; + + private SavepointManager savepointManager; + + + public EntityManagerHolder(EntityManager entityManager) { + Assert.notNull(entityManager, "EntityManager must not be null"); + this.entityManager = entityManager; + } + + + public EntityManager getEntityManager() { + return this.entityManager; + } + + protected void setTransactionActive(boolean transactionActive) { + this.transactionActive = transactionActive; + } + + protected boolean isTransactionActive() { + return this.transactionActive; + } + + protected void setSavepointManager(SavepointManager savepointManager) { + this.savepointManager = savepointManager; + } + + protected SavepointManager getSavepointManager() { + return this.savepointManager; + } + + public void clear() { + super.clear(); + this.transactionActive = false; + this.savepointManager = null; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerPlus.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerPlus.java new file mode 100644 index 00000000000..b1c44058e98 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerPlus.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2006 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 javax.persistence.EntityManager; + +/** + * Extension of the standard JPA EntityManager interface, linking in + * Spring's EntityManagerPlusOperations interface which defines additional + * operations (beyond JPA 1.0) in a vendor-independent fashion. + * + * @author Rod Johnson + * @since 2.0 + * @see javax.persistence.EntityManager + */ +public interface EntityManagerPlus extends EntityManager, EntityManagerPlusOperations { + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerPlusOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerPlusOperations.java new file mode 100644 index 00000000000..d2b835cc055 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerPlusOperations.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 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; + +/** + * Interface that defines common operations beyond the standard + * JPA EntityManager interface, in a vendor-independent fashion. + * To be adapted to specific JPA providers through a JpaDialect. + * + *

As of Spring 2.0, this interface does not define any operations yet. + * The pass-through mechanism to the underlying JpaDialect is already in + * place. Concrete operations will be added in the Spring 2.5 timeframe. + * + * @author Rod Johnson + * @since 2.0 + * @see JpaDialect#getEntityManagerPlusOperations + * @see javax.persistence.EntityManager + */ +public interface EntityManagerPlusOperations { + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerProxy.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerProxy.java new file mode 100644 index 00000000000..575fe432ad7 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/EntityManagerProxy.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2007 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 javax.persistence.EntityManager; + +/** + * Subinterface of {@link javax.persistence.EntityManager} to be implemented by + * EntityManager proxies. Allows access to the underlying target EntityManager. + * + *

This interface is mainly intended for framework usage. Application code + * should prefer the use of the {@link javax.persistence.EntityManager#getDelegate()} + * method to access native functionality of the underlying resource. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public interface EntityManagerProxy extends EntityManager { + + /** + * Return the underlying EntityManager that this proxy will delegate to. + *

In case of an extended EntityManager, this will be the associated + * raw EntityManager. + *

In case of a shared ("transactional") EntityManager, this will be + * the raw EntityManager that is currently associated with the transaction. + * Outside of a transaction, an IllegalStateException will be thrown. + * @return the underlying raw EntityManager (never null) + * @throws IllegalStateException if no underlying EntityManager is available + */ + EntityManager getTargetEntityManager() throws IllegalStateException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java new file mode 100644 index 00000000000..aeb3eb484af --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java @@ -0,0 +1,486 @@ +/* + * Copyright 2002-2008 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.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; +import javax.persistence.TransactionRequiredException; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.persistence.spi.PersistenceUnitTransactionType; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Factory for dynamic EntityManager proxies that follow the JPA spec's + * semantics for "extended" EntityManagers. + * + *

Supports explicit joining of a transaction through the + * joinTransaction() method ("application-managed extended + * EntityManager") as well as automatic joining on each operation + * ("container-managed extended EntityManager"). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class ExtendedEntityManagerCreator { + + /** + * Create an EntityManager that can join transactions with the + * joinTransaction() method, but is not automatically + * managed by the container. + * @param rawEntityManager raw EntityManager + * @param plusOperations an implementation of the EntityManagerPlusOperations + * interface, if those operations should be exposed (may be null) + * @return an application-managed EntityManager that can join transactions + * but does not participate in them automatically + */ + public static EntityManager createApplicationManagedEntityManager( + EntityManager rawEntityManager, EntityManagerPlusOperations plusOperations) { + + return createProxy(rawEntityManager, null, null, plusOperations, null, null, false); + } + + /** + * Create an EntityManager that can join transactions with the + * joinTransaction() method, but is not automatically + * managed by the container. + * @param rawEntityManager raw EntityManager + * @param plusOperations an implementation of the EntityManagerPlusOperations + * interface, if those operations should be exposed (may be null) + * @param exceptionTranslator the exception translator to use for translating + * JPA commit/rollback exceptions during transaction synchronization + * (may be null) + * @return an application-managed EntityManager that can join transactions + * but does not participate in them automatically + */ + public static EntityManager createApplicationManagedEntityManager( + EntityManager rawEntityManager, EntityManagerPlusOperations plusOperations, + PersistenceExceptionTranslator exceptionTranslator) { + + return createProxy(rawEntityManager, null, null, plusOperations, exceptionTranslator, null, false); + } + + /** + * Create an EntityManager that can join transactions with the + * joinTransaction() method, but is not automatically + * managed by the container. + * @param rawEntityManager raw EntityManager + * @param emfInfo the EntityManagerFactoryInfo to obtain the + * EntityManagerPlusOperations and PersistenceUnitInfo from + * @return an application-managed EntityManager that can join transactions + * but does not participate in them automatically + */ + public static EntityManager createApplicationManagedEntityManager( + EntityManager rawEntityManager, EntityManagerFactoryInfo emfInfo) { + + return createProxy(rawEntityManager, emfInfo, false); + } + + + /** + * Create an EntityManager that automatically joins transactions on each + * operation in a transaction. + * @param rawEntityManager raw EntityManager + * @param plusOperations an implementation of the EntityManagerPlusOperations + * interface, if those operations should be exposed (may be null) + * @return a container-managed EntityManager that will automatically participate + * in any managed transaction + */ + public static EntityManager createContainerManagedEntityManager( + EntityManager rawEntityManager, EntityManagerPlusOperations plusOperations) { + + return createProxy(rawEntityManager, null, null, plusOperations, null, null, true); + } + + /** + * Create an EntityManager that automatically joins transactions on each + * operation in a transaction. + * @param rawEntityManager raw EntityManager + * @param plusOperations an implementation of the EntityManagerPlusOperations + * interface, if those operations should be exposed (may be null) + * @param exceptionTranslator the exception translator to use for translating + * JPA commit/rollback exceptions during transaction synchronization + * (may be null) + * @return a container-managed EntityManager that will automatically participate + * in any managed transaction + */ + public static EntityManager createContainerManagedEntityManager( + EntityManager rawEntityManager, EntityManagerPlusOperations plusOperations, + PersistenceExceptionTranslator exceptionTranslator) { + + return createProxy(rawEntityManager, null, null, plusOperations, exceptionTranslator, null, true); + } + + /** + * Create an EntityManager that automatically joins transactions on each + * operation in a transaction. + * @param rawEntityManager raw EntityManager + * @param emfInfo the EntityManagerFactoryInfo to obtain the + * EntityManagerPlusOperations and PersistenceUnitInfo from + * @return a container-managed EntityManager that will automatically participate + * in any managed transaction + */ + public static EntityManager createContainerManagedEntityManager( + EntityManager rawEntityManager, EntityManagerFactoryInfo emfInfo) { + + return createProxy(rawEntityManager, emfInfo, true); + } + + + /** + * Create an EntityManager that automatically joins transactions on each + * operation in a transaction. + * @param emf the EntityManagerFactory to create the EntityManager with. + * If this implements the EntityManagerFactoryInfo interface, appropriate handling + * of the native EntityManagerFactory and available EntityManagerPlusOperations + * will automatically apply. + * @return a container-managed EntityManager that will automatically participate + * in any managed transaction + * @see javax.persistence.EntityManagerFactory#createEntityManager() + */ + public static EntityManager createContainerManagedEntityManager(EntityManagerFactory emf) { + return createContainerManagedEntityManager(emf, null); + } + + /** + * Create an EntityManager that automatically joins transactions on each + * operation in a transaction. + * @param emf the EntityManagerFactory to create the EntityManager with. + * If this implements the EntityManagerFactoryInfo interface, appropriate handling + * of the native EntityManagerFactory and available EntityManagerPlusOperations + * will automatically apply. + * @param properties the properties to be passed into the createEntityManager + * call (may be null) + * @return a container-managed EntityManager that will automatically participate + * in any managed transaction + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + public static EntityManager createContainerManagedEntityManager(EntityManagerFactory emf, Map properties) { + Assert.notNull(emf, "EntityManagerFactory must not be null"); + if (emf instanceof EntityManagerFactoryInfo) { + EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) emf; + EntityManagerFactory nativeEmf = emfInfo.getNativeEntityManagerFactory(); + EntityManager rawEntityManager = (!CollectionUtils.isEmpty(properties) ? + nativeEmf.createEntityManager(properties) : nativeEmf.createEntityManager()); + return createProxy(rawEntityManager, emfInfo, true); + } + else { + EntityManager rawEntityManager = (!CollectionUtils.isEmpty(properties) ? + emf.createEntityManager(properties) : emf.createEntityManager()); + return createProxy(rawEntityManager, null, null, null, null, null, true); + } + } + + + /** + * Actually create the EntityManager proxy. + * @param rawEntityManager raw EntityManager + * @param emfInfo the EntityManagerFactoryInfo to obtain the + * EntityManagerPlusOperations and PersistenceUnitInfo from + * @param containerManaged whether to follow container-managed EntityManager + * or application-managed EntityManager semantics + * @return the EntityManager proxy + */ + private static EntityManager createProxy( + EntityManager rawEntityManager, EntityManagerFactoryInfo emfInfo, boolean containerManaged) { + + Assert.notNull(emfInfo, "EntityManagerFactoryInfo must not be null"); + JpaDialect jpaDialect = emfInfo.getJpaDialect(); + EntityManagerPlusOperations plusOperations = null; + if (jpaDialect != null && jpaDialect.supportsEntityManagerPlusOperations()) { + plusOperations = jpaDialect.getEntityManagerPlusOperations(rawEntityManager); + } + PersistenceUnitInfo pui = emfInfo.getPersistenceUnitInfo(); + Boolean jta = (pui != null ? pui.getTransactionType() == PersistenceUnitTransactionType.JTA : null); + return createProxy(rawEntityManager, emfInfo.getEntityManagerInterface(), + emfInfo.getBeanClassLoader(), plusOperations, jpaDialect, jta, containerManaged); + } + + /** + * Actually create the EntityManager proxy. + * @param rawEm raw EntityManager + * @param emIfc the (potentially vendor-specific) EntityManager + * interface to proxy, or null for default detection of all interfaces + * @param plusOperations an implementation of the EntityManagerPlusOperations + * interface, if those operations should be exposed (may be null) + * @param exceptionTranslator the PersistenceException translator to use + * @param jta whether to create a JTA-aware EntityManager + * (or null if not known in advance) + * @param containerManaged whether to follow container-managed EntityManager + * or application-managed EntityManager semantics + * @return the EntityManager proxy + */ + private static EntityManager createProxy( + EntityManager rawEm, Class emIfc, ClassLoader cl, + EntityManagerPlusOperations plusOperations, PersistenceExceptionTranslator exceptionTranslator, + Boolean jta, boolean containerManaged) { + + Assert.notNull(rawEm, "EntityManager must not be null"); + Set ifcs = new LinkedHashSet(); + if (emIfc != null) { + ifcs.add(emIfc); + } + else { + ifcs.addAll(ClassUtils.getAllInterfacesForClassAsSet(rawEm.getClass(), cl)); + } + ifcs.add(EntityManagerProxy.class); + if (plusOperations != null) { + ifcs.add(EntityManagerPlusOperations.class); + } + return (EntityManager) Proxy.newProxyInstance( + (cl != null ? cl : ExtendedEntityManagerCreator.class.getClassLoader()), + ifcs.toArray(new Class[ifcs.size()]), + new ExtendedEntityManagerInvocationHandler( + rawEm, plusOperations, exceptionTranslator, jta, containerManaged)); + } + + + /** + * InvocationHandler for extended EntityManagers as defined in the JPA spec. + */ + private static class ExtendedEntityManagerInvocationHandler implements InvocationHandler, Serializable { + + private static final Log logger = LogFactory.getLog(ExtendedEntityManagerInvocationHandler.class); + + private final EntityManager target; + + private final EntityManagerPlusOperations plusOperations; + + private final PersistenceExceptionTranslator exceptionTranslator; + + private final boolean containerManaged; + + private boolean jta; + + private ExtendedEntityManagerInvocationHandler( + EntityManager target, EntityManagerPlusOperations plusOperations, + PersistenceExceptionTranslator exceptionTranslator, Boolean jta, boolean containerManaged) { + + this.target = target; + this.plusOperations = plusOperations; + this.exceptionTranslator = exceptionTranslator; + this.jta = (jta != null ? jta.booleanValue() : isJtaEntityManager()); + this.containerManaged = containerManaged; + } + + private boolean isJtaEntityManager() { + try { + this.target.getTransaction(); + return false; + } + catch (IllegalStateException ex) { + logger.debug("Cannot access EntityTransaction handle - assuming we're in a JTA environment"); + return true; + } + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on EntityManager interface 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 hashCode(); + } + else if (method.getName().equals("getTargetEntityManager")) { + return this.target; + } + else if (method.getName().equals("isOpen")) { + if (this.containerManaged) { + return true; + } + } + else if (method.getName().equals("close")) { + if (this.containerManaged) { + throw new IllegalStateException("Invalid usage: Cannot close a container-managed EntityManager"); + } + } + else if (method.getName().equals("getTransaction")) { + if (this.containerManaged) { + throw new IllegalStateException( + "Cannot execute getTransaction() on a container-managed EntityManager"); + } + } + else if (method.getName().equals("joinTransaction")) { + doJoinTransaction(true); + return null; + } + + // Do automatic joining if required. + if (this.containerManaged && method.getDeclaringClass().isInterface()) { + doJoinTransaction(false); + } + + // Invoke method on current EntityManager. + try { + if (method.getDeclaringClass().equals(EntityManagerPlusOperations.class)) { + return method.invoke(this.plusOperations, args); + } + else { + return method.invoke(this.target, args); + } + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + /** + * Join an existing transaction, if not already joined. + * @param enforce whether to enforce the transaction + * (i.e. whether failure to join is considered fatal) + */ + private void doJoinTransaction(boolean enforce) { + if (this.jta) { + // Let's try whether we're in a JTA transaction. + try { + this.target.joinTransaction(); + logger.debug("Joined JTA transaction"); + } + catch (TransactionRequiredException ex) { + if (!enforce) { + logger.debug("No JTA transaction to join: " + ex); + } + else { + throw ex; + } + } + } + else { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + if (!TransactionSynchronizationManager.hasResource(this.target) && + !this.target.getTransaction().isActive()) { + enlistInCurrentTransaction(); + } + logger.debug("Joined local transaction"); + } + else { + if (!enforce) { + logger.debug("No local transaction to join"); + } + else { + throw new TransactionRequiredException("No local transaction to join"); + } + } + } + } + + /** + * Enlist this application-managed EntityManager in the current transaction. + */ + private void enlistInCurrentTransaction() { + // Resource local transaction, need to acquire the EntityTransaction, + // start a transaction now and enlist a synchronization for + // commit or rollback later. + EntityTransaction et = this.target.getTransaction(); + et.begin(); + if (logger.isDebugEnabled()) { + logger.debug("Starting resource local transaction on application-managed " + + "EntityManager [" + this.target + "]"); + } + ExtendedEntityManagerSynchronization extendedEntityManagerSynchronization = + new ExtendedEntityManagerSynchronization(this.target, this.exceptionTranslator); + TransactionSynchronizationManager.bindResource(this.target, + extendedEntityManagerSynchronization); + TransactionSynchronizationManager.registerSynchronization(extendedEntityManagerSynchronization); + } + } + + + /** + * TransactionSynchronization enlisting an extended EntityManager + * with a current Spring transaction. + */ + private static class ExtendedEntityManagerSynchronization extends ResourceHolderSynchronization + implements Ordered { + + private final EntityManager entityManager; + + private final PersistenceExceptionTranslator exceptionTranslator; + + public ExtendedEntityManagerSynchronization( + EntityManager em, PersistenceExceptionTranslator exceptionTranslator) { + super(new EntityManagerHolder(em), em); + this.entityManager = em; + this.exceptionTranslator = exceptionTranslator; + } + + public int getOrder() { + return EntityManagerFactoryUtils.ENTITY_MANAGER_SYNCHRONIZATION_ORDER + 1; + } + + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + public void afterCommit() { + super.afterCommit(); + // Trigger commit here to let exceptions propagate to the caller. + try { + this.entityManager.getTransaction().commit(); + } + catch (RuntimeException ex) { + throw convertCompletionException(ex); + } + } + + public void afterCompletion(int status) { + super.afterCompletion(status); + if (status != STATUS_COMMITTED) { + // Haven't had an afterCommit call: trigger a rollback. + try { + this.entityManager.getTransaction().rollback(); + } + catch (RuntimeException ex) { + throw convertCompletionException(ex); + } + } + } + + private RuntimeException convertCompletionException(RuntimeException ex) { + DataAccessException daex = (this.exceptionTranslator != null) ? + this.exceptionTranslator.translateExceptionIfPossible(ex) : + EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex); + return (daex != null ? daex : ex); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaAccessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaAccessor.java new file mode 100644 index 00000000000..ee265ace0dd --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaAccessor.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2007 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 javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.support.DataAccessUtils; + +/** + * Base class for JpaTemplate and JpaInterceptor, defining common + * properties such as EntityManagerFactory and flushing behavior. + * + *

Not intended to be used directly. + * See {@link JpaTemplate} and {@link JpaInterceptor}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setEntityManagerFactory + * @see #setEntityManager + * @see #setJpaDialect + * @see #setFlushEager + * @see JpaTemplate + * @see JpaInterceptor + * @see JpaDialect + */ +public abstract class JpaAccessor extends EntityManagerFactoryAccessor implements InitializingBean { + + private EntityManager entityManager; + + private JpaDialect jpaDialect = new DefaultJpaDialect(); + + private boolean flushEager = false; + + + /** + * Set the JPA EntityManager to use. + */ + public void setEntityManager(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Return the JPA EntityManager to use. + */ + public EntityManager getEntityManager() { + return entityManager; + } + + /** + * Set the JPA dialect to use for this accessor. + *

The dialect object can be used to retrieve the underlying JDBC + * connection, for example. + */ + public void setJpaDialect(JpaDialect jpaDialect) { + this.jpaDialect = (jpaDialect != null ? jpaDialect : new DefaultJpaDialect()); + } + + /** + * Return the JPA dialect to use for this accessor. + *

Creates a default one for the specified EntityManagerFactory if none set. + */ + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + /** + * Set if this accessor should flush changes to the database eagerly. + *

Eager flushing leads to immediate synchronization with the database, + * even if in a transaction. This causes inconsistencies to show up and throw + * a respective exception immediately, and JDBC access code that participates + * in the same transaction will see the changes as the database is already + * aware of them then. But the drawbacks are: + *

+ */ + public void setFlushEager(boolean flushEager) { + this.flushEager = flushEager; + } + + /** + * Return if this accessor should flush changes to the database eagerly. + */ + public boolean isFlushEager() { + return this.flushEager; + } + + /** + * Eagerly initialize the JPA dialect, creating a default one + * for the specified EntityManagerFactory if none set. + */ + public void afterPropertiesSet() { + EntityManagerFactory emf = getEntityManagerFactory(); + if (emf == null && getEntityManager() == null) { + throw new IllegalArgumentException("entityManagerFactory or entityManager is required"); + } + if (emf instanceof EntityManagerFactoryInfo) { + JpaDialect jpaDialect = ((EntityManagerFactoryInfo) emf).getJpaDialect(); + if (jpaDialect != null) { + setJpaDialect(jpaDialect); + } + } + } + + + /** + * Flush the given JPA entity manager if necessary. + * @param em the current JPA PersistenceManage + * @param existingTransaction if executing within an existing transaction + * @throws javax.persistence.PersistenceException in case of JPA flushing errors + */ + protected void flushIfNecessary(EntityManager em, boolean existingTransaction) throws PersistenceException { + if (isFlushEager()) { + logger.debug("Eagerly flushing JPA entity manager"); + em.flush(); + } + } + + /** + * Convert the given runtime exception to an appropriate exception from the + * org.springframework.dao hierarchy if necessary, or + * return the exception itself if it is not persistence related + *

Default implementation delegates to the JpaDialect. + * May be overridden in subclasses. + * @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 + * @see org.springframework.dao.support.DataAccessUtils#translateIfNecessary + */ + public RuntimeException translateIfNecessary(RuntimeException ex) { + return DataAccessUtils.translateIfNecessary(ex, getJpaDialect()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaCallback.java new file mode 100644 index 00000000000..42fef7fad5b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaCallback.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 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 javax.persistence.EntityManager; +import javax.persistence.PersistenceException; + +/** + * Callback interface for JPA code. To be used with {@link JpaTemplate}'s + * execution method, often as anonymous classes within a method implementation. + * A typical implementation will call EntityManager.find/merge + * to perform some operations on persistent objects. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.orm.jpa.JpaTemplate + * @see org.springframework.orm.jpa.JpaTransactionManager + */ +public interface JpaCallback { + + /** + * Gets called by JpaTemplate.execute with an active + * JPA EntityManager. Does not need to care about activating + * or closing the EntityManager, or handling transactions. + * + *

Note that JPA callback code will not flush any modifications to the + * database if not executed within a transaction. Thus, you need to make + * sure that JpaTransactionManager has initiated a JPA transaction when + * the callback gets called, at least if you want to write to the database. + * + *

Allows for returning a result object created within the callback, + * i.e. a domain object or a collection of domain objects. + * A thrown custom RuntimeException is treated as an application exception: + * It gets propagated to the caller of the template. + * + * @param em active EntityManager + * @return a result object, or null if none + * @throws PersistenceException if thrown by the JPA API + * @see org.springframework.orm.jpa.JpaTemplate#execute + * @see org.springframework.orm.jpa.JpaTemplate#executeFind + */ + Object doInJpa(EntityManager em) throws PersistenceException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java new file mode 100644 index 00000000000..b3802f05591 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2007 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.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; + +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * SPI strategy that encapsulates certain functionality that standard JPA 1.0 + * does not offer, such as access to the underlying JDBC Connection. This + * strategy is mainly intended for standalone usage of a JPA provider; most + * of its functionality is not relevant when running with JTA transactions. + * + *

Also allows for the provision of value-added methods for portable yet + * more capable EntityManager and EntityManagerFactory subinterfaces offered + * by Spring. + * + *

In general, it is recommended to derive from DefaultJpaDialect instead of + * implementing this interface directly. This allows for inheriting common + * behavior (present and future) from DefaultJpaDialect, only overriding + * specific hooks to plug in concrete vendor-specific behavior. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see DefaultJpaDialect + * @see JpaAccessor#setJpaDialect + * @see JpaTransactionManager#setJpaDialect + * @see JpaVendorAdapter#getJpaDialect() + * @see AbstractEntityManagerFactoryBean#setJpaDialect + * @see AbstractEntityManagerFactoryBean#setJpaVendorAdapter + */ +public interface JpaDialect extends PersistenceExceptionTranslator { + + //----------------------------------------------------------------------------------- + // Hooks for non-standard persistence operations (used by EntityManagerFactory beans) + //----------------------------------------------------------------------------------- + + /** + * Return whether the EntityManagerFactoryPlus(Operations) interface is + * supported by this provider. + * @see EntityManagerFactoryPlusOperations + * @see EntityManagerFactoryPlus + */ + boolean supportsEntityManagerFactoryPlusOperations(); + + /** + * Return whether the EntityManagerPlus(Operations) interface is + * supported by this provider. + * @see EntityManagerPlusOperations + * @see EntityManagerPlus + */ + boolean supportsEntityManagerPlusOperations(); + + /** + * Return an EntityManagerFactoryPlusOperations implementation for + * the given raw EntityManagerFactory. This operations object can be + * used to serve the additional operations behind a proxy that + * implements the EntityManagerFactoryPlus interface. + * @param rawEntityManager the raw provider-specific EntityManagerFactory + * @return the EntityManagerFactoryPlusOperations implementation + */ + EntityManagerFactoryPlusOperations getEntityManagerFactoryPlusOperations(EntityManagerFactory rawEntityManager); + + /** + * Return an EntityManagerPlusOperations implementation for + * the given raw EntityManager. This operations object can be + * used to serve the additional operations behind a proxy that + * implements the EntityManagerPlus interface. + * @param rawEntityManager the raw provider-specific EntityManagerFactory + * @return the EntityManagerFactoryPlusOperations implementation + */ + EntityManagerPlusOperations getEntityManagerPlusOperations(EntityManager rawEntityManager); + + + //------------------------------------------------------------------------- + // Hooks for transaction management (used by JpaTransactionManager) + //------------------------------------------------------------------------- + + /** + * Begin the given JPA transaction, applying the semantics specified by the + * given Spring transaction definition (in particular, an isolation level + * and a timeout). Called by JpaTransactionManager on transaction begin. + *

An implementation can configure the JPA Transaction object and then + * invoke begin, or invoke a special begin method that takes, + * for example, an isolation level. + *

An implementation can apply the read-only flag as flush mode. In that case, + * a transaction data object can be returned that holds the previous flush mode + * (and possibly other data), to be reset in cleanupTransaction. + * It may also apply the read-only flag and isolation level to the underlying + * JDBC Connection before beginning the transaction. + *

Implementations can also use the Spring transaction name, as exposed by the + * passed-in TransactionDefinition, to optimize for specific data access use cases + * (effectively using the current transaction name as use case identifier). + *

This method also allows for exposing savepoint capabilities if supported by + * the persistence provider, through returning an Object that implements Spring's + * {@link org.springframework.transaction.SavepointManager} interface. + * {@link JpaTransactionManager} will use this capability if needed. + * @param entityManager the EntityManager to begin a JPA transaction on + * @param definition the Spring transaction definition that defines semantics + * @return an arbitrary object that holds transaction data, if any + * (to be passed into {@link #cleanupTransaction}). May implement the + * {@link org.springframework.transaction.SavepointManager} interface. + * @throws javax.persistence.PersistenceException if thrown by JPA methods + * @throws java.sql.SQLException if thrown by JDBC methods + * @throws org.springframework.transaction.TransactionException in case of invalid arguments + * @see #cleanupTransaction + * @see javax.persistence.EntityTransaction#begin + * @see org.springframework.jdbc.datasource.DataSourceUtils#prepareConnectionForTransaction + */ + Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + throws PersistenceException, SQLException, TransactionException; + + /** + * Prepare a JPA transaction, applying the specified semantics. Called by + * EntityManagerFactoryUtils when enlisting an EntityManager in a JTA transaction. + *

An implementation can apply the read-only flag as flush mode. In that case, + * a transaction data object can be returned that holds the previous flush mode + * (and possibly other data), to be reset in cleanupTransaction. + *

Implementations can also use the Spring transaction name, as exposed by the + * passed-in TransactionDefinition, to optimize for specific data access use cases + * (effectively using the current transaction name as use case identifier). + * @param entityManager the EntityManager to begin a JPA transaction on + * @param readOnly whether the transaction is supposed to be read-only + * @param name the name of the transaction (if any) + * @return an arbitrary object that holds transaction data, if any + * (to be passed into cleanupTransaction) + * @throws javax.persistence.PersistenceException if thrown by JPA methods + * @see #cleanupTransaction + */ + Object prepareTransaction(EntityManager entityManager, boolean readOnly, String name) + throws PersistenceException; + + /** + * Clean up the transaction via the given transaction data. Called by + * JpaTransactionManager and EntityManagerFactoryUtils on transaction cleanup. + *

An implementation can, for example, reset read-only flag and + * isolation level of the underlying JDBC Connection. Furthermore, + * an exposed data access use case can be reset here. + * @param transactionData arbitrary object that holds transaction data, if any + * (as returned by beginTransaction or prepareTransaction) + * @see #beginTransaction + * @see org.springframework.jdbc.datasource.DataSourceUtils#resetConnectionAfterTransaction + */ + void cleanupTransaction(Object transactionData); + + /** + * Retrieve the JDBC Connection that the given JPA EntityManager uses underneath, + * if accessing a relational database. This method will just get invoked if actually + * needing access to the underlying JDBC Connection, usually within an active JPA + * transaction (for example, by JpaTransactionManager). The returned handle will + * be passed into the releaseJdbcConnection method when not needed anymore. + *

This strategy is necessary as JPA 1.0 does not provide a standard way to retrieve + * the underlying JDBC Connection (due to the fact that a JPA implementation might not + * work with a relational database at all). + *

Implementations are encouraged to return an unwrapped Connection object, i.e. + * the Connection as they got it from the connection pool. This makes it easier for + * application code to get at the underlying native JDBC Connection, like an + * OracleConnection, which is sometimes necessary for LOB handling etc. We assume + * that calling code knows how to properly handle the returned Connection object. + *

In a simple case where the returned Connection will be auto-closed with the + * EntityManager or can be released via the Connection object itself, an + * implementation can return a SimpleConnectionHandle that just contains the + * Connection. If some other object is needed in releaseJdbcConnection, + * an implementation should use a special handle that references that other object. + * @param entityManager the current JPA EntityManager + * @param readOnly whether the Connection is only needed for read-only purposes + * @return a handle for the JDBC Connection, to be passed into + * releaseJdbcConnection, or null + * if no JDBC Connection can be retrieved + * @throws javax.persistence.PersistenceException if thrown by JPA methods + * @throws java.sql.SQLException if thrown by JDBC methods + * @see #releaseJdbcConnection + * @see org.springframework.jdbc.datasource.ConnectionHandle#getConnection + * @see org.springframework.jdbc.datasource.SimpleConnectionHandle + * @see JpaTransactionManager#setDataSource + * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor + */ + ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) + throws PersistenceException, SQLException; + + /** + * Release the given JDBC Connection, which has originally been retrieved + * via getJdbcConnection. This should be invoked in any case, + * to allow for proper release of the retrieved Connection handle. + *

An implementation might simply do nothing, if the Connection returned + * by getJdbcConnection will be implicitly closed when the JPA + * transaction completes or when the EntityManager is closed. + * @param conHandle the JDBC Connection handle to release + * @param entityManager the current JPA EntityManager + * @throws javax.persistence.PersistenceException if thrown by JPA methods + * @throws java.sql.SQLException if thrown by JDBC methods + * @see #getJdbcConnection + */ + void releaseJdbcConnection(ConnectionHandle conHandle, EntityManager entityManager) + throws PersistenceException, SQLException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaInterceptor.java new file mode 100644 index 00000000000..ea68dcfc8f9 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaInterceptor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2008 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 javax.persistence.EntityManager; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * This interceptor binds a new JPA EntityManager to the thread before a method + * call, closing and removing it afterwards in case of any method outcome. + * If there already is a pre-bound EntityManager (e.g. from JpaTransactionManager, + * or from a surrounding JPA-intercepted method), the interceptor simply participates in it. + * + *

Application code must retrieve a JPA EntityManager via the + * EntityManagerFactoryUtils.getEntityManager method or - preferably - + * via a shared EntityManager reference, to be able to detect a + * thread-bound EntityManager. Typically, the code will look like as follows: + * + *

+ * public void doSomeDataAccessAction() {
+ *   this.entityManager...
+ * }
+ * + *

Note that this interceptor automatically translates PersistenceExceptions, + * via delegating to the EntityManagerFactoryUtils.convertJpaAccessException + * method that converts them to exceptions that are compatible with the + * org.springframework.dao exception hierarchy (like JpaTemplate does). + * + *

This class can be considered a declarative alternative to JpaTemplate's + * callback approach. The advantages are: + *

+ * + *

The drawback is the dependency on interceptor configuration. However, note + * that this interceptor is usually not necessary in scenarios where the + * data access code always executes within transactions. A transaction will always + * have a thread-bound EntityManager in the first place, so adding this interceptor + * to the configuration just adds value when fine-tuning EntityManager settings + * like the flush mode - or when relying on exception translation. + * + * @author Juergen Hoeller + * @since 2.0 + * @see JpaTransactionManager + * @see JpaTemplate + */ +public class JpaInterceptor extends JpaAccessor implements MethodInterceptor { + + private boolean exceptionConversionEnabled = true; + + + /** + * Set whether to convert any PersistenceException raised to a Spring DataAccessException, + * compatible with the org.springframework.dao exception hierarchy. + *

Default is "true". Turn this flag off to let the caller receive raw exceptions + * as-is, without any wrapping. + * @see org.springframework.dao.DataAccessException + */ + public void setExceptionConversionEnabled(boolean exceptionConversionEnabled) { + this.exceptionConversionEnabled = exceptionConversionEnabled; + } + + + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + // Determine current EntityManager: either the transactional one + // managed by the factory or a temporary one for the given invocation. + EntityManager em = getTransactionalEntityManager(); + boolean isNewEm = false; + if (em == null) { + logger.debug("Creating new EntityManager for JpaInterceptor invocation"); + em = createEntityManager(); + isNewEm = true; + TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), new EntityManagerHolder(em)); + } + + try { + Object retVal = methodInvocation.proceed(); + flushIfNecessary(em, !isNewEm); + return retVal; + } + catch (RuntimeException rawException) { + if (this.exceptionConversionEnabled) { + // Translation enabled. Translate if we understand the exception. + throw translateIfNecessary(rawException); + } + else { + // Translation not enabled. Don't try to translate. + throw rawException; + } + } + finally { + if (isNewEm) { + TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); + EntityManagerFactoryUtils.closeEntityManager(em); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaObjectRetrievalFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaObjectRetrievalFailureException.java new file mode 100644 index 00000000000..ef5a04f151e --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaObjectRetrievalFailureException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 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 javax.persistence.EntityNotFoundException; + +import org.springframework.orm.ObjectRetrievalFailureException; + +/** + * JPA-specific subclass of ObjectRetrievalFailureException. + * Converts JPA's EntityNotFoundException. + * + * @author Juergen Hoeller + * @since 2.0 + * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible + */ +public class JpaObjectRetrievalFailureException extends ObjectRetrievalFailureException { + + public JpaObjectRetrievalFailureException(EntityNotFoundException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaOperations.java new file mode 100644 index 00000000000..3e7b8ae138f --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaOperations.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2006 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.util.List; +import java.util.Map; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that specifies a basic set of JPA operations, + * implemented by {@link JpaTemplate}. Not often used, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Defines JpaTemplate's data access methods that mirror + * various {@link javax.persistence.EntityManager} methods. Users are + * strongly encouraged to read the JPA EntityManager + * javadocs for details on the semantics of those methods. + * + *

Note that lazy loading will just work with an open JPA + * EntityManager, either within a managed transaction or within + * {@link org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter}/ + * {@link org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor}. + * Furthermore, some operations just make sense within transactions, + * for example: flush, clear. + * + * @author Juergen Hoeller + * @since 2.0 + * @see JpaTemplate + * @see javax.persistence.EntityManager + * @see JpaTransactionManager + * @see JpaDialect + * @see org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter + * @see org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor + */ +public interface JpaOperations { + + Object execute(JpaCallback action) throws DataAccessException; + + List executeFind(JpaCallback action) throws DataAccessException; + + T find(Class entityClass, Object id) throws DataAccessException; + + T getReference(Class entityClass, Object id) throws DataAccessException; + + boolean contains(Object entity) throws DataAccessException; + + void refresh(Object entity) throws DataAccessException; + + void persist(Object entity) throws DataAccessException; + + T merge(T entity) throws DataAccessException; + + void remove(Object entity) throws DataAccessException; + + void flush() throws DataAccessException; + + List find(String queryString) throws DataAccessException; + + List find(String queryString, Object... values) throws DataAccessException; + + List findByNamedParams(String queryString, Map params) throws DataAccessException; + + List findByNamedQuery(String queryName) throws DataAccessException; + + List findByNamedQuery(String queryName, Object... values) throws DataAccessException; + + List findByNamedQueryAndNamedParams(String queryName, Map params) throws DataAccessException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaOptimisticLockingFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaOptimisticLockingFailureException.java new file mode 100644 index 00000000000..eb77c4154b6 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaOptimisticLockingFailureException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 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 javax.persistence.OptimisticLockException; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +/** + * JPA-specific subclass of ObjectOptimisticLockingFailureException. + * Converts JPA's OptimisticLockException. + * + * @author Juergen Hoeller + * @since 2.0 + * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible + */ +public class JpaOptimisticLockingFailureException extends ObjectOptimisticLockingFailureException { + + public JpaOptimisticLockingFailureException(OptimisticLockException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaSystemException.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaSystemException.java new file mode 100644 index 00000000000..710ab144c83 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaSystemException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2006 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 javax.persistence.PersistenceException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * JPA-specific subclass of UncategorizedDataAccessException, + * for JPA system errors that do not match any concrete + * org.springframework.dao exceptions. + * + * @author Juergen Hoeller + * @since 2.0 + * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible + */ +public class JpaSystemException extends UncategorizedDataAccessException { + + public JpaSystemException(PersistenceException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaTemplate.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaTemplate.java new file mode 100644 index 00000000000..acd36a44848 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaTemplate.java @@ -0,0 +1,412 @@ +/* + * Copyright 2002-2008 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 org.springframework.dao exception hierarchy. + * + *

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. + * + *

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. + * + *

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. + * (Using Spring's SharedEntityManagerBean / PersistenceAnnotationBeanPostProcessor, + * or using a direct JNDI lookup for an EntityManager on a Java EE 5 server.) + * + *

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}. + * + *

{@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 + */ +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 close calls and automatically applying transaction + * timeouts (if any). + *

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 Object execute(JpaCallback 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 null + * @throws org.springframework.dao.DataAccessException in case of JPA errors + */ + public Object execute(JpaCallback 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)); + Object 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 + //------------------------------------------------------------------------- + + @SuppressWarnings("unchecked") + public T find(final Class entityClass, final Object id) throws DataAccessException { + return (T) execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + return em.find(entityClass, id); + } + }, true); + } + + @SuppressWarnings("unchecked") + public T getReference(final Class entityClass, final Object id) throws DataAccessException { + return (T) execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + return em.getReference(entityClass, id); + } + }, true); + } + + public boolean contains(final Object entity) throws DataAccessException { + Boolean result = (Boolean) execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + return new Boolean(em.contains(entity)); + } + }, true); + return result.booleanValue(); + } + + public void refresh(final Object entity) throws DataAccessException { + execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + em.refresh(entity); + return null; + } + }, true); + } + + public void persist(final Object entity) throws DataAccessException { + execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + em.persist(entity); + return null; + } + }, true); + } + + @SuppressWarnings("unchecked") + public T merge(final T entity) throws DataAccessException { + return (T) execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + return em.merge(entity); + } + }, true); + } + + public void remove(final Object entity) throws DataAccessException { + execute(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + em.remove(entity); + return null; + } + }, true); + } + + public void flush() throws DataAccessException { + execute(new JpaCallback() { + 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 executeFind(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + Query queryObject = em.createQuery(queryString); + 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 params) throws DataAccessException { + return executeFind(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + Query queryObject = em.createQuery(queryString); + if (params != null) { + for (Map.Entry 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 executeFind(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + Query queryObject = em.createNamedQuery(queryName); + 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 params) + throws DataAccessException { + + return executeFind(new JpaCallback() { + public Object doInJpa(EntityManager em) throws PersistenceException { + Query queryObject = em.createNamedQuery(queryName); + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + queryObject.setParameter(entry.getKey(), entry.getValue()); + } + } + return queryObject.getResultList(); + } + }); + } + + + /** + * Invocation handler that suppresses close calls on JPA EntityManagers. + * Also prepares returned Query and Criteria 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] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of EntityManager proxy. + return new Integer(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(); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java new file mode 100644 index 00000000000..f69d9ffe242 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java @@ -0,0 +1,652 @@ +/* + * Copyright 2002-2008 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.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; +import javax.persistence.PersistenceException; +import javax.persistence.RollbackException; +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.NestedTransactionNotSupportedException; +import org.springframework.transaction.SavepointManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.CollectionUtils; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} implementation + * for a single JPA {@link javax.persistence.EntityManagerFactory}. Binds a JPA + * EntityManager from the specified factory to the thread, potentially allowing for + * one thread-bound EntityManager per factory. {@link SharedEntityManagerCreator} + * and {@link JpaTemplate} are aware of thread-bound entity managers and participate + * in such transactions automatically. Using either is required for JPA access code + * supporting this transaction management mechanism. + * + *

This transaction manager is appropriate for applications that use a single + * JPA EntityManagerFactory for transactional data access. JTA (usually through + * {@link org.springframework.transaction.jta.JtaTransactionManager}) is necessary + * for accessing multiple transactional resources within the same transaction. + * Note that you need to configure your JPA provider accordingly in order to make + * it participate in JTA transactions. + * + *

This transaction manager also supports direct DataSource access within a + * transaction (i.e. plain JDBC code working with the same DataSource). + * This allows for mixing services which access JPA and services which use plain + * JDBC (without being aware of JPA)! Application code needs to stick to the + * same simple Connection lookup pattern as with + * {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} + * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection} + * or going through a + * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}). + * Note that this requires a vendor-specific {@link JpaDialect} to be configured. + * + *

Note: To be able to register a DataSource's Connection for plain JDBC code, + * this instance needs to be aware of the DataSource ({@link #setDataSource}). + * The given DataSource should obviously match the one used by the given + * EntityManagerFactory. This transaction manager will autodetect the DataSource + * used as known connection factory of the EntityManagerFactory, so you usually + * don't need to explicitly specify the "dataSource" property. + * + *

On JDBC 3.0, this transaction manager supports nested transactions via JDBC 3.0 + * Savepoints. The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} + * flag defaults to "false", though, as nested transactions will just apply to the + * JDBC Connection, not to the JPA EntityManager and its cached objects. + * You can manually set the flag to "true" if you want to use nested transactions + * for JDBC access code which participates in JPA transactions (provided that your + * JDBC driver supports Savepoints). Note that JPA itself does not support + * nested transactions! Hence, do not expect JPA access code to semantically + * participate in a nested transaction. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setEntityManagerFactory + * @see #setDataSource + * @see LocalEntityManagerFactoryBean + * @see JpaTemplate#execute + * @see org.springframework.orm.jpa.support.SharedEntityManagerBean + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class JpaTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private EntityManagerFactory entityManagerFactory; + + private final Map jpaPropertyMap = new HashMap(); + + private DataSource dataSource; + + private JpaDialect jpaDialect = new DefaultJpaDialect(); + + + /** + * Create a new JpaTransactionManager instance. + * A EntityManagerFactory has to be set to be able to use it. + * @see #setEntityManagerFactory + */ + public JpaTransactionManager() { + setNestedTransactionAllowed(true); + } + + /** + * Create a new JpaTransactionManager instance. + * @param emf EntityManagerFactory to manage transactions for + */ + public JpaTransactionManager(EntityManagerFactory emf) { + this(); + this.entityManagerFactory = emf; + afterPropertiesSet(); + } + + + /** + * Set the EntityManagerFactory that this instance should manage transactions for. + */ + public void setEntityManagerFactory(EntityManagerFactory emf) { + this.entityManagerFactory = emf; + } + + /** + * Return the EntityManagerFactory that this instance should manage transactions for. + */ + public EntityManagerFactory getEntityManagerFactory() { + return this.entityManagerFactory; + } + + /** + * Specify JPA properties, to be passed into + * EntityManagerFactory.createEntityManager(Map) (if any). + *

Can be populated with a String "value" (parsed via PropertiesEditor) + * or a "props" element in XML bean definitions. + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + public void setJpaProperties(Properties jpaProperties) { + CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.jpaPropertyMap); + } + + /** + * Specify JPA properties as a Map, to be passed into + * EntityManagerFactory.createEntityManager(Map) (if any). + *

Can be populated with a "map" or "props" element in XML bean definitions. + * @see javax.persistence.EntityManagerFactory#createEntityManager(java.util.Map) + */ + public void setJpaPropertyMap(Map jpaProperties) { + if (jpaProperties != null) { + this.jpaPropertyMap.putAll(jpaProperties); + } + } + + /** + * Allow Map access to the JPA properties to be passed to the persistence + * provider, with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via "jpaPropertyMap[myKey]". + */ + public Map getJpaPropertyMap() { + return this.jpaPropertyMap; + } + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + * The DataSource should match the one used by the JPA EntityManagerFactory: + * for example, you could specify the same JNDI DataSource for both. + *

If the EntityManagerFactory uses a known DataSource as connection factory, + * the DataSource will be autodetected: You can still explictly specify the + * DataSource, but you don't need to in this case. + *

A transactional JDBC Connection for this DataSource will be provided to + * application code accessing this DataSource directly via DataSourceUtils + * or JdbcTemplate. The Connection will be taken from the JPA EntityManager. + *

Note that you need to use a JPA dialect for a specific JPA implementation + * to allow for exposing JPA transactions as JDBC transactions. + *

The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + * @see EntityManagerFactoryInfo#getDataSource() + * @see #setJpaDialect + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.DataSourceUtils + * @see org.springframework.jdbc.core.JdbcTemplate + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set the JPA dialect to use for this transaction manager. + * Used for vendor-specific transaction management and JDBC connection exposure. + *

If the EntityManagerFactory uses a known JpaDialect, it will be autodetected: + * You can still explictly specify the DataSource, but you don't need to in this case. + *

The dialect object can be used to retrieve the underlying JDBC connection + * and thus allows for exposing JPA transactions as JDBC transactions. + * @see EntityManagerFactoryInfo#getJpaDialect() + * @see JpaDialect#beginTransaction + * @see JpaDialect#getJdbcConnection + */ + public void setJpaDialect(JpaDialect jpaDialect) { + this.jpaDialect = (jpaDialect != null ? jpaDialect : new DefaultJpaDialect()); + } + + /** + * Return the JPA dialect to use for this transaction manager. + */ + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + /** + * Eagerly initialize the JPA dialect, creating a default one + * for the specified EntityManagerFactory if none set. + * Auto-detect the EntityManagerFactory's DataSource, if any. + */ + public void afterPropertiesSet() { + if (getEntityManagerFactory() == null) { + throw new IllegalArgumentException("Property 'entityManagerFactory' is required"); + } + if (getEntityManagerFactory() instanceof EntityManagerFactoryInfo) { + EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) getEntityManagerFactory(); + DataSource dataSource = emfInfo.getDataSource(); + if (dataSource != null) { + setDataSource(dataSource); + } + JpaDialect jpaDialect = emfInfo.getJpaDialect(); + if (jpaDialect != null) { + setJpaDialect(jpaDialect); + } + } + } + + + public Object getResourceFactory() { + return getEntityManagerFactory(); + } + + protected Object doGetTransaction() { + JpaTransactionObject txObject = new JpaTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + + EntityManagerHolder emHolder = (EntityManagerHolder) + TransactionSynchronizationManager.getResource(getEntityManagerFactory()); + if (emHolder != null) { + if (logger.isDebugEnabled()) { + logger.debug("Found thread-bound EntityManager [" + + emHolder.getEntityManager() + "] for JPA transaction"); + } + txObject.setEntityManagerHolder(emHolder, false); + } + + if (getDataSource() != null) { + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.getResource(getDataSource()); + txObject.setConnectionHolder(conHolder); + } + + return txObject; + } + + protected boolean isExistingTransaction(Object transaction) { + return ((JpaTransactionObject) transaction).hasTransaction(); + } + + protected void doBegin(Object transaction, TransactionDefinition definition) { + JpaTransactionObject txObject = (JpaTransactionObject) transaction; + + if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + throw new IllegalTransactionStateException( + "Pre-bound JDBC Connection found! JpaTransactionManager does not support " + + "running within DataSourceTransactionManager if told to manage the DataSource itself. " + + "It is recommended to use a single JpaTransactionManager for all transactions " + + "on a single DataSource, no matter whether JPA or JDBC access."); + } + + EntityManager em = null; + + try { + if (txObject.getEntityManagerHolder() == null || + txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) { + EntityManager newEm = createEntityManagerForTransaction(); + if (logger.isDebugEnabled()) { + logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction"); + } + txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true); + } + + em = txObject.getEntityManagerHolder().getEntityManager(); + + // Delegate to JpaDialect for actual transaction begin. + Object transactionData = getJpaDialect().beginTransaction(em, definition); + txObject.setTransactionData(transactionData); + + // Register transaction timeout. + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getEntityManagerHolder().setTimeoutInSeconds(timeout); + } + + // Register the JPA EntityManager's JDBC Connection for the DataSource, if set. + if (getDataSource() != null) { + ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly()); + if (conHandle != null) { + ConnectionHolder conHolder = new ConnectionHolder(conHandle); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + conHolder.setTimeoutInSeconds(timeout); + } + if (logger.isDebugEnabled()) { + logger.debug("Exposing JPA transaction as JDBC transaction [" + conHolder.getConnectionHandle() + "]"); + } + TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); + txObject.setConnectionHolder(conHolder); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because JpaDialect [" + + getJpaDialect() + "] does not support JDBC Connection retrieval"); + } + } + } + + // Bind the entity manager holder to the thread. + if (txObject.isNewEntityManagerHolder()) { + TransactionSynchronizationManager.bindResource( + getEntityManagerFactory(), txObject.getEntityManagerHolder()); + } + txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true); + } + + catch (TransactionException ex) { + closeEntityManagerAfterFailedBegin(txObject); + throw ex; + } + catch (Exception ex) { + closeEntityManagerAfterFailedBegin(txObject); + throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex); + } + } + + /** + * Create a JPA EntityManager to be used for a transaction. + *

The default implementation checks whether the EntityManagerFactory + * is a Spring proxy and unwraps it first. + * @see javax.persistence.EntityManagerFactory#createEntityManager() + * @see EntityManagerFactoryInfo#getNativeEntityManagerFactory() + */ + protected EntityManager createEntityManagerForTransaction() { + EntityManagerFactory emf = getEntityManagerFactory(); + if (emf instanceof EntityManagerFactoryInfo) { + emf = ((EntityManagerFactoryInfo) emf).getNativeEntityManagerFactory(); + } + Map properties = getJpaPropertyMap(); + return (!CollectionUtils.isEmpty(properties) ? + emf.createEntityManager(properties) : emf.createEntityManager()); + } + + /** + * Close the current transaction's EntityManager. + * Called after a transaction begin attempt failed. + * @param txObject the current transaction + */ + protected void closeEntityManagerAfterFailedBegin(JpaTransactionObject txObject) { + if (txObject.isNewEntityManagerHolder()) { + EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); + try { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + } + catch (Throwable ex) { + logger.debug("Could not rollback EntityManager after failed transaction begin", ex); + } + finally { + EntityManagerFactoryUtils.closeEntityManager(em); + } + } + } + + protected Object doSuspend(Object transaction) { + JpaTransactionObject txObject = (JpaTransactionObject) transaction; + txObject.setEntityManagerHolder(null, false); + EntityManagerHolder entityManagerHolder = (EntityManagerHolder) + TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); + txObject.setConnectionHolder(null); + ConnectionHolder connectionHolder = null; + if (getDataSource() != null && TransactionSynchronizationManager.hasResource(getDataSource())) { + connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(getDataSource()); + } + return new SuspendedResourcesHolder(entityManagerHolder, connectionHolder); + } + + protected void doResume(Object transaction, Object suspendedResources) { + SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources; + TransactionSynchronizationManager.bindResource( + getEntityManagerFactory(), resourcesHolder.getEntityManagerHolder()); + if (getDataSource() != null && resourcesHolder.getConnectionHolder() != null) { + TransactionSynchronizationManager.bindResource(getDataSource(), resourcesHolder.getConnectionHolder()); + } + } + + /** + * This implementation returns "true": a JPA commit will properly handle + * transactions that have been marked rollback-only at a global level. + */ + protected boolean shouldCommitOnGlobalRollbackOnly() { + return true; + } + + protected void doCommit(DefaultTransactionStatus status) { + JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Committing JPA transaction on EntityManager [" + + txObject.getEntityManagerHolder().getEntityManager() + "]"); + } + try { + EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction(); + tx.commit(); + } + catch (RollbackException ex) { + if (ex.getCause() instanceof RuntimeException) { + DataAccessException dex = getJpaDialect().translateExceptionIfPossible((RuntimeException) ex.getCause()); + if (dex != null) { + throw dex; + } + } + throw new TransactionSystemException("Could not commit JPA transaction", ex); + } + catch (RuntimeException ex) { + // Assumably failed to flush changes to database. + throw DataAccessUtils.translateIfNecessary(ex, getJpaDialect()); + } + } + + protected void doRollback(DefaultTransactionStatus status) { + JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Rolling back JPA transaction on EntityManager [" + + txObject.getEntityManagerHolder().getEntityManager() + "]"); + } + try { + EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction(); + if (tx.isActive()) { + tx.rollback(); + } + } + catch (PersistenceException ex) { + throw new TransactionSystemException("Could not roll back JPA transaction", ex); + } + finally { + if (!txObject.isNewEntityManagerHolder()) { + // Clear all pending inserts/updates/deletes in the EntityManager. + // Necessary for pre-bound EntityManagers, to avoid inconsistent state. + txObject.getEntityManagerHolder().getEntityManager().clear(); + } + } + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JPA transaction on EntityManager [" + + txObject.getEntityManagerHolder().getEntityManager() + "] rollback-only"); + } + txObject.setRollbackOnly(); + } + + protected void doCleanupAfterCompletion(Object transaction) { + JpaTransactionObject txObject = (JpaTransactionObject) transaction; + + // Remove the entity manager holder from the thread. + if (txObject.isNewEntityManagerHolder()) { + TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); + } + txObject.getEntityManagerHolder().clear(); + + // Remove the JDBC connection holder from the thread, if exposed. + if (txObject.hasConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(getDataSource()); + try { + getJpaDialect().releaseJdbcConnection(txObject.getConnectionHolder().getConnectionHandle(), + txObject.getEntityManagerHolder().getEntityManager()); + } + catch (Exception ex) { + // Just log it, to keep a transaction-related exception. + logger.error("Could not close JDBC connection after transaction", ex); + } + } + + getJpaDialect().cleanupTransaction(txObject.getTransactionData()); + + // Remove the entity manager holder from the thread. + if (txObject.isNewEntityManagerHolder()) { + EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); + if (logger.isDebugEnabled()) { + logger.debug("Closing JPA EntityManager [" + em + "] after transaction"); + } + EntityManagerFactoryUtils.closeEntityManager(em); + } + else { + logger.debug("Not closing pre-bound JPA EntityManager after transaction"); + } + } + + + /** + * JPA transaction object, representing a EntityManagerHolder. + * Used as transaction object by JpaTransactionManager. + */ + private static class JpaTransactionObject extends JdbcTransactionObjectSupport { + + private EntityManagerHolder entityManagerHolder; + + private boolean newEntityManagerHolder; + + private Object transactionData; + + public void setEntityManagerHolder( + EntityManagerHolder entityManagerHolder, boolean newEntityManagerHolder) { + this.entityManagerHolder = entityManagerHolder; + this.newEntityManagerHolder = newEntityManagerHolder; + } + + public EntityManagerHolder getEntityManagerHolder() { + return this.entityManagerHolder; + } + + public boolean isNewEntityManagerHolder() { + return this.newEntityManagerHolder; + } + + public boolean hasTransaction() { + return (this.entityManagerHolder != null && this.entityManagerHolder.isTransactionActive()); + } + + public void setTransactionData(Object transactionData) { + this.transactionData = transactionData; + this.entityManagerHolder.setTransactionActive(true); + if (transactionData instanceof SavepointManager) { + this.entityManagerHolder.setSavepointManager((SavepointManager) transactionData); + } + } + + public Object getTransactionData() { + return this.transactionData; + } + + public void setRollbackOnly() { + EntityTransaction tx = this.entityManagerHolder.getEntityManager().getTransaction(); + if (tx.isActive()) { + tx.setRollbackOnly(); + } + if (hasConnectionHolder()) { + getConnectionHolder().setRollbackOnly(); + } + } + + public boolean isRollbackOnly() { + EntityTransaction tx = this.entityManagerHolder.getEntityManager().getTransaction(); + return tx.getRollbackOnly(); + } + + public Object createSavepoint() throws TransactionException { + return getSavepointManager().createSavepoint(); + } + + public void rollbackToSavepoint(Object savepoint) throws TransactionException { + getSavepointManager().rollbackToSavepoint(savepoint); + } + + public void releaseSavepoint(Object savepoint) throws TransactionException { + getSavepointManager().releaseSavepoint(savepoint); + } + + private SavepointManager getSavepointManager() { + if (!isSavepointAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions"); + } + SavepointManager savepointManager = getEntityManagerHolder().getSavepointManager(); + if (savepointManager == null) { + throw new NestedTransactionNotSupportedException( + "JpaDialect does not support savepoints - check your JPA provider's capabilities"); + } + return savepointManager; + } + } + + + /** + * Holder for suspended resources. + * Used internally by doSuspend and doResume. + */ + private static class SuspendedResourcesHolder { + + private final EntityManagerHolder entityManagerHolder; + + private final ConnectionHolder connectionHolder; + + private SuspendedResourcesHolder(EntityManagerHolder emHolder, ConnectionHolder conHolder) { + this.entityManagerHolder = emHolder; + this.connectionHolder = conHolder; + } + + private EntityManagerHolder getEntityManagerHolder() { + return this.entityManagerHolder; + } + + private ConnectionHolder getConnectionHolder() { + return this.connectionHolder; + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java new file mode 100644 index 00000000000..52598a1141d --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2008 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.util.Map; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.spi.PersistenceProvider; + +/** + * SPI interface that allows to plug in vendor-specific behavior + * into Spring's EntityManagerFactory creators. Serves as single + * configuration point for all vendor-specific properties. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see AbstractEntityManagerFactoryBean#setJpaVendorAdapter + */ +public interface JpaVendorAdapter { + + /** + * Return the vendor-specific persistence provider. + */ + PersistenceProvider getPersistenceProvider(); + + /** + * Return the name of the persistence provider's root package + * (e.g. "oracle.toplink.essentials"). Will be used for + * excluding provider classes from temporary class overriding. + * @since 2.5.2 + */ + String getPersistenceProviderRootPackage(); + + /** + * Return a Map of vendor-specific JPA properties, + * typically based on settings in this JpaVendorAdapter instance. + *

Note that there might be further JPA properties defined on + * the EntityManagerFactory bean, which might potentially override + * individual JPA property values specified here. + * @return a Map of JPA properties, as as accepted by the standard + * JPA bootstrap facilities, or null or an empty Map + * if there are no such properties to expose + * @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map) + * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map) + */ + Map getJpaPropertyMap(); + + /** + * Return the vendor-specific JpaDialect implementation for this + * provider, or null if there is none. + */ + JpaDialect getJpaDialect(); + + /** + * Return the vendor-specific EntityManagerFactory interface + * that the EntityManagerFactory proxy is supposed to implement. + *

If the provider does not offer any EntityManagerFactory extensions, + * the adapter should simply return the standard + * {@link javax.persistence.EntityManagerFactory} class here. + * @since 2.5.2 + */ + Class getEntityManagerFactoryInterface(); + + /** + * Return the vendor-specific EntityManager interface + * that this provider's EntityManagers will implement. + *

If the provider does not offer any EntityManager extensions, + * the adapter should simply return the standard + * {@link javax.persistence.EntityManager} class here. + */ + Class getEntityManagerInterface(); + + /** + * Optional callback for post-processing the native EntityManagerFactory + * before active use. + *

This can be used for triggering vendor-specific initialization processes. + * While this is not expected to be used for most providers, it is included + * here as a general extension hook. + */ + void postProcessEntityManagerFactory(EntityManagerFactory emf); + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java new file mode 100644 index 00000000000..5e5e9f4fb45 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -0,0 +1,281 @@ +/* + * Copyright 2002-2008 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 javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.sql.DataSource; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.jdbc.datasource.lookup.SingleDataSourceLookup; +import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; +import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a JPA + * {@link javax.persistence.EntityManagerFactory} according to JPA's standard + * container bootstrap contract. This is the most powerful way to set + * up a shared JPA EntityManagerFactory in a Spring application context; + * the EntityManagerFactory can then be passed to JPA-based DAOs via + * dependency injection. Note that switching to a JNDI lookup or to a + * {@link org.springframework.orm.jpa.LocalEntityManagerFactoryBean} + * definition is just a matter of configuration! + * + *

As with {@link LocalEntityManagerFactoryBean}, configuration settings + * are usually read in from a META-INF/persistence.xml config file, + * residing in the class path, according to the general JPA configuration contract. + * However, this FactoryBean is more flexible in that you can override the location + * of the persistence.xml file, specify the JDBC DataSources to link to, + * etc. Furthermore, it allows for pluggable class instrumentation through Spring's + * {@link org.springframework.instrument.classloading.LoadTimeWeaver} abstraction, + * instead of being tied to a special VM agent specified on JVM startup. + * + *

Internally, this FactoryBean parses the persistence.xml file + * itself and creates a corresponding {@link javax.persistence.spi.PersistenceUnitInfo} + * object (with further configuration merged in, such as JDBC DataSources and the + * Spring LoadTimeWeaver), to be passed to the chosen JPA + * {@link javax.persistence.spi.PersistenceProvider}. This corresponds to a + * local JPA container with full support for the standard JPA container contract. + * + *

The exposed EntityManagerFactory object will implement all the interfaces of + * the underlying native EntityManagerFactory returned by the PersistenceProvider, + * plus the {@link EntityManagerFactoryInfo} interface which exposes additional + * metadata as assembled by this FactoryBean. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see #setPersistenceXmlLocation + * @see #setJpaProperties + * @see #setJpaVendorAdapter + * @see #setLoadTimeWeaver + * @see #setDataSource + * @see EntityManagerFactoryInfo + * @see LocalEntityManagerFactoryBean + * @see org.springframework.orm.jpa.support.SharedEntityManagerBean + * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory + */ +public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean + implements ResourceLoaderAware, LoadTimeWeaverAware { + + private PersistenceUnitManager persistenceUnitManager; + + private final DefaultPersistenceUnitManager internalPersistenceUnitManager = + new DefaultPersistenceUnitManager(); + + private PersistenceUnitInfo persistenceUnitInfo; + + + /** + * Set the PersistenceUnitManager to use for obtaining the JPA persistence unit + * that this FactoryBean is supposed to build an EntityManagerFactory for. + *

The default is to rely on the local settings specified on this FactoryBean, + * such as "persistenceXmlLocation", "dataSource" and "loadTimeWeaver". + *

For reuse of existing persistence unit configuration or more advanced forms + * of custom persistence unit handling, consider defining a separate + * PersistenceUnitManager bean (typically a DefaultPersistenceUnitManager instance) + * and linking it in here. persistence.xml location, DataSource + * configuration and LoadTimeWeaver will be defined on that separate + * DefaultPersistenceUnitManager bean in such a scenario. + * @see #setPersistenceXmlLocation + * @see #setDataSource + * @see #setLoadTimeWeaver + * @see org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager + */ + public void setPersistenceUnitManager(PersistenceUnitManager persistenceUnitManager) { + this.persistenceUnitManager = persistenceUnitManager; + } + + /** + * Set the location of the persistence.xml file + * we want to use. This is a Spring resource location. + *

Default is "classpath:META-INF/persistence.xml". + *

NOTE: Only applied if no external PersistenceUnitManager specified. + * @param persistenceXmlLocation a Spring resource String + * identifying the location of the persistence.xml file + * that this LocalContainerEntityManagerFactoryBean should parse + * @see #setPersistenceUnitManager + */ + public void setPersistenceXmlLocation(String persistenceXmlLocation) { + this.internalPersistenceUnitManager.setPersistenceXmlLocations(new String[] {persistenceXmlLocation}); + } + + /** + * Specify the JDBC DataSource that the JPA persistence provider is supposed + * to use for accessing the database. This is an alternative to keeping the + * JDBC configuration in persistence.xml, passing in a Spring-managed + * DataSource instead. + *

In JPA speak, a DataSource passed in here will be used as "nonJtaDataSource" + * on the PersistenceUnitInfo passed to the PersistenceProvider, overriding + * data source configuration in persistence.xml (if any). + *

NOTE: Only applied if no external PersistenceUnitManager specified. + * @see javax.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource() + * @see #setPersistenceUnitManager + */ + public void setDataSource(DataSource dataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource)); + this.internalPersistenceUnitManager.setDefaultDataSource(dataSource); + } + + /** + * Set the PersistenceUnitPostProcessors to be applied to the + * PersistenceUnitInfo used for creating this EntityManagerFactory. + *

Such post-processors can, for example, register further entity + * classes and jar files, in addition to the metadata read in from + * persistence.xml. + *

NOTE: Only applied if no external PersistenceUnitManager specified. + * @see #setPersistenceUnitManager + */ + public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor[] postProcessors) { + this.internalPersistenceUnitManager.setPersistenceUnitPostProcessors(postProcessors); + } + + /** + * Specify the Spring LoadTimeWeaver to use for class instrumentation according + * to the JPA class transformer contract. + *

It is a not required to specify a LoadTimeWeaver: Most providers will be + * able to provide a subset of their functionality without class instrumentation + * as well, or operate with their VM agent specified on JVM startup. + *

In terms of Spring-provided weaving options, the most important ones are + * InstrumentationLoadTimeWeaver, which requires a Spring-specific (but very general) + * VM agent specified on JVM startup, and ReflectiveLoadTimeWeaver, which interacts + * with an underlying ClassLoader based on specific extended methods being available + * on it (for example, interacting with Spring's TomcatInstrumentableClassLoader). + *

NOTE: As of Spring 2.5, the context's default LoadTimeWeaver (defined + * as bean with name "loadTimeWeaver") will be picked up automatically, if available, + * removing the need for LoadTimeWeaver configuration on each affected target bean. + * Consider using the context:load-time-weaver XML tag for creating + * such a shared LoadTimeWeaver (autodetecting the environment by default). + *

NOTE: Only applied if no external PersistenceUnitManager specified. + * Otherwise, the external {@link #setPersistenceUnitManager PersistenceUnitManager} + * is responsible for the weaving configuration. + * @see org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver + * @see org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver + * @see org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader + */ + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + this.internalPersistenceUnitManager.setLoadTimeWeaver(loadTimeWeaver); + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.internalPersistenceUnitManager.setResourceLoader(resourceLoader); + } + + + @Override + protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException { + PersistenceUnitManager managerToUse = this.persistenceUnitManager; + if (this.persistenceUnitManager == null) { + this.internalPersistenceUnitManager.afterPropertiesSet(); + managerToUse = this.internalPersistenceUnitManager; + } + + this.persistenceUnitInfo = determinePersistenceUnitInfo(managerToUse); + JpaVendorAdapter jpaVendorAdapter = getJpaVendorAdapter(); + if (jpaVendorAdapter != null && this.persistenceUnitInfo instanceof MutablePersistenceUnitInfo) { + ((MutablePersistenceUnitInfo) this.persistenceUnitInfo).setPersistenceProviderPackageName( + jpaVendorAdapter.getPersistenceProviderRootPackage()); + } + + PersistenceProvider provider = getPersistenceProvider(); + if (provider == null) { + String providerClassName = this.persistenceUnitInfo.getPersistenceProviderClassName(); + if (providerClassName == null) { + throw new IllegalArgumentException( + "No PersistenceProvider specified in EntityManagerFactory configuration, " + + "and chosen PersistenceUnitInfo does not specify a provider class name either"); + } + Class providerClass = ClassUtils.resolveClassName(providerClassName, getBeanClassLoader()); + provider = (PersistenceProvider) BeanUtils.instantiateClass(providerClass); + } + if (provider == null) { + throw new IllegalStateException("Unable to determine persistence provider. " + + "Please check configuration of " + getClass().getName() + "; " + + "ideally specify the appropriate JpaVendorAdapter class for this provider."); + } + + if (logger.isInfoEnabled()) { + logger.info("Building JPA container EntityManagerFactory for persistence unit '" + + this.persistenceUnitInfo.getPersistenceUnitName() + "'"); + } + this.nativeEntityManagerFactory = + provider.createContainerEntityManagerFactory(this.persistenceUnitInfo, getJpaPropertyMap()); + postProcessEntityManagerFactory(this.nativeEntityManagerFactory, this.persistenceUnitInfo); + + return this.nativeEntityManagerFactory; + } + + + /** + * Determine the PersistenceUnitInfo to use for the EntityManagerFactory + * created by this bean. + *

The default implementation reads in all persistence unit infos from + * persistence.xml, as defined in the JPA specification. + * If no entity manager name was specified, it takes the first info in the + * array as returned by the reader. Otherwise, it checks for a matching name. + * @param persistenceUnitManager the PersistenceUnitManager to obtain from + * @return the chosen PersistenceUnitInfo + */ + protected PersistenceUnitInfo determinePersistenceUnitInfo(PersistenceUnitManager persistenceUnitManager) { + if (getPersistenceUnitName() != null) { + return persistenceUnitManager.obtainPersistenceUnitInfo(getPersistenceUnitName()); + } + else { + return persistenceUnitManager.obtainDefaultPersistenceUnitInfo(); + } + } + + /** + * Hook method allowing subclasses to customize the EntityManagerFactory + * after its creation via the PersistenceProvider. + *

The default implementation is empty. + * @param emf the newly created EntityManagerFactory we are working with + * @param pui the PersistenceUnitInfo used to configure the EntityManagerFactory + * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory + */ + protected void postProcessEntityManagerFactory(EntityManagerFactory emf, PersistenceUnitInfo pui) { + } + + + public PersistenceUnitInfo getPersistenceUnitInfo() { + return this.persistenceUnitInfo; + } + + public String getPersistenceUnitName() { + if (this.persistenceUnitInfo != null) { + return this.persistenceUnitInfo.getPersistenceUnitName(); + } + return super.getPersistenceUnitName(); + } + + public DataSource getDataSource() { + if (this.persistenceUnitInfo != null) { + return this.persistenceUnitInfo.getNonJtaDataSource(); + } + return this.internalPersistenceUnitManager.getDefaultDataSource(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java new file mode 100644 index 00000000000..b4251557015 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2007 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 javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.PersistenceException; +import javax.persistence.spi.PersistenceProvider; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a JPA + * {@link javax.persistence.EntityManagerFactory} according to JPA's standard + * standalone bootstrap contract. This is the simplest way to set up a + * shared JPA EntityManagerFactory in a Spring application context; the + * EntityManagerFactory can then be passed to JPA-based DAOs via + * dependency injection. Note that switching to a JNDI lookup or to a + * {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean} + * definition is just a matter of configuration! + * + *

Configuration settings are usually read from a META-INF/persistence.xml + * config file, residing in the class path, according to the JPA standalone bootstrap + * contract. Additionally, most JPA providers will require a special VM agent + * (specified on JVM startup) that allows them to instrument application classes. + * See the Java Persistence API specification and your provider documentation + * for setup details. + * + *

This EntityManagerFactory bootstrap is appropriate for standalone applications + * which solely use JPA for data access. If you want to set up your persistence + * provider for an external DataSource and/or for global transactions which span + * multiple resources, you will need to either deploy it into a full Java EE 5 + * application server and access the deployed EntityManagerFactory via JNDI, + * or use Spring's {@link LocalContainerEntityManagerFactoryBean} with appropriate + * configuration for local setup according to JPA's container contract. + * + *

Note: This FactoryBean has limited configuration power in terms of + * what configuration it is able to pass to the JPA provider. If you need more + * flexible configuration, for example passing a Spring-managed JDBC DataSource + * to the JPA provider, consider using Spring's more powerful + * {@link LocalContainerEntityManagerFactoryBean} instead. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see #setJpaProperties + * @see #setJpaVendorAdapter + * @see JpaTemplate#setEntityManagerFactory + * @see JpaTransactionManager#setEntityManagerFactory + * @see LocalContainerEntityManagerFactoryBean + * @see org.springframework.jndi.JndiObjectFactoryBean + * @see org.springframework.orm.jpa.support.SharedEntityManagerBean + * @see javax.persistence.Persistence#createEntityManagerFactory + * @see javax.persistence.spi.PersistenceProvider#createEntityManagerFactory + */ +public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean { + + /** + * Initialize the EntityManagerFactory for the given configuration. + * @throws javax.persistence.PersistenceException in case of JPA initialization errors + */ + protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException { + if (logger.isInfoEnabled()) { + logger.info("Building JPA EntityManagerFactory for persistence unit '" + getPersistenceUnitName() + "'"); + } + PersistenceProvider provider = getPersistenceProvider(); + if (provider != null) { + // Create EntityManagerFactory directly through PersistenceProvider. + EntityManagerFactory emf = provider.createEntityManagerFactory(getPersistenceUnitName(), getJpaPropertyMap()); + if (emf == null) { + throw new IllegalStateException( + "PersistenceProvider [" + provider + "] did not return an EntityManagerFactory for name '" + + getPersistenceUnitName() + "'"); + } + return emf; + } + else { + // Let JPA perform its standard PersistenceProvider autodetection. + return Persistence.createEntityManagerFactory(getPersistenceUnitName(), getJpaPropertyMap()); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java new file mode 100644 index 00000000000..1e835850eed --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2008 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.Map; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Query; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.CollectionUtils; + +/** + * Factory for a shareable JPA {@link javax.persistence.EntityManager} + * for a given {@link javax.persistence.EntityManagerFactory}. + * + *

The shareable EntityManager will behave just like an EntityManager fetched + * from an application server's JNDI environment, as defined by the JPA + * specification. It will delegate all calls to the current transactional + * EntityManager, if any; otherwise it will fall back to a newly created + * EntityManager per operation. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see org.springframework.orm.jpa.LocalEntityManagerFactoryBean + * @see org.springframework.orm.jpa.JpaTransactionManager + */ +public abstract class SharedEntityManagerCreator { + + /** + * Create a transactional EntityManager proxy for the given EntityManagerFactory. + * @param emf the EntityManagerFactory to delegate to. + * If this implements the {@link EntityManagerFactoryInfo} interface, + * appropriate handling of the native EntityManagerFactory and available + * {@link EntityManagerPlusOperations} will automatically apply. + * @return a shareable transaction EntityManager proxy + */ + public static EntityManager createSharedEntityManager(EntityManagerFactory emf) { + return createSharedEntityManager(emf, null); + } + + /** + * Create a transactional EntityManager proxy for the given EntityManagerFactory. + * @param emf the EntityManagerFactory to delegate to. + * If this implements the {@link EntityManagerFactoryInfo} interface, + * appropriate handling of the native EntityManagerFactory and available + * {@link EntityManagerPlusOperations} will automatically apply. + * @param properties the properties to be passed into the + * createEntityManager call (may be null) + * @return a shareable transaction EntityManager proxy + */ + public static EntityManager createSharedEntityManager(EntityManagerFactory emf, Map properties) { + Class[] emIfcs = null; + if (emf instanceof EntityManagerFactoryInfo) { + EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) emf; + Class emIfc = emfInfo.getEntityManagerInterface(); + if (emIfc == null) { + emIfc = EntityManager.class; + } + JpaDialect jpaDialect = emfInfo.getJpaDialect(); + if (jpaDialect != null && jpaDialect.supportsEntityManagerPlusOperations()) { + emIfcs = new Class[] {emIfc, EntityManagerPlus.class}; + } + else { + emIfcs = new Class[] {emIfc}; + } + } + else { + emIfcs = new Class[] {EntityManager.class}; + } + return createSharedEntityManager(emf, properties, emIfcs); + } + + /** + * Create a transactional EntityManager proxy for the given EntityManagerFactory. + * @param emf EntityManagerFactory to obtain EntityManagers from as needed + * @param properties the properties to be passed into the + * createEntityManager call (may be null) + * @param entityManagerInterfaces the interfaces to be implemented by the + * EntityManager. Allows the addition or specification of proprietary interfaces. + * @return a shareable transactional EntityManager proxy + */ + public static EntityManager createSharedEntityManager( + EntityManagerFactory emf, Map properties, Class... entityManagerInterfaces) { + + ClassLoader cl = null; + if (emf instanceof EntityManagerFactoryInfo) { + cl = ((EntityManagerFactoryInfo) emf).getBeanClassLoader(); + } + Class[] ifcs = new Class[entityManagerInterfaces.length + 1]; + System.arraycopy(entityManagerInterfaces, 0, ifcs, 0, entityManagerInterfaces.length); + ifcs[entityManagerInterfaces.length] = EntityManagerProxy.class; + return (EntityManager) Proxy.newProxyInstance( + (cl != null ? cl : SharedEntityManagerCreator.class.getClassLoader()), + ifcs, new SharedEntityManagerInvocationHandler(emf, properties)); + } + + + /** + * Invocation handler that delegates all calls to the current + * transactional EntityManager, if any; else, it will fall back + * to a newly created EntityManager per operation. + */ + private static class SharedEntityManagerInvocationHandler implements InvocationHandler { + + private final Log logger = LogFactory.getLog(getClass()); + + private final EntityManagerFactory targetFactory; + + private final Map properties; + + public SharedEntityManagerInvocationHandler(EntityManagerFactory target, Map properties) { + this.targetFactory = target; + this.properties = properties; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on EntityManager interface 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 hashCode(); + } + else if (method.getName().equals("toString")) { + // Deliver toString without touching a target EntityManager. + return "Shared EntityManager proxy for target factory [" + this.targetFactory + "]"; + } + else if (method.getName().equals("isOpen")) { + // Handle isOpen method: always return true. + return true; + } + else if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return null; + } + else if (method.getName().equals("getTransaction")) { + throw new IllegalStateException( + "Not allowed to create transaction on shared EntityManager - " + + "use Spring transactions or EJB CMT instead"); + } + else if (method.getName().equals("joinTransaction")) { + throw new IllegalStateException( + "Not allowed to join transaction on shared EntityManager - " + + "use Spring transactions or EJB CMT instead"); + } + + // Determine current EntityManager: either the transactional one + // managed by the factory or a temporary one for the given invocation. + EntityManager target = + EntityManagerFactoryUtils.doGetTransactionalEntityManager(this.targetFactory, this.properties); + + // Handle EntityManagerProxy interface. + if (method.getName().equals("getTargetEntityManager")) { + if (target == null) { + throw new IllegalStateException("No transactional EntityManager available"); + } + return target; + } + + // Regular EntityManager operations. + boolean isNewEm = false; + if (target == null) { + logger.debug("Creating new EntityManager for shared EntityManager invocation"); + target = (!CollectionUtils.isEmpty(this.properties) ? + this.targetFactory.createEntityManager(this.properties) : + this.targetFactory.createEntityManager()); + isNewEm = true; + } + + // Invoke method on current EntityManager. + try { + Object result = method.invoke(target, args); + if (isNewEm && result instanceof Query) { + result = Proxy.newProxyInstance(Query.class.getClassLoader(), new Class[] {Query.class}, + new DeferredQueryInvocationHandler((Query) result, target)); + isNewEm = false; + } + return result; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + finally { + if (isNewEm) { + EntityManagerFactoryUtils.closeEntityManager(target); + } + } + } + } + + + /** + * Invocation handler that handles deferred Query objects created by + * non-transactional createQuery invocations on a shared EntityManager. + */ + private static class DeferredQueryInvocationHandler implements InvocationHandler { + + private final Query target; + + private final EntityManager em; + + private DeferredQueryInvocationHandler(Query target, EntityManager em) { + this.target = target; + this.em = em; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on Query interface 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 hashCode(); + } + + // Invoke method on actual Query object. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + finally { + if (method.getName().equals("getResultList") || method.getName().equals("getSingleResult") || + method.getName().equals("executeUpdate")) { + EntityManagerFactoryUtils.closeEntityManager(this.em); + } + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/package.html new file mode 100644 index 00000000000..a223af08b2c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/package.html @@ -0,0 +1,9 @@ + + + +Package providing integration of JPA (Java Persistence API) with Spring concepts. +Contains EntityManagerFactory helper classes, a template plus callback for JPA access, +and an implementation of Spring's transaction SPI for local JPA transactions. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java new file mode 100644 index 00000000000..7402618f12d --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2006 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.persistenceunit; + +import java.lang.instrument.ClassFileTransformer; +import java.security.ProtectionDomain; + +import javax.persistence.spi.ClassTransformer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; + +/** + * Simple adapter that implements the java.lang.instrument.ClassFileTransformer + * interface based on a JPA ClassTransformer which a JPA PersistenceProvider asks the + * PersistenceUnitInfo to install in the current runtime. + * + * @author Rod Johnson + * @since 2.0 + * @see javax.persistence.spi.PersistenceUnitInfo#addTransformer(javax.persistence.spi.ClassTransformer) + */ +class ClassFileTransformerAdapter implements ClassFileTransformer { + + private static final Log logger = LogFactory.getLog(ClassFileTransformerAdapter.class); + + private final ClassTransformer classTransformer; + + + public ClassFileTransformerAdapter(ClassTransformer classTransformer) { + Assert.notNull(classTransformer, "ClassTransformer must not be null"); + this.classTransformer = classTransformer; + } + + + public byte[] transform( + ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) { + + try { + byte[] transformed = this.classTransformer.transform( + loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + if (transformed != null && logger.isDebugEnabled()) { + logger.debug("Transformer of class [" + this.classTransformer.getClass().getName() + + "] transformed class [" + className + "]; bytes in=" + + classfileBuffer.length + "; bytes out=" + transformed.length); + } + return transformed; + } + catch (ClassCircularityError ex) { + logger.error("Error weaving class [" + className + "] with " + + "transformer of class [" + this.classTransformer.getClass().getName() + "]", ex); + throw new IllegalStateException("Could not weave class [" + className + "]", ex); + } + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error weaving class [" + className + "] with " + + "transformer of class [" + this.classTransformer.getClass().getName() + "]", ex); + } + // The exception will be ignored by the class loader, anyway... + throw new IllegalStateException("Could not weave class [" + className + "]", ex); + } + } + + + @Override + public String toString() { + return "Standard ClassFileTransformer wrapping JPA transformer: " + this.classTransformer; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java new file mode 100644 index 00000000000..25b5a7f1e07 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java @@ -0,0 +1,390 @@ +/* + * Copyright 2002-2008 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.persistenceunit; + +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.persistence.PersistenceException; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.jdbc.datasource.lookup.DataSourceLookup; +import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; +import org.springframework.jdbc.datasource.lookup.MapDataSourceLookup; +import org.springframework.util.ObjectUtils; + +/** + * Default implementation of the {@link PersistenceUnitManager} interface. + * Used as internal default by + * {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean}. + * + *

Supports standard JPA scanning for persistence.xml files, + * with configurable file locations, JDBC DataSource lookup and load-time weaving. + * + *

The default XML file location is classpath*:META-INF/persistence.xml, + * scanning for all matching files in the class path (as defined in the JPA specification). + * DataSource names are by default interpreted as JNDI names, and no load time weaving + * is available (which requires weaving to be turned off in the persistence provider). + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setPersistenceXmlLocations + * @see #setDataSourceLookup + * @see #setLoadTimeWeaver + * @see org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean#setPersistenceUnitManager + */ +public class DefaultPersistenceUnitManager + implements PersistenceUnitManager, ResourceLoaderAware, LoadTimeWeaverAware, InitializingBean { + + /** + * Default location of the persistence.xml file: + * "classpath*:META-INF/persistence.xml". + */ + public final static String DEFAULT_PERSISTENCE_XML_LOCATION = "classpath*:META-INF/persistence.xml"; + + /** + * Default location for the persistence unit root URL: + * "classpath:", indicating the root of the class path. + */ + public final static String ORIGINAL_DEFAULT_PERSISTENCE_UNIT_ROOT_LOCATION = "classpath:"; + + + /** Location of persistence.xml file(s) */ + private String[] persistenceXmlLocations = new String[] {DEFAULT_PERSISTENCE_XML_LOCATION}; + + private String defaultPersistenceUnitRootLocation = ORIGINAL_DEFAULT_PERSISTENCE_UNIT_ROOT_LOCATION; + + private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); + + private DataSource defaultDataSource; + + private PersistenceUnitPostProcessor[] persistenceUnitPostProcessors; + + private LoadTimeWeaver loadTimeWeaver; + + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + + private final Set persistenceUnitInfoNames = new HashSet(); + + private final Map persistenceUnitInfos = + new HashMap(); + + + /** + * Specify the location of the persistence.xml files to load. + * These can be specified as Spring resource locations and/or location patterns. + *

Default is "classpath*:META-INF/persistence.xml". + */ + public void setPersistenceXmlLocation(String persistenceXmlLocation) { + this.persistenceXmlLocations = new String[] {persistenceXmlLocation}; + } + + /** + * Specify multiple locations of persistence.xml files to load. + * These can be specified as Spring resource locations and/or location patterns. + *

Default is "classpath*:META-INF/persistence.xml". + * @param persistenceXmlLocations an array of Spring resource Strings + * identifying the location of the persistence.xml files to read + */ + public void setPersistenceXmlLocations(String[] persistenceXmlLocations) { + this.persistenceXmlLocations = persistenceXmlLocations; + } + + /** + * Set the default persistence unit root location, to be applied + * if no unit-specific persistence unit root could be determined. + *

Default is "classpath:", that is, the root of the current class path + * (nearest root directory). To be overridden if unit-specific resolution + * does not work and the class path root is not appropriate either. + */ + public void setDefaultPersistenceUnitRootLocation(String defaultPersistenceUnitRootLocation) { + this.defaultPersistenceUnitRootLocation = defaultPersistenceUnitRootLocation; + } + + /** + * Specify the JDBC DataSources that the JPA persistence provider is supposed + * to use for accessing the database, resolving data source names in + * persistence.xml against Spring-managed DataSources. + *

The specified Map needs to define data source names for specific DataSource + * objects, matching the data source names used in persistence.xml. + * If not specified, data source names will be resolved as JNDI names instead + * (as defined by standard JPA). + * @see org.springframework.jdbc.datasource.lookup.MapDataSourceLookup + */ + public void setDataSources(Map dataSources) { + this.dataSourceLookup = new MapDataSourceLookup(dataSources); + } + + /** + * Specify the JDBC DataSourceLookup that provides DataSources for the + * persistence provider, resolving data source names in persistence.xml + * against Spring-managed DataSource instances. + *

Default is JndiDataSourceLookup, which resolves DataSource names as + * JNDI names (as defined by standard JPA). Specify a BeanFactoryDataSourceLookup + * instance if you want DataSource names to be resolved against Spring bean names. + *

Alternatively, consider passing in a map from names to DataSource instances + * via the "dataSources" property. If the persistence.xml file + * does not define DataSource names at all, specify a default DataSource + * via the "defaultDataSource" property. + * @see org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup + * @see org.springframework.jdbc.datasource.lookup.BeanFactoryDataSourceLookup + * @see #setDataSources + * @see #setDefaultDataSource + */ + public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { + this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); + } + + /** + * Return the JDBC DataSourceLookup that provides DataSources for the + * persistence provider, resolving data source names in persistence.xml + * against Spring-managed DataSource instances. + */ + public DataSourceLookup getDataSourceLookup() { + return this.dataSourceLookup; + } + + /** + * Specify the JDBC DataSource that the JPA persistence provider is supposed + * to use for accessing the database if none has been specified in + * persistence.xml. + *

In JPA speak, a DataSource passed in here will be uses as "nonJtaDataSource" + * on the PersistenceUnitInfo passed to the PersistenceProvider, provided that + * none has been registered before. + * @see javax.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource() + */ + public void setDefaultDataSource(DataSource defaultDataSource) { + this.defaultDataSource = defaultDataSource; + } + + /** + * Return the JDBC DataSource that the JPA persistence provider is supposed + * to use for accessing the database if none has been specified in + * persistence.xml. + */ + public DataSource getDefaultDataSource() { + return this.defaultDataSource; + } + + /** + * Set the PersistenceUnitPostProcessors to be applied to each + * PersistenceUnitInfo that has been parsed by this manager. + *

Such post-processors can, for example, register further entity + * classes and jar files, in addition to the metadata read in from + * persistence.xml. + */ + public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor[] postProcessors) { + this.persistenceUnitPostProcessors = postProcessors; + } + + /** + * Return the PersistenceUnitPostProcessors to be applied to each + * PersistenceUnitInfo that has been parsed by this manager. + */ + public PersistenceUnitPostProcessor[] getPersistenceUnitPostProcessors() { + return this.persistenceUnitPostProcessors; + } + + /** + * Specify the Spring LoadTimeWeaver to use for class instrumentation according + * to the JPA class transformer contract. + *

It is not required to specify a LoadTimeWeaver: Most providers will be + * able to provide a subset of their functionality without class instrumentation + * as well, or operate with their VM agent specified on JVM startup. + *

In terms of Spring-provided weaving options, the most important ones are + * InstrumentationLoadTimeWeaver, which requires a Spring-specific (but very general) + * VM agent specified on JVM startup, and ReflectiveLoadTimeWeaver, which interacts + * with an underlying ClassLoader based on specific extended methods being available + * on it (for example, interacting with Spring's TomcatInstrumentableClassLoader). + *

NOTE: As of Spring 2.5, the context's default LoadTimeWeaver (defined + * as bean with name "loadTimeWeaver") will be picked up automatically, if available, + * removing the need for LoadTimeWeaver configuration on each affected target bean. + * Consider using the context:load-time-weaver XML tag for creating + * such a shared LoadTimeWeaver (autodetecting the environment by default). + * @see org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver + * @see org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver + * @see org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader + */ + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + this.loadTimeWeaver = loadTimeWeaver; + } + + /** + * Return the Spring LoadTimeWeaver to use for class instrumentation according + * to the JPA class transformer contract. + */ + public LoadTimeWeaver getLoadTimeWeaver() { + return this.loadTimeWeaver; + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = (resourceLoader != null ? + ResourcePatternUtils.getResourcePatternResolver(resourceLoader) : + new PathMatchingResourcePatternResolver()); + } + + + public void afterPropertiesSet() { + if (this.loadTimeWeaver == null && InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) { + this.loadTimeWeaver = new InstrumentationLoadTimeWeaver(this.resourcePatternResolver.getClassLoader()); + } + preparePersistenceUnitInfos(); + } + + /** + * Prepare the PersistenceUnitInfos according to the configuration + * of this manager: scanning for persistence.xml files, + * parsing all matching files, configurating and post-processing them. + *

PersistenceUnitInfos cannot be obtained before this preparation + * method has been invoked. + * @see #obtainDefaultPersistenceUnitInfo() + * @see #obtainPersistenceUnitInfo(String) + */ + public void preparePersistenceUnitInfos() { + this.persistenceUnitInfoNames.clear(); + this.persistenceUnitInfos.clear(); + SpringPersistenceUnitInfo[] puis = readPersistenceUnitInfos(); + for (int i = 0; i < puis.length; i++) { + SpringPersistenceUnitInfo pui = puis[i]; + if (pui.getPersistenceUnitRootUrl() == null) { + pui.setPersistenceUnitRootUrl(determineDefaultPersistenceUnitRootUrl()); + } + if (pui.getNonJtaDataSource() == null) { + pui.setNonJtaDataSource(this.defaultDataSource); + } + if (this.loadTimeWeaver != null) { + pui.init(this.loadTimeWeaver); + } + else { + pui.init(this.resourcePatternResolver.getClassLoader()); + } + postProcessPersistenceUnitInfo(pui); + String name = pui.getPersistenceUnitName(); + this.persistenceUnitInfoNames.add(name); + this.persistenceUnitInfos.put(name, pui); + } + } + + /** + * Read all persistence unit infos from persistence.xml, + * as defined in the JPA specification. + */ + private SpringPersistenceUnitInfo[] readPersistenceUnitInfos() { + PersistenceUnitReader reader = new PersistenceUnitReader(this.resourcePatternResolver, this.dataSourceLookup); + return reader.readPersistenceUnitInfos(this.persistenceXmlLocations); + } + + /** + * Try to determine the persistence unit root URL based on the given + * "defaultPersistenceUnitRootLocation". + * @return the persistence unit root URL to pass to the JPA PersistenceProvider + * @see #setDefaultPersistenceUnitRootLocation + */ + private URL determineDefaultPersistenceUnitRootUrl() { + if (this.defaultPersistenceUnitRootLocation == null) { + return null; + } + try { + Resource res = this.resourcePatternResolver.getResource(this.defaultPersistenceUnitRootLocation); + return res.getURL(); + } + catch (IOException ex) { + throw new PersistenceException("Unable to resolve persistence unit root URL", ex); + } + } + + /** + * Return the specified PersistenceUnitInfo from this manager's cache + * of processed persistence units, keeping it in the cache (i.e. not + * 'obtaining' it for use but rather just accessing it for post-processing). + *

This can be used in {@link #postProcessPersistenceUnitInfo} implementations, + * detecting existing persistence units of the same name and potentially merging them. + * @param persistenceUnitName the name of the desired persistence unit + * @return the PersistenceUnitInfo in mutable form, or null if not available + */ + protected final MutablePersistenceUnitInfo getPersistenceUnitInfo(String persistenceUnitName) { + return this.persistenceUnitInfos.get(persistenceUnitName); + } + + /** + * Hook method allowing subclasses to customize each PersistenceUnitInfo. + *

Default implementation delegates to all registered PersistenceUnitPostProcessors. + * It is usually preferable to register further entity classes, jar files etc there + * rather than in a subclass of this manager, to be able to reuse the post-processors. + * @param pui the chosen PersistenceUnitInfo, as read from persistence.xml. + * Passed in as MutablePersistenceUnitInfo. + * @see #setPersistenceUnitPostProcessors + */ + protected void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) { + PersistenceUnitPostProcessor[] postProcessors = getPersistenceUnitPostProcessors(); + if (postProcessors != null) { + for (PersistenceUnitPostProcessor postProcessor : postProcessors) { + postProcessor.postProcessPersistenceUnitInfo(pui); + } + } + } + + + public PersistenceUnitInfo obtainDefaultPersistenceUnitInfo() { + if (this.persistenceUnitInfoNames.isEmpty()) { + throw new IllegalStateException("No persistence units parsed from " + + ObjectUtils.nullSafeToString(this.persistenceXmlLocations)); + } + if (this.persistenceUnitInfos.isEmpty()) { + throw new IllegalStateException("All persistence units from " + + ObjectUtils.nullSafeToString(this.persistenceXmlLocations) + " already obtained"); + } + if (this.persistenceUnitInfos.size() > 1) { + throw new IllegalStateException("No single default persistence unit defined in " + + ObjectUtils.nullSafeToString(this.persistenceXmlLocations)); + } + PersistenceUnitInfo pui = this.persistenceUnitInfos.values().iterator().next(); + this.persistenceUnitInfos.clear(); + return pui; + } + + public PersistenceUnitInfo obtainPersistenceUnitInfo(String persistenceUnitName) { + PersistenceUnitInfo pui = this.persistenceUnitInfos.remove(persistenceUnitName); + if (pui == null) { + if (!this.persistenceUnitInfoNames.contains(persistenceUnitName)) { + throw new IllegalArgumentException( + "No persistence unit with name '" + persistenceUnitName + "' found"); + } + else { + throw new IllegalStateException( + "Persistence unit with name '" + persistenceUnitName + "' already obtained"); + } + } + return pui; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java new file mode 100644 index 00000000000..aeafe8aad76 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2008 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.persistenceunit; + +import java.net.URL; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; + +import javax.persistence.spi.ClassTransformer; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.persistence.spi.PersistenceUnitTransactionType; +import javax.sql.DataSource; + +import org.springframework.util.ClassUtils; + +/** + * Spring's base implementation of the JPA + * {@link javax.persistence.spi.PersistenceUnitInfo} interface, + * used to bootstrap an EntityManagerFactory in a container. + * + *

This implementation is largely a JavaBean, offering mutators + * for all standard PersistenceUnitInfo properties. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.0 + */ +public class MutablePersistenceUnitInfo implements PersistenceUnitInfo { + + private String persistenceUnitName; + + private String persistenceProviderClassName; + + private PersistenceUnitTransactionType transactionType; + + private DataSource nonJtaDataSource; + + private DataSource jtaDataSource; + + private List mappingFileNames = new LinkedList(); + + private List jarFileUrls = new LinkedList(); + + private URL persistenceUnitRootUrl; + + private List managedClassNames = new LinkedList(); + + private boolean excludeUnlistedClasses = false; + + private Properties properties = new Properties(); + + private String persistenceProviderPackageName; + + + public void setPersistenceUnitName(String persistenceUnitName) { + this.persistenceUnitName = persistenceUnitName; + } + + public String getPersistenceUnitName() { + return this.persistenceUnitName; + } + + public void setPersistenceProviderClassName(String persistenceProviderClassName) { + this.persistenceProviderClassName = persistenceProviderClassName; + } + + public String getPersistenceProviderClassName() { + return this.persistenceProviderClassName; + } + + public void setTransactionType(PersistenceUnitTransactionType transactionType) { + this.transactionType = transactionType; + } + + public PersistenceUnitTransactionType getTransactionType() { + if (this.transactionType != null) { + return this.transactionType; + } + else { + return (this.jtaDataSource != null ? + PersistenceUnitTransactionType.JTA : PersistenceUnitTransactionType.RESOURCE_LOCAL); + } + } + + public void setJtaDataSource(DataSource jtaDataSource) { + this.jtaDataSource = jtaDataSource; + } + + public DataSource getJtaDataSource() { + return this.jtaDataSource; + } + + public void setNonJtaDataSource(DataSource nonJtaDataSource) { + this.nonJtaDataSource = nonJtaDataSource; + } + + public DataSource getNonJtaDataSource() { + return this.nonJtaDataSource; + } + + public void addMappingFileName(String mappingFileName) { + this.mappingFileNames.add(mappingFileName); + } + + public List getMappingFileNames() { + return this.mappingFileNames; + } + + public void addJarFileUrl(URL jarFileUrl) { + this.jarFileUrls.add(jarFileUrl); + } + + public List getJarFileUrls() { + return this.jarFileUrls; + } + + public void setPersistenceUnitRootUrl(URL persistenceUnitRootUrl) { + this.persistenceUnitRootUrl = persistenceUnitRootUrl; + } + + public URL getPersistenceUnitRootUrl() { + return this.persistenceUnitRootUrl; + } + + public void addManagedClassName(String managedClassName) { + this.managedClassNames.add(managedClassName); + } + + public List getManagedClassNames() { + return this.managedClassNames; + } + + public void setExcludeUnlistedClasses(boolean excludeUnlistedClasses) { + this.excludeUnlistedClasses = excludeUnlistedClasses; + } + + public boolean excludeUnlistedClasses() { + return this.excludeUnlistedClasses; + } + + public void addProperty(String name, String value) { + if (this.properties == null) { + this.properties = new Properties(); + } + this.properties.setProperty(name, value); + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + + public Properties getProperties() { + return this.properties; + } + + public void setPersistenceProviderPackageName(String persistenceProviderPackageName) { + this.persistenceProviderPackageName = persistenceProviderPackageName; + } + + public String getPersistenceProviderPackageName() { + return this.persistenceProviderPackageName; + } + + + /** + * This implementation returns the default ClassLoader. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public ClassLoader getClassLoader() { + return ClassUtils.getDefaultClassLoader(); + } + + /** + * This implementation throws an UnsupportedOperationException. + */ + public void addTransformer(ClassTransformer classTransformer) { + throw new UnsupportedOperationException("addTransformer not supported"); + } + + /** + * This implementation throws an UnsupportedOperationException. + */ + public ClassLoader getNewTempClassLoader() { + throw new UnsupportedOperationException("getNewTempClassLoader not supported"); + } + + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PersistenceUnitInfo: name '"); + builder.append(this.persistenceUnitName); + builder.append("', root URL ["); + builder.append(this.persistenceUnitRootUrl); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitManager.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitManager.java new file mode 100644 index 00000000000..d44fc6037ff --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitManager.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2007 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.persistenceunit; + +import javax.persistence.spi.PersistenceUnitInfo; + +/** + * Interface that defines an abstraction for finding and managing + * JPA PersistenceUnitInfos. Used by + * {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean} + * in order to obtain a {@link javax.persistence.spi.PersistenceUnitInfo} + * for building a concrete {@link javax.persistence.EntityManagerFactory}. + * + *

Obtaining a PersistenceUnitInfo instance is an exclusive process. + * A PersistenceUnitInfo instance is not available for further calls + * anymore once it has been obtained. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DefaultPersistenceUnitManager + * @see org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean#setPersistenceUnitManager + */ +public interface PersistenceUnitManager { + + /** + * Obtain the default PersistenceUnitInfo from this manager. + * @return the PersistenceUnitInfo (never null) + * @throws IllegalStateException if there is no default PersistenceUnitInfo defined + * or it has already been obtained + */ + PersistenceUnitInfo obtainDefaultPersistenceUnitInfo() throws IllegalStateException; + + /** + * Obtain the specified PersistenceUnitInfo from this manager. + * @param persistenceUnitName the name of the desired persistence unit + * @return the PersistenceUnitInfo (never null) + * @throws IllegalArgumentException if no PersistenceUnitInfo with the given + * name is defined + * @throws IllegalStateException if the PersistenceUnitInfo with the given + * name has already been obtained + */ + PersistenceUnitInfo obtainPersistenceUnitInfo(String persistenceUnitName) + throws IllegalArgumentException, IllegalStateException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitPostProcessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitPostProcessor.java new file mode 100644 index 00000000000..42f0aa6be6c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitPostProcessor.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 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.persistenceunit; + +/** + * Callback interface for post-processing a JPA PersistenceUnitInfo. + * Implementations can be registered with a DefaultPersistenceUnitManager + * or via a LocalContainerEntityManagerFactoryBean. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DefaultPersistenceUnitManager#setPersistenceUnitPostProcessors + * @see org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean#setPersistenceUnitPostProcessors + */ +public interface PersistenceUnitPostProcessor { + + /** + * Post-process the given PersistenceUnitInfo, for example registering + * further entity classes and jar files. + * @param pui the chosen PersistenceUnitInfo, as read from persistence.xml. + * Passed in as MutablePersistenceUnitInfo. + */ + void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui); + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java new file mode 100644 index 00000000000..19232c585a6 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java @@ -0,0 +1,370 @@ +/* + * Copyright 2002-2008 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.persistenceunit; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.LinkedList; +import java.util.List; + +import javax.persistence.spi.PersistenceUnitTransactionType; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.jdbc.datasource.lookup.DataSourceLookup; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.util.xml.SimpleSaxErrorHandler; + +/** + * Internal helper class for reading persistence.xml files. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +class PersistenceUnitReader { + + private static final String MAPPING_FILE_NAME = "mapping-file"; + + private static final String JAR_FILE_URL = "jar-file"; + + private static final String MANAGED_CLASS_NAME = "class"; + + private static final String PROPERTIES = "properties"; + + private static final String PROVIDER = "provider"; + + private static final String EXCLUDE_UNLISTED_CLASSES = "exclude-unlisted-classes"; + + private static final String NON_JTA_DATA_SOURCE = "non-jta-data-source"; + + private static final String JTA_DATA_SOURCE = "jta-data-source"; + + private static final String TRANSACTION_TYPE = "transaction-type"; + + private static final String PERSISTENCE_UNIT = "persistence-unit"; + + private static final String UNIT_NAME = "name"; + + private static final String META_INF = "META-INF"; + + private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; + + private static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource"; + + private static final String SCHEMA_NAME = "persistence_1_0.xsd"; + + private static final String[] SCHEMA_RESOURCE_LOCATIONS = { + "classpath:persistence_1_0.xsd", + "classpath:org/hibernate/ejb/persistence_1_0.xsd", + "classpath:org/jpox/jpa/persistence_1_0.xsd"}; + + + private final Log logger = LogFactory.getLog(getClass()); + + private final ResourcePatternResolver resourcePatternResolver; + + private final DataSourceLookup dataSourceLookup; + + + /** + * Create a new PersistenceUnitReader. + * @param resourcePatternResolver the ResourcePatternResolver to use for loading resources + * @param dataSourceLookup the DataSourceLookup to resolve DataSource names in + * persistence.xml files against + */ + public PersistenceUnitReader(ResourcePatternResolver resourcePatternResolver, DataSourceLookup dataSourceLookup) { + Assert.notNull(resourcePatternResolver, "ResourceLoader must not be null"); + Assert.notNull(dataSourceLookup, "DataSourceLookup must not be null"); + this.resourcePatternResolver = resourcePatternResolver; + this.dataSourceLookup = dataSourceLookup; + } + + + /** + * Parse and build all persistence unit infos defined in the specified XML file(s). + * @param persistenceXmlLocation the resource location (can be a pattern) + * @return the resulting PersistenceUnitInfo instances + */ + public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String persistenceXmlLocation) { + return readPersistenceUnitInfos(new String[] {persistenceXmlLocation}); + } + + /** + * Parse and build all persistence unit infos defined in the given XML files. + * @param persistenceXmlLocations the resource locations (can be patterns) + * @return the resulting PersistenceUnitInfo instances + */ + public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String[] persistenceXmlLocations) { + ErrorHandler handler = new SimpleSaxErrorHandler(logger); + List infos = new LinkedList(); + String resourceLocation = null; + try { + for (int i = 0; i < persistenceXmlLocations.length; i++) { + Resource[] resources = this.resourcePatternResolver.getResources(persistenceXmlLocations[i]); + for (Resource resource : resources) { + resourceLocation = resource.toString(); + InputStream stream = resource.getInputStream(); + try { + Document document = validateResource(handler, stream); + parseDocument(resource, document, infos); + } + finally { + stream.close(); + } + } + } + } + catch (IOException ex) { + throw new IllegalArgumentException("Cannot parse persistence unit from " + resourceLocation, ex); + } + catch (SAXException ex) { + throw new IllegalArgumentException("Invalid XML in persistence unit from " + resourceLocation, ex); + } + catch (ParserConfigurationException ex) { + throw new IllegalArgumentException("Internal error parsing persistence unit from " + resourceLocation); + } + + return infos.toArray(new SpringPersistenceUnitInfo[infos.size()]); + } + + + /** + * Validate the given stream and return a valid DOM document for parsing. + */ + protected Document validateResource(ErrorHandler handler, InputStream stream) + throws ParserConfigurationException, SAXException, IOException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + // Set schema location only if we found one inside the classpath. + Resource schemaLocation = findSchemaResource(); + if (schemaLocation != null) { + if (logger.isDebugEnabled()) { + logger.debug("Found schema resource: " + schemaLocation.getURL()); + } + dbf.setValidating(true); + dbf.setAttribute(JAXP_SCHEMA_LANGUAGE, XMLConstants.W3C_XML_SCHEMA_NS_URI); + dbf.setAttribute(JAXP_SCHEMA_SOURCE, schemaLocation.getURL().toString()); + } + else { + logger.debug("Schema resource [" + SCHEMA_NAME + + "] not found - falling back to XML parsing without schema validation"); + } + + DocumentBuilder parser = dbf.newDocumentBuilder(); + parser.setErrorHandler(handler); + return parser.parse(stream); + } + + /** + * Try to locate the schema first in the class path before using the URL specified inside the XML. + * @return an existing resource, or null if none found + */ + protected Resource findSchemaResource() { + for (int i = 0; i < SCHEMA_RESOURCE_LOCATIONS.length; i++) { + Resource schemaLocation = this.resourcePatternResolver.getResource(SCHEMA_RESOURCE_LOCATIONS[i]); + if (schemaLocation.exists()) { + return schemaLocation; + } + } + return null; + } + + + /** + * Parse the validated document and add entries to the given unit info list. + */ + protected List parseDocument( + Resource resource, Document document, List infos) throws IOException { + + Element persistence = document.getDocumentElement(); + URL unitRootURL = determinePersistenceUnitRootUrl(resource); + List units = (List) DomUtils.getChildElementsByTagName(persistence, PERSISTENCE_UNIT); + for (Element unit : units) { + SpringPersistenceUnitInfo info = parsePersistenceUnitInfo(unit); + info.setPersistenceUnitRootUrl(unitRootURL); + infos.add(info); + } + + return infos; + } + + /** + * Determine the persistence unit root URL based on the given resource + * (which points to the persistence.xml file we're reading). + * @param resource the resource to check + * @return the corresponding persistence unit root URL + * @throws IOException if the checking failed + */ + protected URL determinePersistenceUnitRootUrl(Resource resource) throws IOException { + URL originalURL = resource.getURL(); + String urlToString = originalURL.toExternalForm(); + + // If we get an archive, simply return the jar URL (section 6.2 from the JPA spec) + if (ResourceUtils.isJarURL(originalURL)) { + return ResourceUtils.extractJarFileURL(originalURL); + } + + else { + // check META-INF folder + if (!urlToString.contains(META_INF)) { + if (logger.isInfoEnabled()) { + logger.info(resource.getFilename() + + " should be located inside META-INF directory; cannot determine persistence unit root URL for " + + resource); + } + return null; + } + if (urlToString.lastIndexOf(META_INF) == urlToString.lastIndexOf('/') - (1 + META_INF.length())) { + if (logger.isInfoEnabled()) { + logger.info(resource.getFilename() + + " is not located in the root of META-INF directory; cannot determine persistence unit root URL for " + + resource); + } + return null; + } + + String persistenceUnitRoot = urlToString.substring(0, urlToString.lastIndexOf(META_INF)); + return new URL(persistenceUnitRoot); + } + } + + /** + * Parse the unit info DOM element. + */ + protected SpringPersistenceUnitInfo parsePersistenceUnitInfo(Element persistenceUnit) throws IOException { + SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo(); + + // set unit name + unitInfo.setPersistenceUnitName(persistenceUnit.getAttribute(UNIT_NAME).trim()); + + // set transaction type + String txType = persistenceUnit.getAttribute(TRANSACTION_TYPE).trim(); + if (StringUtils.hasText(txType)) { + unitInfo.setTransactionType(PersistenceUnitTransactionType.valueOf(txType)); + } + + // data-source + String jtaDataSource = DomUtils.getChildElementValueByTagName(persistenceUnit, JTA_DATA_SOURCE); + if (StringUtils.hasText(jtaDataSource)) { + unitInfo.setJtaDataSource(this.dataSourceLookup.getDataSource(jtaDataSource.trim())); + } + + String nonJtaDataSource = DomUtils.getChildElementValueByTagName(persistenceUnit, NON_JTA_DATA_SOURCE); + if (StringUtils.hasText(nonJtaDataSource)) { + unitInfo.setNonJtaDataSource(this.dataSourceLookup.getDataSource(nonJtaDataSource.trim())); + } + + // provider + String provider = DomUtils.getChildElementValueByTagName(persistenceUnit, PROVIDER); + if (StringUtils.hasText(provider)) { + unitInfo.setPersistenceProviderClassName(provider.trim()); + } + + // exclude unlisted classes + Element excludeUnlistedClasses = DomUtils.getChildElementByTagName(persistenceUnit, EXCLUDE_UNLISTED_CLASSES); + if (excludeUnlistedClasses != null) { + unitInfo.setExcludeUnlistedClasses(true); + } + + parseMappingFiles(persistenceUnit, unitInfo); + parseJarFiles(persistenceUnit, unitInfo); + parseClass(persistenceUnit, unitInfo); + parseProperty(persistenceUnit, unitInfo); + + return unitInfo; + } + + /** + * Parse the property XML elements. + */ + @SuppressWarnings("unchecked") + protected void parseProperty(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES); + if (propRoot == null) { + return; + } + List properties = DomUtils.getChildElementsByTagName(propRoot, "property"); + for (Element property : properties) { + String name = property.getAttribute("name"); + String value = property.getAttribute("value"); + unitInfo.addProperty(name, value); + } + } + + /** + * Parse the class XML elements. + */ + @SuppressWarnings("unchecked") + protected void parseClass(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + List classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME); + for (Element element : classes) { + String value = DomUtils.getTextValue(element).trim(); + if (StringUtils.hasText(value)) + unitInfo.addManagedClassName(value); + } + } + + /** + * Parse the jar-file XML elements. + */ + @SuppressWarnings("unchecked") + protected void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException { + List jars = DomUtils.getChildElementsByTagName(persistenceUnit, JAR_FILE_URL); + for (Element element : jars) { + String value = DomUtils.getTextValue(element).trim(); + if (StringUtils.hasText(value)) { + Resource[] resources = this.resourcePatternResolver.getResources(value); + for (int i = 0; i < resources.length; i++) { + unitInfo.addJarFileUrl(resources[i].getURL()); + } + } + } + } + + /** + * Parse the mapping-file XML elements. + */ + @SuppressWarnings("unchecked") + protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + List files = DomUtils.getChildElementsByTagName(persistenceUnit, MAPPING_FILE_NAME); + for (Element element : files) { + String value = DomUtils.getTextValue(element).trim(); + if (StringUtils.hasText(value)) { + unitInfo.addMappingFileName(value); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java new file mode 100644 index 00000000000..b9ffd62ff3c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2008 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.persistenceunit; + +import javax.persistence.spi.ClassTransformer; + +import org.springframework.core.DecoratingClassLoader; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; +import org.springframework.util.Assert; + +/** + * Subclass of {@link MutablePersistenceUnitInfo} that adds instrumentation hooks based on + * Spring's {@link org.springframework.instrument.classloading.LoadTimeWeaver} abstraction. + * + *

This class is restricted to package visibility, in contrast to its superclass. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.0 + * @see PersistenceUnitManager + */ +class SpringPersistenceUnitInfo extends MutablePersistenceUnitInfo { + + private LoadTimeWeaver loadTimeWeaver; + + private ClassLoader classLoader; + + + /** + * Initialize this PersistenceUnitInfo with the LoadTimeWeaver SPI interface + * used by Spring to add instrumentation to the current class loader. + */ + public void init(LoadTimeWeaver loadTimeWeaver) { + Assert.notNull(loadTimeWeaver, "LoadTimeWeaver must not be null"); + this.loadTimeWeaver = loadTimeWeaver; + this.classLoader = loadTimeWeaver.getInstrumentableClassLoader(); + } + + /** + * Initialize this PersistenceUnitInfo with the current class loader + * (instead of with a LoadTimeWeaver). + */ + public void init(ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + } + + + /** + * This implementation returns the LoadTimeWeaver's instrumentable ClassLoader, + * if specified. + */ + public ClassLoader getClassLoader() { + return this.classLoader; + } + + /** + * This implementation delegates to the LoadTimeWeaver, if specified. + */ + public void addTransformer(ClassTransformer classTransformer) { + if (this.loadTimeWeaver == null) { + throw new IllegalStateException("Cannot apply class transformer without LoadTimeWeaver specified"); + } + this.loadTimeWeaver.addTransformer(new ClassFileTransformerAdapter(classTransformer)); + } + + /** + * This implementation delegates to the LoadTimeWeaver, if specified. + */ + public ClassLoader getNewTempClassLoader() { + ClassLoader tcl = (this.loadTimeWeaver != null ? this.loadTimeWeaver.getThrowawayClassLoader() : + new SimpleThrowawayClassLoader(this.classLoader)); + String packageToExclude = getPersistenceProviderPackageName(); + if (packageToExclude != null && tcl instanceof DecoratingClassLoader) { + ((DecoratingClassLoader) tcl).excludePackage(packageToExclude); + } + return tcl; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package.html new file mode 100644 index 00000000000..5eab4decae0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package.html @@ -0,0 +1,7 @@ + + + +Internal support for managing JPA persistence units. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/JpaDaoSupport.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/JpaDaoSupport.java new file mode 100644 index 00000000000..ff18ed783c4 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/JpaDaoSupport.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2008 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.support; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.springframework.dao.support.DaoSupport; +import org.springframework.orm.jpa.JpaTemplate; + +/** + * Convenient super class for JPA data access objects. Intended for + * JpaTemplate usage. Alternatively, JPA-based DAOs can be coded + * against the plain JPA EntityManagerFactory/EntityManager API. + * + *

Requires an EntityManagerFactory or EntityManager to be set, + * providing a JpaTemplate based on it to subclasses. Can alternatively + * be initialized directly via a JpaTemplate, to reuse the latter's + * settings such as the EntityManagerFactory, JpaDialect, flush mode, etc. + * + *

This class will create its own JpaTemplate if an EntityManagerFactory + * or EntityManager reference is passed in. A custom JpaTemplate instance + * can be used through overriding createJpaTemplate. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setEntityManagerFactory + * @see #setEntityManager + * @see #createJpaTemplate + * @see #setJpaTemplate + * @see org.springframework.orm.jpa.JpaTemplate + */ +public abstract class JpaDaoSupport extends DaoSupport { + + private JpaTemplate jpaTemplate; + + + /** + * Set the JPA EntityManagerFactory to be used by this DAO. + * Will automatically create a JpaTemplate for the given EntityManagerFactory. + * @see #createJpaTemplate + * @see #setJpaTemplate + */ + public final void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { + if (this.jpaTemplate == null || entityManagerFactory != this.jpaTemplate.getEntityManagerFactory()) { + this.jpaTemplate = createJpaTemplate(entityManagerFactory); + } + } + + /** + * Create a JpaTemplate for the given EntityManagerFactory. + * Only invoked if populating the DAO with a EntityManagerFactory reference! + *

Can be overridden in subclasses to provide a JpaTemplate instance + * with different configuration, or a custom JpaTemplate subclass. + * @param entityManagerFactory the JPA EntityManagerFactory to create a JpaTemplate for + * @return the new JpaTemplate instance + * @see #setEntityManagerFactory + */ + protected JpaTemplate createJpaTemplate(EntityManagerFactory entityManagerFactory) { + return new JpaTemplate(entityManagerFactory); + } + + /** + * Set the JPA EntityManager to be used by this DAO. + * Will automatically create a JpaTemplate for the given EntityManager. + * @see #createJpaTemplate + * @see #setJpaTemplate + */ + public final void setEntityManager(EntityManager entityManager) { + this.jpaTemplate = createJpaTemplate(entityManager); + } + + /** + * Create a JpaTemplate for the given EntityManager. + * Only invoked if populating the DAO with a EntityManager reference! + *

Can be overridden in subclasses to provide a JpaTemplate instance + * with different configuration, or a custom JpaTemplate subclass. + * @param entityManager the JPA EntityManager to create a JpaTemplate for + * @return the new JpaTemplate instance + * @see #setEntityManagerFactory + */ + protected JpaTemplate createJpaTemplate(EntityManager entityManager) { + return new JpaTemplate(entityManager); + } + + /** + * Set the JpaTemplate for this DAO explicitly, + * as an alternative to specifying a EntityManagerFactory. + * @see #setEntityManagerFactory + */ + public final void setJpaTemplate(JpaTemplate jpaTemplate) { + this.jpaTemplate = jpaTemplate; + } + + /** + * Return the JpaTemplate for this DAO, pre-initialized + * with the EntityManagerFactory or set explicitly. + */ + public final JpaTemplate getJpaTemplate() { + return jpaTemplate; + } + + protected final void checkDaoConfig() { + if (this.jpaTemplate == null) { + throw new IllegalArgumentException("entityManagerFactory or jpaTemplate is required"); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java new file mode 100644 index 00000000000..d09a997153d --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2008 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.support; + +import java.io.IOException; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Servlet 2.3 Filter that binds a JPA EntityManager to the thread for the + * entire processing of the request. Intended for the "Open EntityManager in + * View" pattern, i.e. to allow for lazy loading in web views despite the + * original transactions already being completed. + * + *

This filter makes JPA EntityManagers available via the current thread, + * which will be autodetected by transaction managers. It is suitable for service + * layer transactions via {@link org.springframework.orm.jpa.JpaTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager} as well + * as for non-transactional read-only execution. + * + *

Looks up the EntityManagerFactory in Spring's root web application context. + * Supports a "entityManagerFactoryBeanName" filter init-param in web.xml; + * the default bean name is "entityManagerFactory". Looks up the EntityManagerFactory + * on each request, to avoid initialization order issues (when using ContextLoaderServlet, + * the root application context will get initialized after this filter). + * + * @author Juergen Hoeller + * @since 2.0 + * @see OpenEntityManagerInViewInterceptor + * @see org.springframework.orm.jpa.JpaInterceptor + * @see org.springframework.orm.jpa.JpaTransactionManager + * @see org.springframework.orm.jpa.JpaTemplate#execute + * @see org.springframework.orm.jpa.SharedEntityManagerCreator + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class OpenEntityManagerInViewFilter extends OncePerRequestFilter { + + public static final String DEFAULT_PERSISTENCE_MANAGER_FACTORY_BEAN_NAME = "entityManagerFactory"; + + private String entityManagerFactoryBeanName = DEFAULT_PERSISTENCE_MANAGER_FACTORY_BEAN_NAME; + + + /** + * Set the bean name of the EntityManagerFactory to fetch from Spring's + * root application context. Default is "entityManagerFactory". + * @see #DEFAULT_PERSISTENCE_MANAGER_FACTORY_BEAN_NAME + */ + public void setEntityManagerFactoryBeanName(String entityManagerFactoryBeanName) { + this.entityManagerFactoryBeanName = entityManagerFactoryBeanName; + } + + /** + * Return the bean name of the EntityManagerFactory to fetch from Spring's + * root application context. + */ + protected String getEntityManagerFactoryBeanName() { + return this.entityManagerFactoryBeanName; + } + + + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + EntityManagerFactory emf = lookupEntityManagerFactory(request); + boolean participate = false; + + if (TransactionSynchronizationManager.hasResource(emf)) { + // Do not modify the EntityManager: just set the participate flag. + participate = true; + } + else { + logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewFilter"); + try { + EntityManager em = createEntityManager(emf); + TransactionSynchronizationManager.bindResource(emf, new EntityManagerHolder(em)); + } + catch (PersistenceException ex) { + throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex); + } + } + + try { + filterChain.doFilter(request, response); + } + + finally { + if (!participate) { + EntityManagerHolder emHolder = (EntityManagerHolder) + TransactionSynchronizationManager.unbindResource(emf); + logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewFilter"); + EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); + } + } + } + + /** + * Look up the EntityManagerFactory that this filter should use, + * taking the current HTTP request as argument. + *

Default implementation delegates to the lookupEntityManagerFactory + * without arguments. + * @return the EntityManagerFactory to use + * @see #lookupEntityManagerFactory() + */ + protected EntityManagerFactory lookupEntityManagerFactory(HttpServletRequest request) { + return lookupEntityManagerFactory(); + } + + /** + * Look up the EntityManagerFactory that this filter should use. + * The default implementation looks for a bean with the specified name + * in Spring's root application context. + * @return the EntityManagerFactory to use + * @see #getEntityManagerFactoryBeanName + */ + protected EntityManagerFactory lookupEntityManagerFactory() { + if (logger.isDebugEnabled()) { + logger.debug("Using EntityManagerFactory '" + getEntityManagerFactoryBeanName() + + "' for OpenEntityManagerInViewFilter"); + } + WebApplicationContext wac = + WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + return (EntityManagerFactory) + wac.getBean(getEntityManagerFactoryBeanName(), EntityManagerFactory.class); + } + + /** + * Create a JPA EntityManager to be bound to a request. + *

Can be overridden in subclasses. + * @param emf the EntityManagerFactory to use + * @see javax.persistence.EntityManagerFactory#createEntityManager() + */ + protected EntityManager createEntityManager(EntityManagerFactory emf) { + return emf.createEntityManager(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java new file mode 100644 index 00000000000..c17a40da348 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2008 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.support; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.jpa.EntityManagerFactoryAccessor; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.ui.ModelMap; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.context.request.WebRequestInterceptor; + +/** + * Spring web request interceptor that binds a JPA EntityManager to the + * thread for the entire processing of the request. Intended for the "Open + * EntityManager in View" pattern, i.e. to allow for lazy loading in + * web views despite the original transactions already being completed. + * + *

This interceptor makes JPA EntityManagers available via the current thread, + * which will be autodetected by transaction managers. It is suitable for service + * layer transactions via {@link org.springframework.orm.jpa.JpaTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager} as well + * as for non-transactional read-only execution. + * + *

In contrast to {@link OpenEntityManagerInViewFilter}, this interceptor + * is set up in a Spring application context and can thus take advantage of + * bean wiring. It inherits common JPA configuration properties from + * {@link org.springframework.orm.jpa.JpaAccessor}, to be configured in a + * bean definition. + * + * @author Juergen Hoeller + * @since 2.0 + * @see OpenEntityManagerInViewFilter + * @see org.springframework.orm.jpa.JpaInterceptor + * @see org.springframework.orm.jpa.JpaTransactionManager + * @see org.springframework.orm.jpa.JpaTemplate#execute + * @see org.springframework.orm.jpa.SharedEntityManagerCreator + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements WebRequestInterceptor { + + /** + * Suffix that gets appended to the EntityManagerFactory toString + * representation for the "participate in existing entity manager + * handling" request attribute. + * @see #getParticipateAttributeName + */ + public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE"; + + + public void preHandle(WebRequest request) throws DataAccessException { + if (TransactionSynchronizationManager.hasResource(getEntityManagerFactory())) { + // do not modify the EntityManager: just mark the request accordingly + String participateAttributeName = getParticipateAttributeName(); + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + int newCount = (count != null) ? count.intValue() + 1 : 1; + request.setAttribute(getParticipateAttributeName(), new Integer(newCount), WebRequest.SCOPE_REQUEST); + } + else { + logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor"); + try { + EntityManager em = createEntityManager(); + TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), new EntityManagerHolder(em)); + } + catch (PersistenceException ex) { + throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex); + } + } + } + + public void postHandle(WebRequest request, ModelMap model) { + } + + public void afterCompletion(WebRequest request, Exception ex) throws DataAccessException { + String participateAttributeName = getParticipateAttributeName(); + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + if (count != null) { + // Do not modify the EntityManager: just clear the marker. + if (count.intValue() > 1) { + request.setAttribute(participateAttributeName, new Integer(count.intValue() - 1), WebRequest.SCOPE_REQUEST); + } + else { + request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + } + } + else { + EntityManagerHolder emHolder = (EntityManagerHolder) + TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); + logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor"); + EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); + } + } + + /** + * Return the name of the request attribute that identifies that a request is + * already filtered. Default implementation takes the toString representation + * of the EntityManagerFactory instance and appends ".FILTERED". + * @see #PARTICIPATE_SUFFIX + */ + protected String getParticipateAttributeName() { + return getEntityManagerFactory().toString() + PARTICIPATE_SUFFIX; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java new file mode 100644 index 00000000000..3ba4a0d35e3 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java @@ -0,0 +1,638 @@ +/* + * Copyright 2002-2008 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.support; + +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.NamingException; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import javax.persistence.PersistenceProperty; +import javax.persistence.PersistenceUnit; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.orm.jpa.EntityManagerFactoryInfo; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.orm.jpa.EntityManagerProxy; +import org.springframework.orm.jpa.ExtendedEntityManagerCreator; +import org.springframework.orm.jpa.SharedEntityManagerCreator; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * BeanPostProcessor that processes {@link javax.persistence.PersistenceUnit} + * and {@link javax.persistence.PersistenceContext} annotations, for injection of + * the corresponding JPA resources {@link javax.persistence.EntityManagerFactory} + * and {@link javax.persistence.EntityManager}. Any such annotated fields or methods + * in any Spring-managed object will automatically be injected. + * + *

This post-processor will inject sub-interfaces of EntityManagerFactory + * and EntityManager if the annotated fields or methods are declared as such. + * The actual type will be verified early, with the exception of a shared ("transactional") + * EntityManager reference, where type mismatches might be detected as late + * as on the first actual invocation. + * + *

Note: In the present implementation, PersistenceAnnotationBeanPostProcessor + * only supports @PersistenceUnit and @PersistenceContext + * with the "unitName" attribute, or no attribute at all (for the default unit). + * If those annotations are present with the "name" attribute at the class level, + * they will simply be ignored, since those only serve as deployment hint + * (as per the Java EE 5 specification). + * + *

This post-processor can either obtain EntityManagerFactory beans defined + * in the Spring application context (the default), or obtain EntityManagerFactory + * references from JNDI ("persistence unit references"). In the bean case, + * the persistence unit name will be matched against the actual deployed unit, + * with the bean name used as fallback unit name if no deployed name found. + * Typically, Spring's {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean} + * will be used for setting up such EntityManagerFactory beans. Alternatively, + * such beans may also be obtained from JNDI, e.g. using the jee:jndi-lookup + * XML configuration element (with the bean name matching the requested unit name). + * In both cases, the post-processor definition will look as simple as this: + * + *

+ * <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
+ * + * In the JNDI case, specify the corresponding JNDI names in this post-processor's + * {@link #setPersistenceUnits "persistenceUnits" map}, typically with matching + * persistence-unit-ref entries in the Java EE deployment descriptor. + * By default, those names are considered as resource references (according to the + * Java EE resource-ref convention), located underneath the "java:comp/env/" namespace. + * For example: + * + *
+ * <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor">
+ *   <property name="persistenceUnits">
+ *     <map/gt;
+ *       <entry key="unit1" value="persistence/unit1"/>
+ *       <entry key="unit2" value="persistence/unit2"/>
+ *     </map/gt;
+ *   </property>
+ * </bean>
+ * + * In this case, the specified persistence units will always be resolved in JNDI + * rather than as Spring-defined beans. The entire persistence unit deployment, + * including the weaving of persistent classes, is then up to the Java EE server. + * Persistence contexts (i.e. EntityManager references) will be built based on + * those server-provided EntityManagerFactory references, using Spring's own + * transaction synchronization facilities for transactional EntityManager handling + * (typically with Spring's @Transactional annotation for demarcation + * and {@link org.springframework.transaction.jta.JtaTransactionManager} as backend). + * + *

If you prefer the Java EE server's own EntityManager handling, specify entries + * in this post-processor's {@link #setPersistenceContexts "persistenceContexts" map} + * (or {@link #setExtendedPersistenceContexts "extendedPersistenceContexts" map}, + * typically with matching persistence-context-ref entries in the + * Java EE deployment descriptor. For example: + * + *

+ * <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor">
+ *   <property name="persistenceContexts">
+ *     <map/gt;
+ *       <entry key="unit1" value="persistence/context1"/>
+ *       <entry key="unit2" value="persistence/context2"/>
+ *     </map/gt;
+ *   </property>
+ * </bean>
+ * + * If the application only obtains EntityManager references in the first place, + * this is all you need to specify. If you need EntityManagerFactory references + * as well, specify entries for both "persistenceUnits" and "persistenceContexts", + * pointing to matching JNDI locations. + * + *

NOTE: In general, do not inject EXTENDED EntityManagers into STATELESS beans, + * i.e. do not use @PersistenceContext with type EXTENDED in + * Spring beans defined with scope 'singleton' (Spring's default scope). + * Extended EntityManagers are not thread-safe, hence they must not be used + * in concurrently accessed beans (which Spring-managed singletons usually are). + * + *

Note: A default PersistenceAnnotationBeanPostProcessor will be registered + * by the "context:annotation-config" and "context:component-scan" XML tags. + * Remove or turn off the default annotation configuration there if you intend + * to specify a custom PersistenceAnnotationBeanPostProcessor bean definition. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see javax.persistence.PersistenceUnit + * @see javax.persistence.PersistenceContext + */ +public class PersistenceAnnotationBeanPostProcessor extends JndiLocatorSupport + implements InstantiationAwareBeanPostProcessor, DestructionAwareBeanPostProcessor, + MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware, Serializable { + + private transient Map persistenceUnits; + + private transient Map persistenceContexts; + + private transient Map extendedPersistenceContexts; + + private transient String defaultPersistenceUnitName = ""; + + private int order = Ordered.LOWEST_PRECEDENCE - 4; + + private transient ListableBeanFactory beanFactory; + + private transient final Map, InjectionMetadata> injectionMetadataCache = + new ConcurrentHashMap, InjectionMetadata>(); + + private final Map extendedEntityManagersToClose = + new ConcurrentHashMap(); + + + public PersistenceAnnotationBeanPostProcessor() { + setResourceRef(true); + } + + + /** + * Specify the persistence units for EntityManagerFactory lookups, + * as a Map from persistence unit name to persistence unit JNDI name + * (which needs to resolve to an EntityManagerFactory instance). + *

JNDI names specified here should refer to persistence-unit-ref + * entries in the Java EE deployment descriptor, matching the target persistence unit. + *

In case of no unit name specified in the annotation, the specified value + * for the {@link #setDefaultPersistenceUnitName default persistence unit} + * will be taken (by default, the value mapped to the empty String), + * or simply the single persistence unit if there is only one. + *

This is mainly intended for use in a Java EE 5 environment, with all + * lookup driven by the standard JPA annotations, and all EntityManagerFactory + * references obtained from JNDI. No separate EntityManagerFactory bean + * definitions are necessary in such a scenario. + *

If no corresponding "persistenceContexts"/"extendedPersistenceContexts" + * are specified, @PersistenceContext will be resolved to + * EntityManagers built on top of the EntityManagerFactory defined here. + * Note that those will be Spring-managed EntityManagers, which implement + * transaction synchronization based on Spring's facilities. + * If you prefer the Java EE 5 server's own EntityManager handling, + * specify corresponding "persistenceContexts"/"extendedPersistenceContexts". + */ + public void setPersistenceUnits(Map persistenceUnits) { + this.persistenceUnits = persistenceUnits; + } + + /** + * Specify the transactional persistence contexts for EntityManager lookups, + * as a Map from persistence unit name to persistence context JNDI name + * (which needs to resolve to an EntityManager instance). + *

JNDI names specified here should refer to persistence-context-ref + * entries in the Java EE deployment descriptors, matching the target persistence unit + * and being set up with persistence context type Transaction. + *

In case of no unit name specified in the annotation, the specified value + * for the {@link #setDefaultPersistenceUnitName default persistence unit} + * will be taken (by default, the value mapped to the empty String), + * or simply the single persistence unit if there is only one. + *

This is mainly intended for use in a Java EE 5 environment, with all + * lookup driven by the standard JPA annotations, and all EntityManager + * references obtained from JNDI. No separate EntityManagerFactory bean + * definitions are necessary in such a scenario, and all EntityManager + * handling is done by the Java EE 5 server itself. + */ + public void setPersistenceContexts(Map persistenceContexts) { + this.persistenceContexts = persistenceContexts; + } + + /** + * Specify the extended persistence contexts for EntityManager lookups, + * as a Map from persistence unit name to persistence context JNDI name + * (which needs to resolve to an EntityManager instance). + *

JNDI names specified here should refer to persistence-context-ref + * entries in the Java EE deployment descriptors, matching the target persistence unit + * and being set up with persistence context type Extended. + *

In case of no unit name specified in the annotation, the specified value + * for the {@link #setDefaultPersistenceUnitName default persistence unit} + * will be taken (by default, the value mapped to the empty String), + * or simply the single persistence unit if there is only one. + *

This is mainly intended for use in a Java EE 5 environment, with all + * lookup driven by the standard JPA annotations, and all EntityManager + * references obtained from JNDI. No separate EntityManagerFactory bean + * definitions are necessary in such a scenario, and all EntityManager + * handling is done by the Java EE 5 server itself. + */ + public void setExtendedPersistenceContexts(Map extendedPersistenceContexts) { + this.extendedPersistenceContexts = extendedPersistenceContexts; + } + + /** + * Specify the default persistence unit name, to be used in case + * of no unit name specified in an @PersistenceUnit / + * @PersistenceContext annotation. + *

This is mainly intended for lookups in the application context, + * indicating the target persistence unit name (typically matching + * the bean name), but also applies to lookups in the + * {@link #setPersistenceUnits "persistenceUnits"} / + * {@link #setPersistenceContexts "persistenceContexts"} / + * {@link #setExtendedPersistenceContexts "extendedPersistenceContexts"} map, + * avoiding the need for duplicated mappings for the empty String there. + *

Default is to check for a single EntityManagerFactory bean + * in the Spring application context, if any. If there are multiple + * such factories, either specify this default persistence unit name + * or explicitly refer to named persistence units in your annotations. + */ + public void setDefaultPersistenceUnitName(String unitName) { + this.defaultPersistenceUnitName = (unitName != null ? unitName : ""); + } + + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return this.order; + } + + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ListableBeanFactory) { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + } + + + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (beanType != null) { + InjectionMetadata metadata = findPersistenceMetadata(beanType); + metadata.checkConfigMembers(beanDefinition); + } + } + + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + return null; + } + + public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { + InjectionMetadata metadata = findPersistenceMetadata(bean.getClass()); + try { + metadata.injectFields(bean, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of persistence fields failed", ex); + } + return true; + } + + public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { + + InjectionMetadata metadata = findPersistenceMetadata(bean.getClass()); + try { + metadata.injectMethods(bean, beanName, pvs); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of persistence methods failed", ex); + } + return pvs; + } + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + EntityManager emToClose = this.extendedEntityManagersToClose.remove(bean); + EntityManagerFactoryUtils.closeEntityManager(emToClose); + } + + + private InjectionMetadata findPersistenceMetadata(final Class clazz) { + // Quick check on the concurrent map first, with minimal locking. + InjectionMetadata metadata = this.injectionMetadataCache.get(clazz); + if (metadata == null) { + synchronized (this.injectionMetadataCache) { + metadata = this.injectionMetadataCache.get(clazz); + if (metadata == null) { + final InjectionMetadata newMetadata = new InjectionMetadata(clazz); + ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() { + public void doWith(Field field) { + PersistenceContext pc = field.getAnnotation(PersistenceContext.class); + PersistenceUnit pu = field.getAnnotation(PersistenceUnit.class); + if (pc != null || pu != null) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("Persistence annotations are not supported on static fields"); + } + newMetadata.addInjectedField(new PersistenceElement(field, null)); + } + } + }); + ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + PersistenceContext pc = method.getAnnotation(PersistenceContext.class); + PersistenceUnit pu = method.getAnnotation(PersistenceUnit.class); + if (pc != null || pu != null && + method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("Persistence annotations are not supported on static methods"); + } + if (method.getParameterTypes().length != 1) { + throw new IllegalStateException("Persistence annotation requires a single-arg method: " + method); + } + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + newMetadata.addInjectedMethod(new PersistenceElement(method, pd)); + } + } + }); + metadata = newMetadata; + this.injectionMetadataCache.put(clazz, metadata); + } + } + } + return metadata; + } + + /** + * Return a specified persistence unit for the given unit name, + * as defined through the "persistenceUnits" map. + * @param unitName the name of the persistence unit + * @return the corresponding EntityManagerFactory, + * or null if none found + * @see #setPersistenceUnits + */ + protected EntityManagerFactory getPersistenceUnit(String unitName) { + if (this.persistenceUnits != null) { + String unitNameForLookup = (unitName != null ? unitName : ""); + if ("".equals(unitNameForLookup)) { + unitNameForLookup = this.defaultPersistenceUnitName; + } + String jndiName = this.persistenceUnits.get(unitNameForLookup); + if (jndiName == null && "".equals(unitNameForLookup) && this.persistenceUnits.size() == 1) { + jndiName = this.persistenceUnits.values().iterator().next(); + } + if (jndiName != null) { + try { + return (EntityManagerFactory) lookup(jndiName, EntityManagerFactory.class); + } + catch (NamingException ex) { + throw new IllegalStateException("Could not obtain EntityManagerFactory [" + jndiName + "] from JNDI", ex); + } + } + } + return null; + } + + /** + * Return a specified persistence context for the given unit name, as defined + * through the "persistenceContexts" (or "extendedPersistenceContexts") map. + * @param unitName the name of the persistence unit + * @param extended whether to obtain an extended persistence context + * @return the corresponding EntityManager, or null if none found + * @see #setPersistenceContexts + * @see #setExtendedPersistenceContexts + */ + protected EntityManager getPersistenceContext(String unitName, boolean extended) { + Map contexts = (extended ? this.extendedPersistenceContexts : this.persistenceContexts); + if (contexts != null) { + String unitNameForLookup = (unitName != null ? unitName : ""); + if ("".equals(unitNameForLookup)) { + unitNameForLookup = this.defaultPersistenceUnitName; + } + String jndiName = contexts.get(unitNameForLookup); + if (jndiName == null && "".equals(unitNameForLookup) && contexts.size() == 1) { + jndiName = contexts.values().iterator().next(); + } + if (jndiName != null) { + try { + return (EntityManager) lookup(jndiName, EntityManager.class); + } + catch (NamingException ex) { + throw new IllegalStateException("Could not obtain EntityManager [" + jndiName + "] from JNDI", ex); + } + } + } + return null; + } + + /** + * Find an EntityManagerFactory with the given name in the current Spring + * application context, falling back to a single default EntityManagerFactory + * (if any) in case of no unit name specified. + * @param unitName the name of the persistence unit (may be null or empty) + * @param requestingBeanName the name of the requesting bean + * @return the EntityManagerFactory + * @throws NoSuchBeanDefinitionException if there is no such EntityManagerFactory in the context + */ + protected EntityManagerFactory findEntityManagerFactory(String unitName, String requestingBeanName) + throws NoSuchBeanDefinitionException { + + if (this.beanFactory == null) { + throw new IllegalStateException("ListableBeanFactory required for EntityManagerFactory bean lookup"); + } + String unitNameForLookup = (unitName != null ? unitName : ""); + if ("".equals(unitNameForLookup)) { + unitNameForLookup = this.defaultPersistenceUnitName; + } + if (!"".equals(unitNameForLookup)) { + return findNamedEntityManagerFactory(unitNameForLookup, requestingBeanName); + } + else { + return findDefaultEntityManagerFactory(requestingBeanName); + } + } + + /** + * Find an EntityManagerFactory with the given name in the current + * Spring application context. + * @param unitName the name of the persistence unit (never empty) + * @param requestingBeanName the name of the requesting bean + * @return the EntityManagerFactory + * @throws NoSuchBeanDefinitionException if there is no such EntityManagerFactory in the context + */ + protected EntityManagerFactory findNamedEntityManagerFactory(String unitName, String requestingBeanName) + throws NoSuchBeanDefinitionException { + + EntityManagerFactory emf = EntityManagerFactoryUtils.findEntityManagerFactory(this.beanFactory, unitName); + if (this.beanFactory instanceof ConfigurableBeanFactory) { + ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(unitName, requestingBeanName); + } + return emf; + } + + /** + * Find a single default EntityManagerFactory in the Spring application context. + * @return the default EntityManagerFactory + * @throws NoSuchBeanDefinitionException if there is no single EntityManagerFactory in the context + */ + protected EntityManagerFactory findDefaultEntityManagerFactory(String requestingBeanName) + throws NoSuchBeanDefinitionException{ + + String[] beanNames = + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, EntityManagerFactory.class); + if (beanNames.length == 1) { + String unitName = beanNames[0]; + EntityManagerFactory emf = (EntityManagerFactory) this.beanFactory.getBean(unitName); + if (this.beanFactory instanceof ConfigurableBeanFactory) { + ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(unitName, requestingBeanName); + } + return emf; + } + else { + throw new NoSuchBeanDefinitionException( + EntityManagerFactory.class, "expected single bean but found " + beanNames.length); + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method. + */ + private class PersistenceElement extends InjectionMetadata.InjectedElement { + + private final String unitName; + + private PersistenceContextType type; + + private Properties properties; + + public PersistenceElement(Member member, PropertyDescriptor pd) { + super(member, pd); + AnnotatedElement ae = (AnnotatedElement) member; + PersistenceContext pc = ae.getAnnotation(PersistenceContext.class); + PersistenceUnit pu = ae.getAnnotation(PersistenceUnit.class); + Class resourceType = EntityManager.class; + if (pc != null) { + if (pu != null) { + throw new IllegalStateException("Member may only be annotated with either " + + "@PersistenceContext or @PersistenceUnit, not both: " + member); + } + Properties properties = null; + PersistenceProperty[] pps = pc.properties(); + if (!ObjectUtils.isEmpty(pps)) { + properties = new Properties(); + for (int i = 0; i < pps.length; i++) { + PersistenceProperty pp = pps[i]; + properties.setProperty(pp.name(), pp.value()); + } + } + this.unitName = pc.unitName(); + this.type = pc.type(); + this.properties = properties; + } + else { + resourceType = EntityManagerFactory.class; + this.unitName = pu.unitName(); + } + checkResourceType(resourceType); + } + + /** + * Resolve the object against the application context. + */ + @Override + protected Object getResourceToInject(Object target, String requestingBeanName) { + // Resolves to EntityManagerFactory or EntityManager. + if (this.type != null) { + return (this.type == PersistenceContextType.EXTENDED ? + resolveExtendedEntityManager(target, requestingBeanName) : + resolveEntityManager(requestingBeanName)); + } + else { + // OK, so we need an EntityManagerFactory... + return resolveEntityManagerFactory(requestingBeanName); + } + } + + private EntityManagerFactory resolveEntityManagerFactory(String requestingBeanName) { + // Obtain EntityManagerFactory from JNDI? + EntityManagerFactory emf = getPersistenceUnit(this.unitName); + if (emf == null) { + // Need to search for EntityManagerFactory beans. + emf = findEntityManagerFactory(this.unitName, requestingBeanName); + } + return emf; + } + + private EntityManager resolveEntityManager(String requestingBeanName) { + // Obtain EntityManager reference from JNDI? + EntityManager em = getPersistenceContext(this.unitName, false); + if (em == null) { + // No pre-built EntityManager found -> build one based on factory. + // Obtain EntityManagerFactory from JNDI? + EntityManagerFactory emf = getPersistenceUnit(this.unitName); + if (emf == null) { + // Need to search for EntityManagerFactory beans. + emf = findEntityManagerFactory(this.unitName, requestingBeanName); + } + // Inject a shared transactional EntityManager proxy. + if (emf instanceof EntityManagerFactoryInfo && + ((EntityManagerFactoryInfo) emf).getEntityManagerInterface() != null) { + // Create EntityManager based on the info's vendor-specific type + // (which might be more specific than the field's type). + em = SharedEntityManagerCreator.createSharedEntityManager(emf, this.properties); + } + else { + // Create EntityManager based on the field's type. + em = SharedEntityManagerCreator.createSharedEntityManager(emf, this.properties, getResourceType()); + } + } + return em; + } + + private EntityManager resolveExtendedEntityManager(Object target, String requestingBeanName) { + // Obtain EntityManager reference from JNDI? + EntityManager em = getPersistenceContext(this.unitName, true); + if (em == null) { + // No pre-built EntityManager found -> build one based on factory. + // Obtain EntityManagerFactory from JNDI? + EntityManagerFactory emf = getPersistenceUnit(this.unitName); + if (emf == null) { + // Need to search for EntityManagerFactory beans. + emf = findEntityManagerFactory(this.unitName, requestingBeanName); + } + // Inject a container-managed extended EntityManager. + em = ExtendedEntityManagerCreator.createContainerManagedEntityManager(emf, this.properties); + } + if (em instanceof EntityManagerProxy && + beanFactory != null && !beanFactory.isPrototype(requestingBeanName)) { + extendedEntityManagersToClose.put(target, ((EntityManagerProxy) em).getTargetEntityManager()); + } + return em; + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java new file mode 100644 index 00000000000..5d865c63f9d --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2008 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.support; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.orm.jpa.EntityManagerFactoryAccessor; +import org.springframework.orm.jpa.EntityManagerFactoryInfo; +import org.springframework.orm.jpa.EntityManagerPlus; +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.SharedEntityManagerCreator; +import org.springframework.util.Assert; + +/** + * {@link FactoryBean} that exposes a shared JPA {@link javax.persistence.EntityManager} + * reference for a given EntityManagerFactory. Typically used for an EntityManagerFactory + * created by {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean}, + * as direct alternative to a JNDI lookup for a Java EE 5 server's EntityManager reference. + * + *

The shared EntityManager will behave just like an EntityManager fetched from an + * application server's JNDI environment, as defined by the JPA specification. + * It will delegate all calls to the current transactional EntityManager, if any; + * otherwise, it will fall back to a newly created EntityManager per operation. + * + *

Can be passed to DAOs that expect a shared EntityManager reference rather than an + * EntityManagerFactory. Note that Spring's {@link org.springframework.orm.jpa.JpaTransactionManager} + * always needs an EntityManagerFactory in order to create new transactional EntityManager instances. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setEntityManagerFactory + * @see #setEntityManagerInterface + * @see org.springframework.orm.jpa.LocalEntityManagerFactoryBean + * @see org.springframework.orm.jpa.JpaTransactionManager + */ +public class SharedEntityManagerBean extends EntityManagerFactoryAccessor implements FactoryBean, InitializingBean { + + private Class entityManagerInterface; + + private EntityManager shared; + + + /** + * Specify the EntityManager interface to expose. + *

Default is the the EntityManager interface as defined by the + * EntityManagerFactoryInfo, if available. Else, the standard + * javax.persistence.EntityManager interface will be used. + * @see org.springframework.orm.jpa.EntityManagerFactoryInfo#getEntityManagerInterface() + * @see javax.persistence.EntityManager + */ + public void setEntityManagerInterface(Class entityManagerInterface) { + Assert.notNull(entityManagerInterface, "entityManagerInterface must not be null"); + Assert.isAssignable(EntityManager.class, entityManagerInterface); + this.entityManagerInterface = entityManagerInterface; + } + + + public final void afterPropertiesSet() { + EntityManagerFactory emf = getEntityManagerFactory(); + if (emf == null) { + throw new IllegalArgumentException("entityManagerFactory is required"); + } + Class[] ifcs = null; + if (emf instanceof EntityManagerFactoryInfo) { + EntityManagerFactoryInfo emfInfo = (EntityManagerFactoryInfo) emf; + if (this.entityManagerInterface == null) { + this.entityManagerInterface = emfInfo.getEntityManagerInterface(); + if (this.entityManagerInterface == null) { + this.entityManagerInterface = EntityManager.class; + } + } + JpaDialect jpaDialect = emfInfo.getJpaDialect(); + if (jpaDialect != null && jpaDialect.supportsEntityManagerPlusOperations()) { + ifcs = new Class[] {this.entityManagerInterface, EntityManagerPlus.class}; + } + else { + ifcs = new Class[] {this.entityManagerInterface}; + } + } + else { + if (this.entityManagerInterface == null) { + this.entityManagerInterface = EntityManager.class; + } + ifcs = new Class[] {this.entityManagerInterface}; + } + this.shared = SharedEntityManagerCreator.createSharedEntityManager(emf, getJpaPropertyMap(), ifcs); + } + + + public EntityManager getObject() { + return this.shared; + } + + public Class getObjectType() { + return this.entityManagerInterface; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/package.html new file mode 100644 index 00000000000..8e750da9b1f --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.orm.jpa package. +Contains a DAO base class for JpaTemplate usage. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java new file mode 100644 index 00000000000..63e4c36b11f --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.util.Map; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.JpaVendorAdapter; + +/** + * Abstract {@link JpaVendorAdapter} implementation that defines common properties, + * to be translated into vendor-specific JPA properties by concrete subclasses. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + */ +public abstract class AbstractJpaVendorAdapter implements JpaVendorAdapter { + + private Database database = Database.DEFAULT; + + private String databasePlatform; + + private boolean generateDdl = false; + + private boolean showSql = false; + + + /** + * Specify the target database to operate on, as a value of the Database enum: + * DB2, DERBY, H2, HSQL, INFORMIX, MYSQL, ORACLE, POSTGRESQL, SQL_SERVER, SYBASE + */ + public void setDatabase(Database database) { + this.database = database; + } + + /** + * Return the target database to operate on. + */ + protected Database getDatabase() { + return this.database; + } + + /** + * Specify the name of the target database to operate on. + * The supported values are vendor-dependent platform identifiers. + */ + public void setDatabasePlatform(String databasePlatform) { + this.databasePlatform = databasePlatform; + } + + /** + * Return the name of the target database to operate on. + */ + protected String getDatabasePlatform() { + return this.databasePlatform; + } + + /** + * Set whether to generate DDL after the EntityManagerFactory has been initialized, + * creating/updating all relevant tables. + *

Note that the exact semantics of this flag depend on the underlying + * persistence provider. For any more advanced needs, specify the appropriate + * vendor-specific settings as "jpaProperties". + * @see org.springframework.orm.jpa.AbstractEntityManagerFactoryBean#setJpaProperties + */ + public void setGenerateDdl(boolean generateDdl) { + this.generateDdl = generateDdl; + } + + /** + * Return whether to generate DDL after the EntityManagerFactory has been initialized + * creating/updating all relevant tables. + */ + protected boolean isGenerateDdl() { + return this.generateDdl; + } + + /** + * Set whether to show SQL in the log (or in the console). + *

For more specific logging configuration, specify the appropriate + * vendor-specific settings as "jpaProperties". + * @see org.springframework.orm.jpa.AbstractEntityManagerFactoryBean#setJpaProperties + */ + public void setShowSql(boolean showSql) { + this.showSql = showSql; + } + + /** + * Return whether to show SQL in the log (or in the console). + */ + protected boolean isShowSql() { + return this.showSql; + } + + + public String getPersistenceProviderRootPackage() { + return null; + } + + public Map getJpaPropertyMap() { + return null; + } + + public JpaDialect getJpaDialect() { + return null; + } + + public Class getEntityManagerFactoryInterface() { + return EntityManagerFactory.class; + } + + public Class getEntityManagerInterface() { + return EntityManager.class; + } + + /** + * Post-process the EntityManagerFactory after it has been initialized. + * @param emf the EntityManagerFactory to process + */ + public void postProcessEntityManagerFactory(EntityManagerFactory emf) { + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/Database.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/Database.java new file mode 100644 index 00000000000..053acca358b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/Database.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2008 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.vendor; + +/** + * Enumeration for common database platforms. Allows strong typing of database type + * and portable configuration between JpaVendorDialect implementations. + * + *

If a given PersistenceProvider supports a database not listed here, + * the strategy class can still be specified using the fully-qualified class name. + * This enumeration is merely a convenience. The database products listed here + * are the same as those explicitly supported for Spring JDBC exception translation + * in sql-error-codes.xml. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see AbstractJpaVendorAdapter#setDatabase + */ +public enum Database { + + DEFAULT, + + DB2, + + DERBY, + + H2, + + HSQL, + + INFORMIX, + + MYSQL, + + ORACLE, + + POSTGRESQL, + + SQL_SERVER, + + SYBASE + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java new file mode 100644 index 00000000000..a3a5005727c --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; + +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.jpa.JpaEntityManager; +import org.eclipse.persistence.sessions.Session; +import org.eclipse.persistence.sessions.UnitOfWork; + +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.datasource.SimpleConnectionHandle; +import org.springframework.orm.jpa.DefaultJpaDialect; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * {@link org.springframework.orm.jpa.JpaDialect} implementation for Eclipse + * Persistence Services (EclipseLink). Developed and tested against EclipseLink 1.0. + * + *

By default, this class acquires a EclipseLink transaction to get the JDBC Connection + * early. This allows mixing JDBC and JPA/EclipseLink operations in the same transaction. + * In some cases, this eager acquisition of a transaction/connection may impact + * scalability. In that case, set the "lazyDatabaseTransaction" flag to true if you + * do not require mixing JDBC and JPA operations in the same transaction. Otherwise, + * use a {@link org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy} + * to ensure that the cost of connection acquisition is near zero until code actually + * needs a JDBC Connection. + * + *

This class is very analogous to {@link TopLinkJpaDialect}, since + * EclipseLink is effectively the next generation of the TopLink product. + * Thanks to Mike Keith for the original EclipseLink support prototype! + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #setLazyDatabaseTransaction + * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy + */ +public class EclipseLinkJpaDialect extends DefaultJpaDialect { + + private boolean lazyDatabaseTransaction = false; + + + /** + * Set whether to lazily start a database transaction within an + * EclipseLink transaction. + *

By default, database transactions are started early. This allows + * for reusing the same JDBC Connection throughout an entire transaction, + * including read operations, and also for exposing EclipseLink transactions + * to JDBC access code (working on the same DataSource). + *

It is only recommended to switch this flag to "true" when no JDBC access + * code is involved in any of the transactions, and when it is acceptable to + * perform read operations outside of the transactional JDBC Connection. + * @see oracle.toplink.sessions.UnitOfWork#beginEarlyTransaction() + */ + public void setLazyDatabaseTransaction(boolean lazyDatabaseTransaction) { + this.lazyDatabaseTransaction = lazyDatabaseTransaction; + } + + + @Override + public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + throws PersistenceException, SQLException, TransactionException { + + super.beginTransaction(entityManager, definition); + if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { + // This is the magic bit. As with the existing Spring TopLink integration, + // begin an early transaction to force EclipseLink to get a JDBC Connection + // so that Spring can manage transactions with JDBC as well as EclipseLink. + UnitOfWork uow = (UnitOfWork) getSession(entityManager); + uow.beginEarlyTransaction(); + } + // Could return the UOW, if there were any advantage in having it later. + return null; + } + + @Override + public ConnectionHandle getJdbcConnection(EntityManager em, boolean readOnly) + throws PersistenceException, SQLException { + + AbstractSession session = (AbstractSession) getSession(em); + // The connection was already acquired eagerly in beginTransaction, + // unless lazyDatabaseTransaction was set to true. + Connection con = session.getAccessor().getConnection(); + return (con != null ? new SimpleConnectionHandle(con) : null); + } + + /** + * Get a traditional EclipseLink Session from the given EntityManager. + */ + protected Session getSession(EntityManager em) { + JpaEntityManager emi = (JpaEntityManager) em; + return emi.getActiveSession(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java new file mode 100644 index 00000000000..759189e4527 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; + +import javax.persistence.EntityManager; +import javax.persistence.spi.PersistenceProvider; + +import org.eclipse.persistence.config.PersistenceUnitProperties; +import org.eclipse.persistence.config.TargetDatabase; +import org.eclipse.persistence.jpa.JpaEntityManager; + +import org.springframework.orm.jpa.JpaDialect; + +/** + * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for Eclipse + * Persistence Services (EclipseLink). Developed and tested against EclipseLink 1.0. + * + *

Exposes EclipseLink's persistence provider and EntityManager extension interface, + * and supports {@link AbstractJpaVendorAdapter}'s common configuration settings. + * + *

This class is very analogous to {@link TopLinkJpaVendorAdapter}, since + * EclipseLink is effectively the next generation of the TopLink product. + * Thanks to Mike Keith for the original EclipseLink support prototype! + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see org.eclipse.persistence.jpa.PersistenceProvider + * @see org.eclipse.persistence.jpa.JpaEntityManager + */ +public class EclipseLinkJpaVendorAdapter extends AbstractJpaVendorAdapter { + + private final PersistenceProvider persistenceProvider = new org.eclipse.persistence.jpa.PersistenceProvider(); + + private final JpaDialect jpaDialect = new EclipseLinkJpaDialect(); + + + public PersistenceProvider getPersistenceProvider() { + return this.persistenceProvider; + } + + public String getPersistenceProviderRootPackage() { + return "org.eclipse.persistence"; + } + + public Map getJpaPropertyMap() { + Properties jpaProperties = new Properties(); + + if (getDatabasePlatform() != null) { + jpaProperties.setProperty(PersistenceUnitProperties.TARGET_DATABASE, getDatabasePlatform()); + } + else if (getDatabase() != null) { + String targetDatabase = determineTargetDatabaseName(getDatabase()); + if (targetDatabase != null) { + jpaProperties.setProperty(PersistenceUnitProperties.TARGET_DATABASE, targetDatabase); + } + } + + if (isGenerateDdl()) { + jpaProperties.setProperty(PersistenceUnitProperties.DDL_GENERATION, + PersistenceUnitProperties.CREATE_ONLY); + jpaProperties.setProperty(PersistenceUnitProperties.DDL_GENERATION_MODE, + PersistenceUnitProperties.DDL_DATABASE_GENERATION); + } + if (isShowSql()) { + jpaProperties.setProperty(PersistenceUnitProperties.LOGGING_LEVEL, Level.FINE.toString()); + } + + return jpaProperties; + } + + /** + * Determine the EclipseLink target database name for the given database. + * @param database the specified database + * @return the EclipseLink target database name, or null if none found + */ + protected String determineTargetDatabaseName(Database database) { + switch (database) { + case DB2: return TargetDatabase.DB2; + case DERBY: return TargetDatabase.Derby; + case HSQL: return TargetDatabase.HSQL; + case INFORMIX: return TargetDatabase.Informix; + case MYSQL: return TargetDatabase.MySQL4; + case ORACLE: return TargetDatabase.Oracle; + case POSTGRESQL: return TargetDatabase.PostgreSQL; + case SQL_SERVER: return TargetDatabase.SQLServer; + case SYBASE: return TargetDatabase.Sybase; + default: return null; + } + } + + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + public Class getEntityManagerInterface() { + return JpaEntityManager.class; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java new file mode 100644 index 00000000000..74c9bac17c5 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2007 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.vendor; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.ejb.HibernateEntityManager; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.datasource.SimpleConnectionHandle; +import org.springframework.orm.hibernate3.SessionFactoryUtils; +import org.springframework.orm.jpa.DefaultJpaDialect; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * {@link org.springframework.orm.jpa.JpaDialect} implementation for + * Hibernate EntityManager. Developed and tested against Hibernate 3.2. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +public class HibernateJpaDialect extends DefaultJpaDialect { + + public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + throws PersistenceException, SQLException, TransactionException { + + super.beginTransaction(entityManager, definition); + return prepareTransaction(entityManager, definition.isReadOnly(), definition.getName()); + } + + public Object prepareTransaction(EntityManager entityManager, boolean readOnly, String name) + throws PersistenceException { + + Session session = getSession(entityManager); + FlushMode flushMode = session.getFlushMode(); + FlushMode previousFlushMode = null; + if (readOnly) { + // We should suppress flushing for a read-only transaction. + session.setFlushMode(FlushMode.MANUAL); + previousFlushMode = flushMode; + } + else { + // We need AUTO or COMMIT for a non-read-only transaction. + if (flushMode.lessThan(FlushMode.COMMIT)) { + session.setFlushMode(FlushMode.AUTO); + previousFlushMode = flushMode; + } + } + return new SessionTransactionData(session, previousFlushMode); + } + + public void cleanupTransaction(Object transactionData) { + ((SessionTransactionData) transactionData).resetFlushMode(); + } + + @Override + public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) + throws PersistenceException, SQLException { + + Session session = getSession(entityManager); + Connection con = session.connection(); + return (con != null ? new SimpleConnectionHandle(con) : null); + } + + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof HibernateException) { + return SessionFactoryUtils.convertHibernateAccessException((HibernateException) ex); + } + if (ex instanceof PersistenceException && ex.getCause() instanceof HibernateException) { + return SessionFactoryUtils.convertHibernateAccessException((HibernateException) ex.getCause()); + } + return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex); + } + + protected Session getSession(EntityManager em) { + return ((HibernateEntityManager) em).getSession(); + } + + + private static class SessionTransactionData { + + private final Session session; + + private final FlushMode previousFlushMode; + + public SessionTransactionData(Session session, FlushMode previousFlushMode) { + this.session = session; + this.previousFlushMode = previousFlushMode; + } + + public void resetFlushMode() { + if (this.previousFlushMode != null) { + this.session.setFlushMode(this.previousFlushMode); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java new file mode 100644 index 00000000000..52ef073297a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.util.Map; +import java.util.Properties; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.spi.PersistenceProvider; + +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.InformixDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.Oracle9Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.ejb.HibernateEntityManager; +import org.hibernate.ejb.HibernateEntityManagerFactory; +import org.hibernate.ejb.HibernatePersistence; + +import org.springframework.orm.jpa.JpaDialect; + +/** + * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for + * Hibernate EntityManager. Developed and tested against Hibernate 3.2. + * + *

Exposes Hibernate's persistence provider and EntityManager extension interface, + * and supports {@link AbstractJpaVendorAdapter}'s common configuration settings. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.0 + * @see org.hibernate.ejb.HibernatePersistence + * @see org.hibernate.ejb.HibernateEntityManager + */ +public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter { + + private final PersistenceProvider persistenceProvider = new HibernatePersistence(); + + private final JpaDialect jpaDialect = new HibernateJpaDialect(); + + + public PersistenceProvider getPersistenceProvider() { + return this.persistenceProvider; + } + + public String getPersistenceProviderRootPackage() { + return "org.hibernate"; + } + + public Map getJpaPropertyMap() { + Properties jpaProperties = new Properties(); + + if (getDatabasePlatform() != null) { + jpaProperties.setProperty(Environment.DIALECT, getDatabasePlatform()); + } + else if (getDatabase() != null) { + Class databaseDialectClass = determineDatabaseDialectClass(getDatabase()); + if (databaseDialectClass != null) { + jpaProperties.setProperty(Environment.DIALECT, databaseDialectClass.getName()); + } + } + + if (isGenerateDdl()) { + jpaProperties.setProperty(Environment.HBM2DDL_AUTO, "update"); + } + if (isShowSql()) { + jpaProperties.setProperty(Environment.SHOW_SQL, "true"); + } + + return jpaProperties; + } + + /** + * Determine the Hibernate database dialect class for the given target database. + * @param database the target database + * @return the Hibernate database dialect class, or null if none found + */ + protected Class determineDatabaseDialectClass(Database database) { + switch (database) { + case DB2: return DB2Dialect.class; + case DERBY: return DerbyDialect.class; + case H2: return H2Dialect.class; + case HSQL: return HSQLDialect.class; + case INFORMIX: return InformixDialect.class; + case MYSQL: return MySQLDialect.class; + case ORACLE: return Oracle9Dialect.class; // deprecated since Hibernate 3.2.5 - to be updated in Spring 3.0 + case POSTGRESQL: return PostgreSQLDialect.class; + case SQL_SERVER: return SQLServerDialect.class; + case SYBASE: return SybaseDialect.class; + default: return null; + } + } + + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + public Class getEntityManagerFactoryInterface() { + return HibernateEntityManagerFactory.class; + } + + public Class getEntityManagerInterface() { + return HibernateEntityManager.class; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/OpenJpaDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/OpenJpaDialect.java new file mode 100644 index 00000000000..328f9ce5ddc --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/OpenJpaDialect.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2007 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.vendor; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; + +import org.apache.openjpa.persistence.OpenJPAEntityManager; +import org.apache.openjpa.persistence.OpenJPAPersistence; + +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.orm.jpa.DefaultJpaDialect; +import org.springframework.transaction.SavepointManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * {@link org.springframework.orm.jpa.JpaDialect} implementation for + * Apache OpenJPA. Developed and tested against OpenJPA 0.9.7. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +public class OpenJpaDialect extends DefaultJpaDialect { + + @Override + public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + throws PersistenceException, SQLException, TransactionException { + + super.beginTransaction(entityManager, definition); + OpenJPAEntityManager em = getOpenJPAEntityManager(entityManager); + if (!definition.isReadOnly()) { + // Like with TopLink, make sure to start the logic transaction early so that other + // participants using the connection (such as JdbcTemplate) run in a transaction. + em.beginStore(); + } + return new OpenJpaTransactionData(em); + } + + @Override + public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) + throws PersistenceException, SQLException { + + return new OpenJpaConnectionHandle(getOpenJPAEntityManager(entityManager)); + } + + /** + * Return the OpenJPA-specific interface of EntityManager. + * @param em the generic EntityManager instance + * @return the OpenJPA-specific interface of EntityManager + */ + protected OpenJPAEntityManager getOpenJPAEntityManager(EntityManager em) { + return OpenJPAPersistence.cast(em); + } + + + /** + * Transaction data Object exposed from beginTransaction, + * implementing the SavepointManager interface. + */ + private static class OpenJpaTransactionData implements SavepointManager { + + private final OpenJPAEntityManager entityManager; + + private int savepointCounter = 0; + + public OpenJpaTransactionData(OpenJPAEntityManager entityManager) { + this.entityManager = entityManager; + } + + public Object createSavepoint() throws TransactionException { + this.savepointCounter++; + String savepointName = ConnectionHolder.SAVEPOINT_NAME_PREFIX + this.savepointCounter; + this.entityManager.setSavepoint(savepointName); + return savepointName; + } + + public void rollbackToSavepoint(Object savepoint) throws TransactionException { + this.entityManager.rollbackToSavepoint((String) savepoint); + } + + public void releaseSavepoint(Object savepoint) throws TransactionException { + this.entityManager.releaseSavepoint((String) savepoint); + } + } + + + /** + * ConnectionHandle implementation that fetches a new OpenJPA-provided Connection + * for every getConnection call and closes the Connection on + * releaseConnection. This is necessary because OpenJPA requires the + * fetched Connection to be closed before continuing EntityManager work. + * @see org.apache.openjpa.persistence.OpenJPAEntityManager#getConnection() + */ + private static class OpenJpaConnectionHandle implements ConnectionHandle { + + private final OpenJPAEntityManager entityManager; + + public OpenJpaConnectionHandle(OpenJPAEntityManager entityManager) { + this.entityManager = entityManager; + } + + public Connection getConnection() { + return (Connection) this.entityManager.getConnection(); + } + + public void releaseConnection(Connection con) { + JdbcUtils.closeConnection(con); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/OpenJpaVendorAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/OpenJpaVendorAdapter.java new file mode 100644 index 00000000000..f3c6c297d75 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/OpenJpaVendorAdapter.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.util.Map; +import java.util.Properties; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.spi.PersistenceProvider; + +import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; +import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI; +import org.apache.openjpa.persistence.PersistenceProviderImpl; + +import org.springframework.orm.jpa.JpaDialect; + +/** + * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for + * Apache OpenJPA. Developed and tested against OpenJPA 1.0.0. + * + *

Exposes OpenJPA's persistence provider and EntityManager extension interface, + * and supports {@link AbstractJpaVendorAdapter}'s common configuration settings. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see org.apache.openjpa.persistence.PersistenceProviderImpl + * @see org.apache.openjpa.persistence.OpenJPAEntityManager + */ +public class OpenJpaVendorAdapter extends AbstractJpaVendorAdapter { + + private final PersistenceProvider persistenceProvider = new PersistenceProviderImpl(); + + private final OpenJpaDialect jpaDialect = new OpenJpaDialect(); + + + public PersistenceProvider getPersistenceProvider() { + return this.persistenceProvider; + } + + public String getPersistenceProviderRootPackage() { + return "org.apache.openjpa"; + } + + public Map getJpaPropertyMap() { + Properties jpaProperties = new Properties(); + + if (getDatabasePlatform() != null) { + jpaProperties.setProperty("openjpa.jdbc.DBDictionary", getDatabasePlatform()); + } + else if (getDatabase() != null) { + String databaseDictonary = determineDatabaseDictionary(getDatabase()); + if (databaseDictonary != null) { + jpaProperties.setProperty("openjpa.jdbc.DBDictionary", databaseDictonary); + } + } + + if (isGenerateDdl()) { + jpaProperties.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)"); + } + + if (isShowSql()) { + // Taken from the OpenJPA 0.9.6 docs ("Standard OpenJPA Log Configuration + All SQL Statements") + jpaProperties.setProperty("openjpa.Log", "DefaultLevel=WARN, Runtime=INFO, Tool=INFO, SQL=TRACE"); + } + + return jpaProperties; + } + + /** + * Determine the OpenJPA database dictionary name for the given database. + * @param database the specified database + * @return the OpenJPA database dictionary name, or null if none found + */ + protected String determineDatabaseDictionary(Database database) { + switch (database) { + case DB2: return "db2"; + case DERBY: return "derby"; + case HSQL: return "hsql(SimulateLocking=true)"; + case INFORMIX: return "informix"; + case MYSQL: return "mysql"; + case ORACLE: return "oracle"; + case POSTGRESQL: return "postgres"; + case SQL_SERVER: return "sqlserver"; + case SYBASE: return "sybase"; + default: return null; + } + } + + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + public Class getEntityManagerFactoryInterface() { + return OpenJPAEntityManagerFactorySPI.class; + } + + public Class getEntityManagerInterface() { + return OpenJPAEntityManagerSPI.class; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/TopLinkJpaDialect.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/TopLinkJpaDialect.java new file mode 100644 index 00000000000..42693f59c8a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/TopLinkJpaDialect.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceException; + +import oracle.toplink.essentials.internal.sessions.AbstractSession; +import oracle.toplink.essentials.sessions.Session; +import oracle.toplink.essentials.sessions.UnitOfWork; + +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.jdbc.datasource.SimpleConnectionHandle; +import org.springframework.orm.jpa.DefaultJpaDialect; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * {@link org.springframework.orm.jpa.JpaDialect} implementation for + * Oracle TopLink Essentials. Developed and tested against TopLink Essentials v2. + * + *

By default, this class acquires a TopLink transaction to get the JDBC Connection + * early. This allows mixing JDBC and JPA/TopLink operations in the same transaction. + * In some cases, this eager acquisition of a transaction/connection may impact + * scalability. In that case, set the "lazyDatabaseTransaction" flag to true if you + * do not require mixing JDBC and JPA operations in the same transaction. Otherwise, + * use a {@link org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy} + * to ensure that the cost of connection acquisition is near zero until code actually + * needs a JDBC Connection. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see #setLazyDatabaseTransaction + * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy + */ +public class TopLinkJpaDialect extends DefaultJpaDialect { + + private boolean lazyDatabaseTransaction = false; + + + /** + * Set whether to lazily start a database transaction within a TopLink + * transaction. + *

By default, database transactions are started early. This allows + * for reusing the same JDBC Connection throughout an entire transaction, + * including read operations, and also for exposing TopLink transactions + * to JDBC access code (working on the same DataSource). + *

It is only recommended to switch this flag to "true" when no JDBC access + * code is involved in any of the transactions, and when it is acceptable to + * perform read operations outside of the transactional JDBC Connection. + * @see oracle.toplink.sessions.UnitOfWork#beginEarlyTransaction() + */ + public void setLazyDatabaseTransaction(boolean lazyDatabaseTransaction) { + this.lazyDatabaseTransaction = lazyDatabaseTransaction; + } + + + @Override + public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + throws PersistenceException, SQLException, TransactionException { + + super.beginTransaction(entityManager, definition); + if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { + // This is the magic bit. As with the existing Spring TopLink integration, + // begin an early transaction to force TopLink to get a JDBC Connection + // so that Spring can manage transactions with JDBC as well as TopLink. + UnitOfWork uow = (UnitOfWork) getSession(entityManager); + uow.beginEarlyTransaction(); + } + // Could return the UOW, if there were any advantage in having it later. + return null; + } + + @Override + public ConnectionHandle getJdbcConnection(EntityManager em, boolean readOnly) + throws PersistenceException, SQLException { + + AbstractSession session = (AbstractSession) getSession(em); + // The connection was already acquired eagerly in beginTransaction, + // unless lazyDatabaseTransaction was set to true. + Connection con = session.getAccessor().getConnection(); + return (con != null ? new SimpleConnectionHandle(con) : null); + } + + /** + * Get a traditional TopLink Session from the given EntityManager. + */ + protected Session getSession(EntityManager em) { + oracle.toplink.essentials.ejb.cmp3.EntityManager emi = (oracle.toplink.essentials.ejb.cmp3.EntityManager) em; + return emi.getActiveSession(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/TopLinkJpaVendorAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/TopLinkJpaVendorAdapter.java new file mode 100644 index 00000000000..698656d53ec --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/TopLinkJpaVendorAdapter.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2008 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.vendor; + +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; + +import javax.persistence.EntityManager; +import javax.persistence.spi.PersistenceProvider; + +import oracle.toplink.essentials.config.TargetDatabase; +import oracle.toplink.essentials.config.TopLinkProperties; +import oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider; + +import org.springframework.orm.jpa.JpaDialect; + +/** + * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for + * Oracle TopLink Essentials. Developed and tested against TopLink Essentials v2. + * + *

Exposes TopLink's persistence provider and EntityManager extension interface, + * and supports {@link AbstractJpaVendorAdapter}'s common configuration settings. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider + * @see oracle.toplink.essentials.ejb.cmp3.EntityManager + */ +public class TopLinkJpaVendorAdapter extends AbstractJpaVendorAdapter { + + private final PersistenceProvider persistenceProvider = new EntityManagerFactoryProvider(); + + private final JpaDialect jpaDialect = new TopLinkJpaDialect(); + + + public PersistenceProvider getPersistenceProvider() { + return this.persistenceProvider; + } + + public String getPersistenceProviderRootPackage() { + return "oracle.toplink.essentials"; + } + + public Map getJpaPropertyMap() { + Properties jpaProperties = new Properties(); + + if (getDatabasePlatform() != null) { + jpaProperties.setProperty(TopLinkProperties.TARGET_DATABASE, getDatabasePlatform()); + } + else if (getDatabase() != null) { + String targetDatabase = determineTargetDatabaseName(getDatabase()); + if (targetDatabase != null) { + jpaProperties.setProperty(TopLinkProperties.TARGET_DATABASE, targetDatabase); + } + } + + if (isGenerateDdl()) { + jpaProperties.setProperty(EntityManagerFactoryProvider.DDL_GENERATION, + EntityManagerFactoryProvider.CREATE_ONLY); + jpaProperties.setProperty(EntityManagerFactoryProvider.DDL_GENERATION_MODE, + EntityManagerFactoryProvider.DDL_DATABASE_GENERATION); + } + if (isShowSql()) { + jpaProperties.setProperty(TopLinkProperties.LOGGING_LEVEL, Level.FINE.toString()); + } + + return jpaProperties; + } + + /** + * Determine the TopLink target database name for the given database. + * @param database the specified database + * @return the TopLink target database name, or null if none found + */ + protected String determineTargetDatabaseName(Database database) { + switch (database) { + case DB2: return TargetDatabase.DB2; + case DERBY: return TargetDatabase.Derby; + case HSQL: return TargetDatabase.HSQL; + case INFORMIX: return TargetDatabase.Informix; + case MYSQL: return TargetDatabase.MySQL4; + case ORACLE: return TargetDatabase.Oracle; + case POSTGRESQL: return TargetDatabase.PostgreSQL; + case SQL_SERVER: return TargetDatabase.SQLServer; + case SYBASE: return TargetDatabase.Sybase; + default: return null; + } + } + + public JpaDialect getJpaDialect() { + return this.jpaDialect; + } + + public Class getEntityManagerInterface() { + return oracle.toplink.essentials.ejb.cmp3.EntityManager.class; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/package.html new file mode 100644 index 00000000000..447589e6950 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/jpa/vendor/package.html @@ -0,0 +1,7 @@ + + + +Support classes for adapting to specific JPA vendors. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/package.html new file mode 100644 index 00000000000..4bb458ff620 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/package.html @@ -0,0 +1,8 @@ + + + +Root package for Spring's O/R Mapping integration classes. +Contains generic DataAccessExceptions related to O/R Mapping. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/AbstractSessionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/AbstractSessionFactory.java new file mode 100644 index 00000000000..2f4d7ec7a64 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/AbstractSessionFactory.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2008 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.toplink; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; +import oracle.toplink.sessions.UnitOfWork; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstract SessionFactory implementation that creates proxies for + * "managed" client Sessions and transaction-aware Session references. + * + *

Delegates to two template methods: + * + * @author Juergen Hoeller + * @since 1.2.6 + * @see #getMasterSession() + * @see #createClientSession() + */ +public abstract class AbstractSessionFactory implements SessionFactory { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + /** + * Create a plain client Session for this factory's master Session. + * @see #createClientSession() + */ + public Session createSession() throws TopLinkException { + logger.debug("Creating TopLink client Session"); + return createClientSession(); + } + + /** + * Create a "managed" client Session reference for an underlying + * client Session created for this factory. + * @see #createClientSession() + */ + public Session createManagedClientSession() throws TopLinkException { + logger.debug("Creating managed TopLink client Session"); + Session target = createClientSession(); + return (Session) Proxy.newProxyInstance(target.getClass().getClassLoader(), + new Class[] {Session.class}, new ManagedClientInvocationHandler(target)); + } + + /** + * Create a transaction-aware Session reference for this factory's master Session, + * expecting transactions to be registered for this SessionFactory. + * @see #getMasterSession() + * @see oracle.toplink.sessions.Session#getActiveSession() + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + */ + public Session createTransactionAwareSession() throws TopLinkException { + logger.debug("Creating transaction-aware TopLink Session"); + return createTransactionAwareSession(this); + } + + /** + * Create a transaction-aware Session reference for this factory's master Session, + * expecting transactions to be registered for the given SessionFactory. + *

This method is public to allow custom SessionFactory facades to access + * it directly, if necessary. + * @param sessionFactory the SessionFactory that transactions + * are expected to be registered for + * @see #getMasterSession() + * @see oracle.toplink.sessions.Session#getActiveSession() + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + */ + public Session createTransactionAwareSession(SessionFactory sessionFactory) throws TopLinkException { + Session target = getMasterSession(); + return (Session) Proxy.newProxyInstance( + target.getClass().getClassLoader(), new Class[] {Session.class}, + new TransactionAwareInvocationHandler(sessionFactory, target)); + } + + + /** + * Return this factory's "master" Session. + * For example, a TopLink ServerSession. + *

Used for creating transaction-aware Session reference. + */ + protected abstract Session getMasterSession(); + + /** + * Create a new client Session for this factory's master Session. + * For example, a TopLink ClientSession. + *

Used for creating plain Sessions and "managed" client Sessions. + * @throws TopLinkException if creation of a client Session failed + */ + protected abstract Session createClientSession() throws TopLinkException; + + + /** + * Invocation handler that decorates a client Session with an "active" + * UnitOfWork. For use in situations where Spring's TopLinkTransactionManager + * requires a "managed" thread-safe TopLink Session. + */ + private static class ManagedClientInvocationHandler implements InvocationHandler { + + private final Session target; + + private final UnitOfWork uow; + + public ManagedClientInvocationHandler(Session target) { + this.target = target; + this.uow = this.target.acquireUnitOfWork(); + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on Session interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of SessionFactory proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("getActiveSession")) { + return this.target; + } + else if (method.getName().equals("getActiveUnitOfWork")) { + return this.uow; + } + else if (method.getName().equals("release")) { + this.uow.release(); + this.target.release(); + } + + // Invoke method on target Session. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + + /** + * Invocation handler that delegates getActiveSession calls + * to SessionFactoryUtils, for being aware of thread-bound transactions. + */ + private static class TransactionAwareInvocationHandler implements InvocationHandler { + + private final SessionFactory sessionFactory; + + private final Session target; + + public TransactionAwareInvocationHandler(SessionFactory sessionFactory, Session target) { + this.sessionFactory = sessionFactory; + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on Session interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of SessionFactory proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("getActiveSession")) { + // Handle getActiveSession method: return transactional Session, if any. + try { + return SessionFactoryUtils.doGetSession(this.sessionFactory, false); + } + catch (IllegalStateException ex) { + // getActiveSession is supposed to return the Session itself if no active one found. + return this.target; + } + } + else if (method.getName().equals("getActiveUnitOfWork")) { + // Handle getActiveUnitOfWork method: return transactional UnitOfWork, if any. + try { + return SessionFactoryUtils.doGetSession(this.sessionFactory, false).getActiveUnitOfWork(); + } + catch (IllegalStateException ex) { + // getActiveUnitOfWork is supposed to return null if no active one found. + return null; + } + } + + // Invoke method on target Session. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/LocalSessionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/LocalSessionFactory.java new file mode 100644 index 00000000000..f85ee3d2034 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/LocalSessionFactory.java @@ -0,0 +1,458 @@ +/* + * Copyright 2002-2008 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.toplink; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.internal.databaseaccess.DatabasePlatform; +import oracle.toplink.jndi.JNDIConnector; +import oracle.toplink.sessionbroker.SessionBroker; +import oracle.toplink.sessions.DatabaseLogin; +import oracle.toplink.sessions.DatabaseSession; +import oracle.toplink.sessions.SessionLog; +import oracle.toplink.threetier.ServerSession; +import oracle.toplink.tools.sessionconfiguration.XMLLoader; +import oracle.toplink.tools.sessionmanagement.SessionManager; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Convenient JavaBean-style factory for a TopLink SessionFactory instance. + * Loads a TopLink sessions.xml file from the class path, exposing a + * specific TopLink Session defined there (usually a ServerSession). + * + *

TopLink Session configuration is done using a sessions.xml file. + * The most convenient way to create the sessions.xml file is to use + * the Oracle TopLink SessionsEditor workbench. The sessions.xml file + * contains all runtime configuration and points to a second XML or Class resource + * from which to load the actual TopLink project metadata (which defines mappings). + * + *

LocalSessionFactory loads the sessions.xml file during + * initialization in order to bootstrap the specified TopLink (Server)Session. + * The name of the actual config resource and the name of the Session to be loaded, + * if different from sessions.xml and "Session", respectively, can be + * configured through bean properties. + * + *

All resources (sessions.xml and Mapping Workbench metadata) are + * loaded using ClassLoader.getResourceAsStream calls by TopLink, so + * users may need to configure a ClassLoader with appropriate visibility. This is + * particularly important in J2EE environments where the TopLink metadata might be + * deployed to a different location than the Spring configuration. The ClassLoader + * used to search for the TopLink metadata and to load the persistent classes + * defined there will default to the the context ClassLoader for the current Thread. + * + *

TopLink's debug logging can be redirected to Commons Logging by passing a + * CommonsLoggingSessionLog to the "sessionLog" bean property. Otherwise, TopLink + * uses it's own DefaultSessionLog, whose levels are configured in the + * sessions.xml file. + * + *

This class has been tested against both TopLink 9.0.4 and TopLink 10.1.3. + * It will automatically adapt to the TopLink version encountered: for example, + * using an XMLSessionConfigLoader on 10.1.3, but an XMLLoader on 9.0.4. + * + *

NOTE: When defining a TopLink SessionFactory in a Spring application + * context, you will usually define a bean of type LocalSessionFactoryBean. + * LocalSessionFactoryBean is a subclass of this factory, which will automatically + * expose the created TopLink SessionFactory instance as bean reference. + * + * @author Juergen Hoeller + * @author James Clark + * @since 1.2 + * @see LocalSessionFactoryBean + * @see TopLinkTemplate#setSessionFactory + * @see TopLinkTransactionManager#setSessionFactory + * @see SingleSessionFactory + * @see ServerSessionFactory + * @see oracle.toplink.threetier.ServerSession + * @see oracle.toplink.tools.sessionconfiguration.XMLLoader + * @see oracle.toplink.tools.sessionconfiguration.XMLSessionConfigLoader + */ +public class LocalSessionFactory { + + /** + * The default location of the sessions.xml TopLink configuration file: + * "sessions.xml" in the class path. + */ + public static final String DEFAULT_SESSIONS_XML = "sessions.xml"; + + /** + * The default session name to look for in the sessions.xml: "Session". + */ + public static final String DEFAULT_SESSION_NAME = "Session"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The classpath location of the sessions TopLink configuration file. + */ + private String configLocation = DEFAULT_SESSIONS_XML; + + /** + * The session name to look for in the sessions.xml configuration file. + */ + private String sessionName = DEFAULT_SESSION_NAME; + + /** + * The ClassLoader to use to load the sessions.xml and project XML files. + */ + private ClassLoader sessionClassLoader; + + private DatabaseLogin databaseLogin; + + private final Map loginPropertyMap = new HashMap(); + + private DataSource dataSource; + + private DatabasePlatform databasePlatform; + + private SessionLog sessionLog; + + + /** + * Set the TopLink sessions.xml configuration file that defines + * TopLink Sessions, as class path resource location. + *

The sessions.xml file will usually be placed in the META-INF + * directory or root path of a JAR file, or the WEB-INF/classes + * directory of a WAR file (specifying "META-INF/toplink-sessions.xml" or + * simply "toplink-sessions.xml" as config location, respectively). + *

The default config location is "sessions.xml" in the root of the class path. + * @param configLocation the class path location of the sessions.xml file + */ + public void setConfigLocation(String configLocation) { + this.configLocation = configLocation; + } + + /** + * Set the name of the TopLink Session, as defined in TopLink's + * sessions.xml configuration file. + * The default session name is "Session". + */ + public void setSessionName(String sessionName) { + this.sessionName = sessionName; + } + + /** + * Set the ClassLoader that should be used to lookup the config resources. + * If nothing is set here, then we will try to use the Thread context ClassLoader + * and the ClassLoader that loaded this factory class, in that order. + *

This ClassLoader will be used to load the TopLink configuration files + * and the project metadata. Furthermore, the TopLink ConversionManager will + * use this ClassLoader to load all TopLink entity classes. If the latter is not + * appropriate, users can configure a pre-login SessionEvent to alter the + * ConversionManager ClassLoader that TopLink will use at runtime. + */ + public void setSessionClassLoader(ClassLoader sessionClassLoader) { + this.sessionClassLoader = sessionClassLoader; + } + + /** + * Specify the DatabaseLogin instance that carries the TopLink database + * configuration to use. This is an alternative to specifying that information + * in a <login> tag in the sessions.xml configuration file, + * allowing for configuring a DatabaseLogin instance as standard Spring bean + * definition (being able to leverage Spring's placeholder mechanism, etc). + *

The DatabaseLogin instance can either carry traditional JDBC config properties + * or hold a nested TopLink Connector instance, pointing to the connection pool to use. + * DatabaseLogin also holds the TopLink DatabasePlatform instance that defines the + * database product that TopLink is talking to (for example, HSQLPlatform). + *

WARNING: Overriding the Login instance has been reported to not + * work on TopLink 10.1.3.0 and 10.1.3.1. Specify {@link #setLoginProperties + * "loginProperties"} or {@link #getLoginPropertyMap "loginPropertyMap[...]"} + * entries instead, if you prefer to have the login configuration defined + * on the Spring LocalSessionFactory. + */ + public void setDatabaseLogin(DatabaseLogin databaseLogin) { + this.databaseLogin = databaseLogin; + } + + /** + * Specify TopLink login properties, to be passed to + * the {@link oracle.toplink.sessions.DatabaseLogin} instance. + *

Can be populated with a String "value" (parsed via PropertiesEditor) + * or a "props" element in XML bean definitions. + * @see oracle.toplink.sessions.DatabaseLogin + */ + public void setLoginProperties(Properties loginProperties) { + CollectionUtils.mergePropertiesIntoMap(loginProperties, this.loginPropertyMap); + } + + /** + * Specify TopLink login properties as a Map, to be passed to + * the {@link oracle.toplink.sessions.DatabaseLogin} instance. + *

Can be populated with a "map" or "props" element in XML bean definitions. + * @see oracle.toplink.sessions.DatabaseLogin + */ + public void setLoginPropertyMap(Map loginProperties) { + if (loginProperties != null) { + this.loginPropertyMap.putAll(loginProperties); + } + } + + /** + * Allow Map access to the TopLink login properties to be passed to the + * DatabaseLogin instance, with the option to add or override specific entries. + *

Useful for specifying entries directly, for example via + * "loginPropertyMap[tableQualifier]". + * @see oracle.toplink.sessions.DatabaseLogin + */ + public Map getLoginPropertyMap() { + return this.loginPropertyMap; + } + + /** + * Specify a standard JDBC DataSource that TopLink should use as connection pool. + * This allows for using a shared DataSource definition instead of TopLink's + * own connection pool. + *

A passed-in DataSource will be wrapped in an appropriate TopLink Connector + * and registered with the TopLink DatabaseLogin instance (either the default + * instance or one passed in through the "databaseLogin" property). The + * "usesExternalConnectionPooling" flag will automatically be set to "true". + * @see oracle.toplink.sessions.DatabaseLogin#setConnector(oracle.toplink.sessions.Connector) + * @see oracle.toplink.sessions.DatabaseLogin#setUsesExternalConnectionPooling(boolean) + * @see #setDatabaseLogin(oracle.toplink.sessions.DatabaseLogin) + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Specify the TopLink DatabasePlatform instance that the Session should use: + * for example, HSQLPlatform. This is an alternative to specifying the platform + * in a <login> tag in the sessions.xml configuration file. + *

A passed-in DatabasePlatform will be registered with the TopLink + * DatabaseLogin instance (either the default instance or one passed in + * through the "databaseLogin" property). + * @see oracle.toplink.internal.databaseaccess.HSQLPlatform + * @see oracle.toplink.platform.database.HSQLPlatform + */ + public void setDatabasePlatform(DatabasePlatform databasePlatform) { + this.databasePlatform = databasePlatform; + } + + /** + * Specify a TopLink SessionLog instance to use for detailed logging of the + * Session's activities: for example, DefaultSessionLog (which logs to the + * console), JavaLog (which logs through JDK 1.4'S java.util.logging, + * available as of TopLink 10.1.3), or CommonsLoggingSessionLog / + * CommonsLoggingSessionLog904 (which logs through Commons Logging, + * on TopLink 10.1.3 and 9.0.4, respectively). + *

Note that detailed Session logging is usually only useful for debug + * logging, with adjustable detail level. As of TopLink 10.1.3, TopLink also + * uses different log categories, which allows for fine-grained filtering of + * log messages. For standard execution, no SessionLog needs to be specified. + * @see oracle.toplink.sessions.DefaultSessionLog + * @see oracle.toplink.logging.DefaultSessionLog + * @see oracle.toplink.logging.JavaLog + * @see org.springframework.orm.toplink.support.CommonsLoggingSessionLog + * @see org.springframework.orm.toplink.support.CommonsLoggingSessionLog904 + */ + public void setSessionLog(SessionLog sessionLog) { + this.sessionLog = sessionLog; + } + + + /** + * Create a TopLink SessionFactory according to the configuration settings. + * @return the new TopLink SessionFactory + * @throws TopLinkException in case of errors + */ + public SessionFactory createSessionFactory() throws TopLinkException { + if (logger.isInfoEnabled()) { + logger.info("Initializing TopLink SessionFactory from [" + this.configLocation + "]"); + } + + // Determine class loader to use. + ClassLoader classLoader = + (this.sessionClassLoader != null ? this.sessionClassLoader : ClassUtils.getDefaultClassLoader()); + + // Initialize the TopLink Session, using the configuration file + // and the session name. + DatabaseSession session = loadDatabaseSession(this.configLocation, this.sessionName, classLoader); + + // It is possible for SessionManager to return a null Session! + if (session == null) { + throw new IllegalStateException( + "A session named '" + this.sessionName + "' could not be loaded from resource [" + + this.configLocation + "] using ClassLoader [" + classLoader + "]. " + + "This is most likely a deployment issue: Can the class loader access the resource?"); + } + + DatabaseLogin login = (this.databaseLogin != null ? this.databaseLogin : session.getLogin()); + + // Apply specified login properties to the DatabaseLogin instance. + if (this.loginPropertyMap != null) { + PropertyAccessorFactory.forBeanPropertyAccess(login).setPropertyValues(this.loginPropertyMap); + } + + // Override default connection pool with specified DataSource, if any. + if (this.dataSource != null) { + login.setConnector(new JNDIConnector(this.dataSource)); + login.setUsesExternalConnectionPooling(true); + } + + // Override default DatabasePlatform with specified one, if any. + if (this.databasePlatform != null) { + login.usePlatform(this.databasePlatform); + } + + // Override default DatabaseLogin instance with specified one, if any. + if (this.databaseLogin != null) { + setDatabaseLogin(session, this.databaseLogin); + } + + // Override default SessionLog with specified one, if any. + if (this.sessionLog != null) { + session.setSessionLog(this.sessionLog); + session.logMessages(); + } + + // Log in and create corresponding SessionFactory. + session.login(); + return newSessionFactory(session); + } + + /** + * Handle differences between the Session.setLogin interface + * between TopLink 9.0.4 to 10.1.3. + *

The Login interface was introduced in TopLink 10.1.3. + * @param session the DatabaseSession being logged in + * @param login the DatabaseLogin injected by Spring + * @see oracle.toplink.sessions.DatabaseSession#setLogin + */ + protected void setDatabaseLogin(DatabaseSession session, DatabaseLogin login) { + Method setLoginMethod = null; + try { + // Search for the new 10.1.3 Login interface... + Class loginClass = DatabaseSession.class.getClassLoader().loadClass("oracle.toplink.sessions.Login"); + setLoginMethod = DatabaseSession.class.getMethod("setLogin", new Class[] {loginClass}); + if (logger.isDebugEnabled()) { + logger.debug("Using TopLink 10.1.3 setLogin(Login) API"); + } + } + catch (Exception ex) { + // TopLink 10.1.3 Login interface not found -> + // fall back to TopLink 9.0.4's setLogin(DatabaseLogin) + if (logger.isDebugEnabled()) { + logger.debug("Using TopLink 9.0.4 setLogin(DatabaseLogin) API"); + } + session.setLogin(login); + return; + } + + // Invoke the 10.1.3 version: Session.setLogin(Login) + ReflectionUtils.invokeMethod(setLoginMethod, session, new Object[] {login}); + } + + /** + * Load the specified DatabaseSession from the TopLink sessions.xml + * configuration file. + * @param configLocation the class path location of the sessions.xml file + * @param sessionName the name of the TopLink Session in the configuration file + * @param sessionClassLoader the class loader to use + * @return the DatabaseSession instance + * @throws TopLinkException in case of errors + */ + protected DatabaseSession loadDatabaseSession( + String configLocation, String sessionName, ClassLoader sessionClassLoader) + throws TopLinkException { + + SessionManager manager = getSessionManager(); + + // Try to find TopLink 10.1.3 XMLSessionConfigLoader. + Method getSessionMethod = null; + Object loader = null; + try { + Class loaderClass = SessionManager.class.getClassLoader().loadClass( + "oracle.toplink.tools.sessionconfiguration.XMLSessionConfigLoader"); + getSessionMethod = SessionManager.class.getMethod("getSession", + new Class[] {loaderClass, String.class, ClassLoader.class, boolean.class, boolean.class, boolean.class}); + if (logger.isDebugEnabled()) { + logger.debug("Using TopLink 10.1.3 XMLSessionConfigLoader"); + } + Constructor ctor = loaderClass.getConstructor(new Class[] {String.class}); + loader = ctor.newInstance(new Object[] {configLocation}); + } + catch (Exception ex) { + // TopLink 10.1.3 XMLSessionConfigLoader not found -> + // fall back to TopLink 9.0.4 XMLLoader. + if (logger.isDebugEnabled()) { + logger.debug("Using TopLink 9.0.4 XMLLoader"); + } + XMLLoader xmlLoader = new XMLLoader(configLocation); + return (DatabaseSession) manager.getSession(xmlLoader, sessionName, sessionClassLoader, false, false); + } + + // TopLink 10.1.3 XMLSessionConfigLoader found -> create loader instance + // through reflection and fetch specified Session from SessionManager. + // This invocation will check if the ClassLoader passed in is the same + // as the one used to a session currently loaded with the same "sessionName" + // If the ClassLoaders are different, then this LocalSessionFactory is being + // re-loaded after a hot-deploy and the existing DatabaseSession will be logged + // out and re-built from scratch. + return (DatabaseSession) ReflectionUtils.invokeMethod(getSessionMethod, manager, + new Object[] {loader, sessionName, sessionClassLoader, Boolean.FALSE, Boolean.FALSE, Boolean.TRUE}); + } + + /** + * Return the TopLink SessionManager to use for loading DatabaseSessions. + *

The default implementation creates a new plain SessionManager instance, + * leading to completely independent TopLink Session instances. Could be + * overridden to return a shared or pre-configured SessionManager. + * @return the TopLink SessionManager instance + */ + protected SessionManager getSessionManager() { + return new SessionManager(); + } + + /** + * Create a new SessionFactory for the given TopLink DatabaseSession. + *

The default implementation creates a ServerSessionFactory for a + * ServerSession and a SingleSessionFactory for a plain DatabaseSession. + * @param session the TopLink DatabaseSession to create a SessionFactory for + * @return the SessionFactory + * @throws TopLinkException in case of errors + * @see ServerSessionFactory + * @see SingleSessionFactory + * @see oracle.toplink.threetier.ServerSession + * @see oracle.toplink.sessions.DatabaseSession + */ + protected SessionFactory newSessionFactory(DatabaseSession session) { + if (session instanceof ServerSession) { + return new ServerSessionFactory((ServerSession) session); + } + else if (session instanceof SessionBroker) { + return new SessionBrokerSessionFactory((SessionBroker) session); + } + else { + return new SingleSessionFactory(session); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/LocalSessionFactoryBean.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/LocalSessionFactoryBean.java new file mode 100644 index 00000000000..4a87c958526 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/LocalSessionFactoryBean.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2007 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.toplink; + +import java.sql.SQLException; + +import oracle.toplink.exceptions.DatabaseException; +import oracle.toplink.exceptions.TopLinkException; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a + * TopLink {@link SessionFactory}. This is the usual way to set up a shared + * TopLink SessionFactory in a Spring application context; the SessionFactory + * can then be passed to TopLink-based DAOs via dependency injection. + * + *

See the base class {@link LocalSessionFactory} for configuration details. + * + *

This class also implements the + * {@link org.springframework.dao.support.PersistenceExceptionTranslator} + * interface, as autodetected by Spring's + * {@link org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor}, + * for AOP-based translation of native exceptions to Spring DataAccessExceptions. + * Hence, the presence of a LocalSessionFactoryBean automatically enables a + * PersistenceExceptionTranslationPostProcessor to translate TopLink exceptions. + * + *

If your DAOs expect to receive a raw TopLink Session, consider defining a + * {@link org.springframework.orm.toplink.support.TransactionAwareSessionAdapter} + * in front of this bean. This adapter will provide a TopLink Session rather + * than a SessionFactory as bean reference. Your DAOs can then, for example, + * access the currently active Session and UnitOfWork via + * Session.getActiveSession() and Session.getActiveUnitOfWork(), + * respectively. Note that you can still access the SessionFactory as well, by + * defining a bean reference that points directly at the LocalSessionFactoryBean. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocalSessionFactory + * @see org.springframework.orm.toplink.support.TransactionAwareSessionAdapter + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + */ +public class LocalSessionFactoryBean extends LocalSessionFactory + implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean, PersistenceExceptionTranslator { + + private SessionFactory sessionFactory; + + private SQLExceptionTranslator jdbcExceptionTranslator; + + + /** + * Set the JDBC exception translator for this SessionFactory. + *

Applied to any SQLException root cause of a TopLink DatabaseException, + * within Spring's PersistenceExceptionTranslator mechanism. + * The default is to rely on TopLink's native exception translation. + * @see oracle.toplink.exceptions.DatabaseException + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + /** + * Return the JDBC exception translator for this instance, if any. + */ + public SQLExceptionTranslator getJdbcExceptionTranslator() { + return this.jdbcExceptionTranslator; + } + + /** + * Sets the given bean ClassLoader as TopLink Session ClassLoader. + * @see #setSessionClassLoader + */ + public void setBeanClassLoader(ClassLoader classLoader) { + setSessionClassLoader(classLoader); + } + + public void afterPropertiesSet() throws TopLinkException { + this.sessionFactory = createSessionFactory(); + } + + + public Object getObject() { + return this.sessionFactory; + } + + public Class getObjectType() { + return (this.sessionFactory != null ? this.sessionFactory.getClass() : SessionFactory.class); + } + + public boolean isSingleton() { + return true; + } + + + /** + * Implementation of the PersistenceExceptionTranslator interface, + * as autodetected by Spring's PersistenceExceptionTranslationPostProcessor. + *

Converts the exception if it is a TopLinkException; + * else returns null to indicate an unknown exception. + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * @see #convertTopLinkAccessException + */ + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof TopLinkException) { + return convertTopLinkAccessException((TopLinkException) ex); + } + return null; + } + + /** + * Convert the given TopLinkException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * TopLink DatabaseException, else rely on TopLink's default translation. + * @param ex TopLinkException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertTopLinkAccessException + * @see #setJdbcExceptionTranslator + */ + public DataAccessException convertTopLinkAccessException(TopLinkException ex) { + if (getJdbcExceptionTranslator() != null && ex instanceof DatabaseException) { + Throwable internalEx = ex.getInternalException(); + // Should always be a SQLException inside a DatabaseException. + if (internalEx instanceof SQLException) { + return getJdbcExceptionTranslator().translate( + "TopLink operation: " + ex.getMessage(), null, (SQLException) internalEx); + } + } + return SessionFactoryUtils.convertTopLinkAccessException(ex); + } + + + public void destroy() { + logger.info("Closing TopLink SessionFactory"); + this.sessionFactory.close(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/ServerSessionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/ServerSessionFactory.java new file mode 100644 index 00000000000..b8104442b6a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/ServerSessionFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; +import oracle.toplink.threetier.ServerSession; + +/** + * Full-fledged default implementation of the SessionFactory interface: + * creates ClientSessions for a given ServerSession. + * + *

Can create a special ClientSession subclass for managed Sessions, carrying + * an active UnitOfWork that expects to be committed at transaction completion + * (just like a plain TopLink Session does within a JTA transaction). + * + *

Can also create a transaction-aware Session reference that returns the + * active transactional Session on getActiveSession. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SingleSessionFactory + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + * @see oracle.toplink.sessions.Session#getActiveSession() + */ +public class ServerSessionFactory extends AbstractSessionFactory { + + private final ServerSession serverSession; + + + /** + * Create a new ServerSessionFactory for the given ServerSession. + * @param serverSession the TopLink ServerSession to create ClientSessions for + */ + public ServerSessionFactory(ServerSession serverSession) { + this.serverSession = serverSession; + } + + + /** + * Return this factory's ServerSession as-is. + */ + protected Session getMasterSession() { + return this.serverSession; + } + + /** + * Create a plain ClientSession for this factory's ServerSession. + * @see oracle.toplink.threetier.ServerSession#acquireClientSession() + */ + protected Session createClientSession() throws TopLinkException { + return this.serverSession.acquireClientSession(); + } + + + /** + * Shut the pre-configured TopLink ServerSession down. + * @see oracle.toplink.sessions.DatabaseSession#logout() + * @see oracle.toplink.sessions.Session#release() + */ + public void close() { + this.serverSession.logout(); + this.serverSession.release(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionBrokerSessionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionBrokerSessionFactory.java new file mode 100644 index 00000000000..7a3c0cb122d --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionBrokerSessionFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.exceptions.ValidationException; +import oracle.toplink.sessionbroker.SessionBroker; +import oracle.toplink.sessions.Session; + +/** + * Spring SessionFactory implementation allowing users to + * inject a TopLink Session built from a TopLink SessionBroker. + * + * SessionBrokers are used identically to any other TopLink Session. DAO code + * should never have to distinguish between Sessions which broker requests to + * multiple databases and Sessions which manage requests to a single database. + * + * The only pertinent difference in the SessionBroker api involves the method + * for obtaining a thread-safe "client" Session from the SessionBroker. + * Instead of the typical acquireClientSession + * method, this SessionFactory implementation uses the + * acquireClientSessionBroker method. + * If a SessionBroker aggregates non thread-safe DatabaseSessions, + * the factory will throw UnsupportedOperationExceptions + * if used to create managed or transaction-aware Sessions. + * + * @author James Clark + * @author Juergen Hoeller + * @since 1.2.6 + * @see org.springframework.orm.toplink.ServerSessionFactory + * @see oracle.toplink.threetier.ServerSession#acquireClientSession() + * @see oracle.toplink.sessionbroker.SessionBroker#acquireClientSessionBroker() + */ +public class SessionBrokerSessionFactory extends AbstractSessionFactory { + + private final SessionBroker sessionBroker; + + + /** + * Create a new SessionBrokerSessionFactory for the given SessionBroker. + * @param broker the TopLink SessionBroker to fetch Sessions from + */ + public SessionBrokerSessionFactory(SessionBroker broker) { + this.sessionBroker = broker; + } + + + /** + * Try to create a client Session; fall back to the master Session, + * if no client Session can be created (because of the session broker's + * configuration). + * @see #createClientSession() + * @see #getMasterSession() + */ + public Session createSession() throws TopLinkException { + try { + return createClientSession(); + } + catch (ValidationException ex) { + logger.debug( + "Could not create TopLink client session for SessionBroker - returning SessionBroker itself", ex); + return getMasterSession(); + } + } + + /** + * Return this factory's SessionBroker as-is. + */ + protected Session getMasterSession() { + return this.sessionBroker; + } + + /** + * Create a plain client SessionBroker for this factory's ServerSession. + * @see oracle.toplink.sessionbroker.SessionBroker#acquireClientSessionBroker() + */ + protected Session createClientSession() throws TopLinkException { + return this.sessionBroker.acquireClientSessionBroker(); + } + + + /** + * Shut the pre-configured TopLink SessionBroker down. + * @see oracle.toplink.sessions.DatabaseSession#logout() + * @see oracle.toplink.sessions.Session#release() + */ + public void close() { + this.sessionBroker.logout(); + this.sessionBroker.release(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionFactory.java new file mode 100644 index 00000000000..cc9dd3853d4 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionFactory.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; + +/** + * The SessionFactory interface serves as factory for TopLink Sessions, + * allowing for dependency injection on thread-safe TopLink-based DAOs. + * Used by TopLinkAccessor/Template and TopLinkTransactionManager. + * + *

In contrast to JDO or Hibernate (which define native PersistenceManagerFactory + * and SessionFactory interfaces, respectively), TopLink itself does not provide + * such a factory interface: hence, it is necessary to define it within Spring. + * Note that this interface does not depend on any other Spring interfaces or + * classes, to allow for keeping TopLink-based DAOs as independent as possible. + * + * @author Juergen Hoeller + * @since 1.2 + * @see TopLinkAccessor#setSessionFactory + * @see TopLinkTransactionManager#setSessionFactory + */ +public interface SessionFactory { + + /** + * Create a plain TopLink Session for the current application context. + * Will usually be a new ClientSession for the current thread. + *

The returned Session will participate in JTA transactions (provided that + * TopLink is configured with a corresponding external transaction controller), + * but not in Spring-managed transactions (by TopLinkTransactionManager). + *

This is the factory method to be called by TopLink data access code, + * usually through the SessionFactoryUtils.getSession method + * that checks for a transactional (thread-bound) Session first. + * @return the new TopLink Session + * @throws TopLinkException in case of errors + * @see SessionFactoryUtils#getSession(SessionFactory, boolean) + */ + Session createSession() throws TopLinkException; + + /** + * Create a new managed TopLink client Session for the current context. + * Will usually be a new special ClientSession for the current thread. + *

The returned Session will be prepared to be managed within a Spring + * transaction (by TopLinkTransactionManager). It will carry an active + * UnitOfWork that expects to be committed at transaction completion, + * just like a plain TopLink Session does within a JTA transaction. + *

This method is only supposed to be called by Spring's + * TopLinkTransactionManager or similar TopLink-based transaction managers. + * If a SessionFactory does not support managed Sessions, it should throw + * an UnsupportedOperationException. + * @return the new TopLink Session + * @throws TopLinkException in case of errors + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + */ + Session createManagedClientSession() throws TopLinkException; + + /** + * Create a new transaction-aware TopLink Session that exposes the currently + * active Session and UnitOfWork via Session.getActiveSession() + * and Session.getActiveUnitOfWork(), respectively. + *

Such a Session reference can be used analogously to a managed TopLink + * Session in a JTA environment, with Spring-managed transactions backing it. + *

It is usually preferable to let DAOs work with a full SessionFactory, + * accessing TopLink Sessions via SessionFactoryUtils.getSession. + * However, a transaction-aware TopLink Session reference does not impose any + * Spring dependency, so might be preferable if you'd like to keep your data + * access code tied to TopLink API only. + * @return the new TopLink Session + * @throws TopLinkException in case of errors + * @see oracle.toplink.sessions.Session#getActiveSession() + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + */ + Session createTransactionAwareSession() throws TopLinkException; + + /** + * Close this SessionFactory, shutting down all internal resources. + */ + void close(); + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionFactoryUtils.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionFactoryUtils.java new file mode 100644 index 00000000000..cda7f58c317 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionFactoryUtils.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2008 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.toplink; + +import oracle.toplink.exceptions.ConcurrencyException; +import oracle.toplink.exceptions.ConversionException; +import oracle.toplink.exceptions.DatabaseException; +import oracle.toplink.exceptions.OptimisticLockException; +import oracle.toplink.exceptions.QueryException; +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.TypeMismatchDataAccessException; +import org.springframework.transaction.support.ResourceHolder; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class featuring methods for TopLink Session handling, + * allowing for reuse of TopLink Session instances within transactions. + * Also provides support for exception translation. + * + *

Mainly intended for internal use within the framework. + * + * @author Juergen Hoeller + * @author James Clark + * @since 1.2 + */ +public abstract class SessionFactoryUtils { + + private static final Log logger = LogFactory.getLog(SessionFactoryUtils.class); + + + /** + * Get a TopLink Session for the given SessionFactory. Is aware of and will + * return any existing corresponding Session bound to the current thread, for + * example when using TopLinkTransactionManager. Will create a new Session + * otherwise, if "allowCreate" is true. + *

This is the getSession method used by typical data access code, + * in combination with releaseSession called when done with + * the Session. Note that TopLinkTemplate allows to write data access code + * without caring about such resource handling. + * @param sessionFactory TopLink SessionFactory to create the session with + * @param allowCreate if a non-transactional Session should be created when no + * transactional Session can be found for the current thread + * @return the TopLink Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and + * "allowCreate" is false + * @see #releaseSession + * @see TopLinkTemplate + */ + public static Session getSession(SessionFactory sessionFactory, boolean allowCreate) + throws DataAccessResourceFailureException, IllegalStateException { + + try { + return doGetSession(sessionFactory, allowCreate); + } + catch (TopLinkException ex) { + throw new DataAccessResourceFailureException("Could not open TopLink Session", ex); + } + } + + /** + * Get a TopLink Session for the given SessionFactory. Is aware of and will + * return any existing corresponding Session bound to the current thread, for + * example when using TopLinkTransactionManager. Will create a new Session + * otherwise, if "allowCreate" is true. + *

Same as getSession, but throwing the original TopLinkException. + * @param sessionFactory TopLink SessionFactory to create the session with + * @param allowCreate if a non-transactional Session should be created when no + * transactional Session can be found for the current thread + * @return the TopLink Session + * @throws TopLinkException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and + * "allowCreate" is false + * @see #releaseSession + * @see TopLinkTemplate + */ + public static Session doGetSession(SessionFactory sessionFactory, boolean allowCreate) + throws TopLinkException, IllegalStateException { + + Assert.notNull(sessionFactory, "No SessionFactory specified"); + + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + if (sessionHolder != null) { + return sessionHolder.getSession(); + } + + if (!allowCreate && !TransactionSynchronizationManager.isSynchronizationActive()) { + throw new IllegalStateException("No TopLink Session bound to thread, " + + "and configuration does not allow creation of non-transactional one here"); + } + + logger.debug("Creating TopLink Session"); + Session session = sessionFactory.createSession(); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + logger.debug("Registering new Spring transaction synchronization for new TopLink Session"); + // Use same Session for further TopLink actions within the transaction. + // Thread object will get removed by synchronization at transaction completion. + sessionHolder = new SessionHolder(session); + sessionHolder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.registerSynchronization( + new SessionSynchronization(sessionHolder, sessionFactory)); + TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); + } + + return session; + } + + /** + * Return whether the given TopLink Session is transactional, that is, + * bound to the current thread by Spring's transaction facilities. + * @param session the TopLink Session to check + * @param sessionFactory TopLink SessionFactory that the Session was created with + * (can be null) + * @return whether the Session is transactional + */ + public static boolean isSessionTransactional(Session session, SessionFactory sessionFactory) { + if (sessionFactory == null) { + return false; + } + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); + return (sessionHolder != null && session == sessionHolder.getSession()); + } + + /** + * Convert the given TopLinkException to an appropriate exception from the + * org.springframework.dao hierarchy. + * @param ex TopLinkException that occured + * @return the corresponding DataAccessException instance + */ + public static DataAccessException convertTopLinkAccessException(TopLinkException ex) { + if (ex instanceof DatabaseException) { + // SQLException during TopLink access: only passed in here from custom code, + // as TopLinkTemplate will use SQLExceptionTranslator-based handling. + return new TopLinkJdbcException((DatabaseException) ex); + } + if (ex instanceof OptimisticLockException) { + return new TopLinkOptimisticLockingFailureException((OptimisticLockException) ex); + } + if (ex instanceof QueryException) { + return new TopLinkQueryException((QueryException) ex); + } + if (ex instanceof ConcurrencyException) { + return new ConcurrencyFailureException(ex.getMessage(), ex); + } + if (ex instanceof ConversionException) { + return new TypeMismatchDataAccessException(ex.getMessage(), ex); + } + // fallback + return new TopLinkSystemException(ex); + } + + /** + * Close the given Session, created via the given factory, + * if it is not managed externally (i.e. not bound to the thread). + * @param session the TopLink Session to close + * @param sessionFactory TopLink SessionFactory that the Session was created with + * (can be null) + */ + public static void releaseSession(Session session, SessionFactory sessionFactory) { + if (session == null) { + return; + } + // Only release non-transactional Sessions. + if (!isSessionTransactional(session, sessionFactory)) { + doRelease(session); + } + } + + /** + * Perform the actual releasing of the TopLink Session. + * @param session the TopLink Session to release + */ + private static void doRelease(Session session) { + if (session != null) { + logger.debug("Closing TopLink Session"); + try { + session.release(); + } + catch (TopLinkException ex) { + logger.debug("Could not close TopLink Session", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing TopLink Session", ex); + } + } + } + + + /** + * Callback for resource cleanup at the end of a Spring-managed JTA transaction, + * i.e. when participating in a JtaTransactionManager transaction. + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + private static class SessionSynchronization extends ResourceHolderSynchronization { + + public SessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory) { + super(sessionHolder, sessionFactory); + } + + protected boolean shouldReleaseBeforeCompletion() { + return false; + } + + protected void releaseResource(ResourceHolder resourceHolder, Object resourceKey) { + releaseSession(((SessionHolder) resourceHolder).getSession(), (SessionFactory) resourceKey); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionHolder.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionHolder.java new file mode 100644 index 00000000000..871bd19119e --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionHolder.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.sessions.Session; + +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Session holder, wrapping a TopLink Session. + * TopLinkTransactionManager binds instances of this class + * to the thread, for a given SessionFactory. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class SessionHolder extends ResourceHolderSupport { + + private final Session session; + + + /** + * Create a new SessionHolder for the given TopLink Session. + * @param session the TopLink Session + */ + public SessionHolder(Session session) { + Assert.notNull(session, "Session must not be null"); + this.session = session; + } + + /** + * Return this holder's TopLink Session. + */ + public Session getSession() { + return session; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionReadCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionReadCallback.java new file mode 100644 index 00000000000..3a020d33e66 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SessionReadCallback.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; +import oracle.toplink.sessions.UnitOfWork; + +/** + * Convenient abstract implementation of the TopLinkCallback interface, + * exposing either the plain TopLink Session or the TopLink UnitOfWork + * (which extends the Session interface) to code that reads persistent objects. + * + *

Exposes the UnitOfWork if there is an active one (that is, if we're running + * within a non-read-only transaction); else exposes the Session itself. + * This allows to modify returned objects within a transaction, which is + * often desired, while the same code will return shared cache objects + * if running outside a transaction. + * + *

If "enforceReadOnly" is demanded, the callback will always expose the + * Session itself, avoiding the UnitOfWork overhead in any case. + * + * @author Juergen Hoeller + * @since 1.2 + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + * @see #readFromSession(oracle.toplink.sessions.Session) + */ +public abstract class SessionReadCallback implements TopLinkCallback { + + private final boolean enforceReadOnly; + + /** + * Create a new SessionReadCallback, not enforcing read-only objects. + */ + public SessionReadCallback() { + this.enforceReadOnly = false; + } + + /** + * Create a new SessionReadCallback, enforcing read-only objects if demanded. + * @param enforceReadOnly whether to enforce returning read-only objects, + * even if running within a non-read-only transaction + */ + public SessionReadCallback(boolean enforceReadOnly) { + this.enforceReadOnly = enforceReadOnly; + } + + /** + * Determines the Session to work on (either the active UnitOfWork + * or the plain Session) and delegates to readFromSession. + * @see #readFromSession(oracle.toplink.sessions.Session) + */ + public final Object doInTopLink(Session session) throws TopLinkException { + Session sessionToUse = session; + if (!this.enforceReadOnly) { + UnitOfWork unitOfWork = session.getActiveUnitOfWork(); + if (unitOfWork != null) { + sessionToUse = unitOfWork; + } + } + return readFromSession(sessionToUse); + } + + /** + * Called with a Session to work on, either the active UnitOfWork + * or the plain Session (as determined by the transaction status). + * @param session the TopLink Session to perform read operations on + * @return a result object, or null if none + * @throws TopLinkException in case of TopLink errors + */ + protected abstract Object readFromSession(Session session) throws TopLinkException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SingleSessionFactory.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SingleSessionFactory.java new file mode 100644 index 00000000000..0dbe4c5fbb3 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/SingleSessionFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.sessions.DatabaseSession; +import oracle.toplink.sessions.Session; + +/** + * Simple implementation of the SessionFactory interface: always returns + * the passed-in Session as-is. + * + *

Useful for testing or standalone usage of TopLink-based data access objects. + * In a server environment, use ServerSessionFactory instead. + * + * @author Juergen Hoeller + * @since 1.2 + * @see ServerSessionFactory + */ +public class SingleSessionFactory implements SessionFactory { + + private final Session session; + + + /** + * Create a new SingleSessionFactory with the given Session. + * @param session the TopLink Session to hold + */ + public SingleSessionFactory(Session session) { + this.session = session; + } + + + /** + * Return the held TopLink Session as-is. + */ + public Session createSession() { + return this.session; + } + + /** + * Throws an UnsupportedOperationException: SingleSessionFactory does not + * support managed client Sessions. Use ServerSessionFactory instead. + */ + public Session createManagedClientSession() { + throw new UnsupportedOperationException("SingleSessionFactory does not support managed client Sessions"); + } + + /** + * Throws an UnsupportedOperationException: SingleSessionFactory does not + * support transaction-aware Sessions. Use ServerSessionFactory instead. + */ + public Session createTransactionAwareSession() { + throw new UnsupportedOperationException("SingleSessionFactory does not support transaction-aware Sessions"); + } + + + /** + * Shut the pre-configured TopLink Session down. + * @see oracle.toplink.sessions.DatabaseSession#logout() + * @see oracle.toplink.sessions.Session#release() + */ + public void close() { + if (this.session instanceof DatabaseSession) { + ((DatabaseSession) this.session).logout(); + } + this.session.release(); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkAccessor.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkAccessor.java new file mode 100644 index 00000000000..d1d9ba32e0b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkAccessor.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2006 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.toplink; + +import java.sql.SQLException; + +import oracle.toplink.exceptions.DatabaseException; +import oracle.toplink.exceptions.TopLinkException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * Base class for TopLinkTemplate and TopLinkInterceptor, defining common properties + * such as SessionFactory and JDBC exception translator. + * + *

Not intended to be used directly. See TopLinkTemplate and TopLinkInterceptor. + * + *

Thanks to Slavik Markovich for implementing the initial TopLink support prototype! + * + * @author Juergen Hoeller + * @since 1.2 + * @see TopLinkTemplate + * @see TopLinkInterceptor + */ +public abstract class TopLinkAccessor implements InitializingBean { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private SessionFactory sessionFactory; + + private SQLExceptionTranslator jdbcExceptionTranslator; + + + /** + * Set the the TopLink SessionFactory that should be used to create TopLink + * Sessions. This will usually be a ServerSessionFactory in a multi-threaded + * environment, but can also be a SingleSessionFactory for testing purposes + * or for standalone execution. + *

The passed-in SessionFactory will usually be asked for a plain Session + * to perform data access on, unless an active transaction with a thread-bound + * Session is found. + * @see ServerSessionFactory + * @see SingleSessionFactory + * @see SessionFactory#createSession() + * @see SessionFactoryUtils#getSession(SessionFactory, boolean) + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the TopLink SessionFactory that should be used to create + * TopLink Sessions. + */ + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + /** + * Set the JDBC exception translator for this instance. + *

Applied to any SQLException root cause of a TopLink DatabaseException. + * The default is to rely on TopLink's native exception translation. + * @param jdbcExceptionTranslator the exception translator + * @see oracle.toplink.exceptions.DatabaseException + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + /** + * Return the JDBC exception translator for this instance, if any. + */ + public SQLExceptionTranslator getJdbcExceptionTranslator() { + return this.jdbcExceptionTranslator; + } + + + /** + * Check that we were provided with a session to use + */ + public void afterPropertiesSet() { + if (this.sessionFactory == null) { + throw new IllegalArgumentException("sessionFactory is required"); + } + } + + + /** + * Convert the given TopLinkException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * TopLink DatabaseException, else rely on TopLink's default translation. + * @param ex TopLinkException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertTopLinkAccessException + * @see #setJdbcExceptionTranslator + */ + public DataAccessException convertTopLinkAccessException(TopLinkException ex) { + if (getJdbcExceptionTranslator() != null && ex instanceof DatabaseException) { + Throwable internalEx = ex.getInternalException(); + // Should always be a SQLException inside a DatabaseException. + if (internalEx instanceof SQLException) { + return getJdbcExceptionTranslator().translate( + "TopLink operation: " + ex.getMessage(), null, (SQLException) internalEx); + } + } + return SessionFactoryUtils.convertTopLinkAccessException(ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkCallback.java new file mode 100644 index 00000000000..a4a38591bbd --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkCallback.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; + +/** + * Callback interface for TopLink code. To be used with {@link TopLinkTemplate}'s + * execution methods, often as anonymous classes within a method implementation. + * A typical implementation will call TopLink Session CRUD to perform some + * operations on persistent objects. + * + *

The Session that gets passed into the doInTopLink method + * is usually a thread-safe ClientSession. Since this provides access to the + * TopLink shared cache, it is possible for implementations of this interface to return + * references to read-only objects from the shared cache. These objects + * must not be modified by application code outside of the DAO layer. + * If persistent objects need to be edited, they should be loaded from (or registered with) + * a TopLink UnitOfWork, or they should be explicitly copied and merged back into a + * UnitOfWork at a later point of time. + * + *

Users can access a UnitOfWork by using the getActiveUnitOfWork + * method on the Session. Normally, this will only be done when there is an + * active non-read-only transaction being managed by Spring's {@link TopLinkTransactionManager} + * or by an external transaction controller (usually a J2EE server's JTA provider, + * configured in TopLink). The getActiveUnitOfWork method will return + * null outside of a managed transaction. + * + * @author Juergen Hoeller + * @author James Clark + * @see TopLinkTemplate + * @see TopLinkTransactionManager + */ +public interface TopLinkCallback { + + /** + * Gets called by TopLinkTemplate.execute with an active + * Session. Does not need to care about activating or closing + * the TopLink Session, or handling transactions. + * + *

Note that write operations should usually be performed on the active + * UnitOfWork within an externally controlled transaction, through + * calling getActiveUnitOfWork. However, an implementation can also + * choose to use acquireUnitOfWork to create an independent + * UnitOfWork, which it needs to commit at the end of the operation. + * + *

Allows for returning a result object created within the callback, + * i.e. a domain object or a collection of domain objects. + * A thrown custom RuntimeException is treated as an application exception: + * It gets propagated to the caller of the template. + * + * @param session active TopLink Session + * @return a result object, or null if none + * @throws TopLinkException if thrown by the TopLink API + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + * @see oracle.toplink.sessions.Session#acquireUnitOfWork() + * @see TopLinkTemplate#execute + * @see TopLinkTemplate#executeFind + */ + Object doInTopLink(Session session) throws TopLinkException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkInterceptor.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkInterceptor.java new file mode 100644 index 00000000000..c49eb2e60c0 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkInterceptor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2006 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * This interceptor binds a new TopLink Session to the thread before a method + * call, closing and removing it afterwards in case of any method outcome. + * If there already is a pre-bound Session (e.g. from TopLinkTransactionManager, + * or from a surrounding TopLink-intercepted method), the interceptor simply + * takes part in it. + * + *

Application code must retrieve a TopLink Session via the + * SessionFactoryUtils.getSession method or - preferably - + * TopLink's own Session.getActiveSession() method, to be able to + * detect a thread-bound Session. Typically, the code will look like as follows: + * + *

+ * public void doSomeDataAccessAction() {
+ *   Session session = this.serverSession.getActiveSession();
+ *   ...
+ * }
+ * + * Note that this interceptor automatically translates TopLinkExceptions, + * via delegating to the SessionFactoryUtils.convertTopLikAccessException + * method that converts them to exceptions that are compatible with the + * org.springframework.dao exception hierarchy (like TopLinkTemplate does). + * This can be turned off if the raw exceptions are preferred. + * + *

This class can be considered a declarative alternative to TopLinkTemplate's + * callback approach. The advantages are: + *

+ * + *

The drawback is the dependency on interceptor configuration. However, note + * that this interceptor is usually not necessary in scenarios where the + * data access code always executes within transactions. A transaction will always + * have a thread-bound Session in the first place, so adding this interceptor to the + * configuration just adds value when potentially executing outside of transactions + * and/or when relying on exception translation. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class TopLinkInterceptor extends TopLinkAccessor implements MethodInterceptor { + + private boolean exceptionConversionEnabled = true; + + + /** + * Set whether to convert any TopLinkException raised to a Spring DataAccessException, + * compatible with the org.springframework.dao exception hierarchy. + *

Default is "true". Turn this flag off to let the caller receive raw exceptions + * as-is, without any wrapping. + * @see org.springframework.dao.DataAccessException + */ + public void setExceptionConversionEnabled(boolean exceptionConversionEnabled) { + this.exceptionConversionEnabled = exceptionConversionEnabled; + } + + + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + boolean existingTransaction = false; + Session session = SessionFactoryUtils.getSession(getSessionFactory(), true); + if (TransactionSynchronizationManager.hasResource(getSessionFactory())) { + logger.debug("Found thread-bound Session for TopLink interceptor"); + existingTransaction = true; + } + else { + logger.debug("Using new Session for TopLink interceptor"); + TransactionSynchronizationManager.bindResource(getSessionFactory(), new SessionHolder(session)); + } + try { + return methodInvocation.proceed(); + } + catch (TopLinkException ex) { + if (this.exceptionConversionEnabled) { + throw convertTopLinkAccessException(ex); + } + else { + throw ex; + } + } + finally { + if (existingTransaction) { + logger.debug("Not closing pre-bound TopLink Session after interceptor"); + } + else { + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + SessionFactoryUtils.releaseSession(session, getSessionFactory()); + } + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkJdbcException.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkJdbcException.java new file mode 100644 index 00000000000..75e84330dfb --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkJdbcException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.DatabaseException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * TopLink-specific subclass of DataAccessException, for JDBC exceptions + * that TopLink rethrew. + * + * @author Juergen Hoeller + * @see SessionFactoryUtils#convertTopLinkAccessException + * @since 1.2 + */ +public class TopLinkJdbcException extends UncategorizedDataAccessException { + + public TopLinkJdbcException(DatabaseException ex) { + super("JDBC exception on TopLink data access: " + ex.getMessage(), ex.getInternalException()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkOperations.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkOperations.java new file mode 100644 index 00000000000..42cd7a4902b --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkOperations.java @@ -0,0 +1,703 @@ +/* + * Copyright 2002-2006 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.toplink; + +import java.util.Collection; +import java.util.List; + +import oracle.toplink.expressions.Expression; +import oracle.toplink.queryframework.Call; +import oracle.toplink.queryframework.DatabaseQuery; +import oracle.toplink.sessions.ObjectCopyingPolicy; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that specifies a basic set of TopLink operations, + * implemented by {@link TopLinkTemplate}. Not often used, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Defines TopLinkTemplate's data access methods that + * mirror various TopLink {@link oracle.toplink.sessions.Session} / + * {@link oracle.toplink.sessions.UnitOfWork} methods. Users are + * strongly encouraged to read the TopLink javadocs for details + * on the semantics of those methods. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public interface TopLinkOperations { + + /** + * Execute the action specified by the given action object within a + * TopLink Session. Application exceptions thrown by the action object + * get propagated to the caller (can only be unchecked). TopLink exceptions + * are transformed into appropriate DAO ones. Allows for returning a + * result object, i.e. a domain object or a collection of domain objects. + *

Note: Callback code is not supposed to handle transactions itself! + * Use an appropriate transaction manager like TopLinkTransactionManager. + * @param action callback object that specifies the TopLink action + * @return a result object returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see TopLinkTransactionManager + * @see org.springframework.dao + * @see org.springframework.transaction + * @see oracle.toplink.sessions.Session + */ + Object execute(TopLinkCallback action) throws DataAccessException; + + /** + * Execute the specified action assuming that the result object is a + * Collection. This is a convenience method for executing TopLink queries + * within an action. + * @param action callback object that specifies the TopLink action + * @return a Collection result returned by the action, or null + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + */ + List executeFind(TopLinkCallback action) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for executing generic queries + //------------------------------------------------------------------------- + + /** + * Execute a given named query with the given arguments. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class that has the named query descriptor + * @param queryName the name of the query + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(String, Class) + */ + Object executeNamedQuery(Class entityClass, String queryName) throws DataAccessException; + + /** + * Execute a given named query with the given arguments. + * @param entityClass the entity class that has the named query descriptor + * @param queryName the name of the query + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(String, Class) + */ + Object executeNamedQuery(Class entityClass, String queryName, boolean enforceReadOnly) + throws DataAccessException; + + /** + * Execute a given named query with the given arguments. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class that has the named query descriptor + * @param queryName the name of the query + * @param args the arguments for the query (can be null) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(String, Class, java.util.Vector) + */ + Object executeNamedQuery(Class entityClass, String queryName, Object[] args) throws DataAccessException; + + /** + * Execute a given named query with the given arguments. + * @param entityClass the entity class that has the named query descriptor + * @param queryName the name of the query + * @param args the arguments for the query (can be null) + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(String, Class, java.util.Vector) + */ + Object executeNamedQuery(Class entityClass, String queryName, Object[] args, boolean enforceReadOnly) + throws DataAccessException; + + /** + * Execute the given query object with the given arguments. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param query the query object to execute (for example, + * a ReadObjectQuery or ReadAllQuery instance) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(oracle.toplink.queryframework.DatabaseQuery) + */ + Object executeQuery(DatabaseQuery query) throws DataAccessException; + + /** + * Execute the given query object with the given arguments. + * @param query the query object to execute (for example, + * a ReadObjectQuery or ReadAllQuery instance) + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(oracle.toplink.queryframework.DatabaseQuery) + */ + Object executeQuery(DatabaseQuery query, boolean enforceReadOnly) throws DataAccessException; + + /** + * Execute the given query object with the given arguments. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param query the query object to execute (for example, + * a ReadObjectQuery or ReadAllQuery instance) + * @param args the arguments for the query (can be null) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(oracle.toplink.queryframework.DatabaseQuery, java.util.Vector) + */ + Object executeQuery(DatabaseQuery query, Object[] args) throws DataAccessException; + + /** + * Execute the given query object with the given arguments. + * @param query the query object to execute (for example, + * a ReadObjectQuery or ReadAllQuery instance) + * @param args the arguments for the query (can be null) + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the result object or list of result objects for the query + * (can be cast to the entity class or Collection/List, respectively) + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#executeQuery(oracle.toplink.queryframework.DatabaseQuery, java.util.Vector) + */ + Object executeQuery(DatabaseQuery query, Object[] args, boolean enforceReadOnly) + throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for reading a specific set of objects + //------------------------------------------------------------------------- + + /** + * Read all entity instances of the given class. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @return the list of entity instances + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class) + */ + List readAll(Class entityClass) throws DataAccessException; + + /** + * Read all entity instances of the given class. + * @param entityClass the entity class + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the list of entity instances + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class) + */ + List readAll(Class entityClass, boolean enforceReadOnly) throws DataAccessException; + + /** + * Read all entity instances of the given class that match the given expression. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param expression the TopLink expression to match, + * usually built through the TopLink ExpressionBuilder + * @return the list of matching entity instances + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.expressions.Expression) + * @see oracle.toplink.expressions.ExpressionBuilder + */ + List readAll(Class entityClass, Expression expression) throws DataAccessException; + + /** + * Read all entity instances of the given class that match the given expression. + * @param entityClass the entity class + * @param expression the TopLink expression to match, + * usually built through the TopLink ExpressionBuilder + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the list of matching entity instances + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.expressions.Expression) + * @see oracle.toplink.expressions.ExpressionBuilder + */ + List readAll(Class entityClass, Expression expression, boolean enforceReadOnly) + throws DataAccessException; + + /** + * Read all entity instances of the given class, as returned by the given call. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param call the TopLink Call object to apply (either a SQLCall or an EJBQLCall) + * @return the list of matching entity instances + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.queryframework.Call) + * @see oracle.toplink.queryframework.SQLCall + * @see oracle.toplink.queryframework.EJBQLCall + */ + List readAll(Class entityClass, Call call) throws DataAccessException; + + /** + * Read all entity instances of the given class, as returned by the given call. + * @param entityClass the entity class + * @param call the TopLink Call object to apply (either a SQLCall or an EJBQLCall) + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the list of matching entity instances + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.expressions.Expression) + * @see oracle.toplink.queryframework.SQLCall + * @see oracle.toplink.queryframework.EJBQLCall + */ + List readAll(Class entityClass, Call call, boolean enforceReadOnly) throws DataAccessException; + + /** + * Read an entity instance of the given class that matches the given expression. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param expression the TopLink expression to match, + * usually built through the TopLink ExpressionBuilder + * @return the matching entity instance, or null if none found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.expressions.Expression) + * @see oracle.toplink.expressions.ExpressionBuilder + */ + Object read(Class entityClass, Expression expression) throws DataAccessException; + + /** + * Read an entity instance of the given class that matches the given expression. + * @param entityClass the entity class + * @param expression the TopLink expression to match, + * usually built through the TopLink ExpressionBuilder + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return a matching entity instance, or null if none found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.expressions.Expression) + * @see oracle.toplink.expressions.ExpressionBuilder + */ + Object read(Class entityClass, Expression expression, boolean enforceReadOnly) + throws DataAccessException; + + /** + * Read an entity instance of the given class, as returned by the given call. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param call the TopLink Call object to apply (either a SQLCall or an EJBQLCall) + * @return a matching entity instance, or null if none found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.queryframework.Call) + * @see oracle.toplink.queryframework.SQLCall + * @see oracle.toplink.queryframework.EJBQLCall + */ + Object read(Class entityClass, Call call) throws DataAccessException; + + /** + * Read an entity instance of the given class, as returned by the given call. + * @param entityClass the entity class + * @param call the TopLink Call object to apply (either a SQLCall or an EJBQLCall) + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return a matching entity instance, or null if none found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#readAllObjects(Class, oracle.toplink.expressions.Expression) + * @see oracle.toplink.queryframework.SQLCall + * @see oracle.toplink.queryframework.EJBQLCall + */ + Object read(Class entityClass, Call call, boolean enforceReadOnly) + throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for reading an individual object by id + //------------------------------------------------------------------------- + + /** + * Read the entity instance of the given class with the given id, + * throwing an exception if not found. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param id the id of the desired object + * @return the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + */ + Object readById(Class entityClass, Object id) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given id, + * throwing an exception if not found. + * @param entityClass the entity class + * @param id the id of the desired object + * @return the entity instance + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + */ + Object readById(Class entityClass, Object id, boolean enforceReadOnly) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given composite id, + * throwing an exception if not found. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param keys the composite id elements of the desired object + * @return the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + */ + Object readById(Class entityClass, Object[] keys) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given composite id, + * throwing an exception if not found. + * @param entityClass the entity class + * @param keys the composite id elements of the desired object + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + */ + Object readById(Class entityClass, Object[] keys, boolean enforceReadOnly) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given id, + * throwing an exception if not found. A detached copy of the entity object + * will be returned, allowing for modifications outside the current transaction, + * with the changes to be merged into a later transaction. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param id the id of the desired object + * @return a copy of the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + Object readAndCopy(Class entityClass, Object id) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given id, + * throwing an exception if not found. A detached copy of the entity object + * will be returned, allowing for modifications outside the current transaction, + * with the changes to be merged into a later transaction. + * @param entityClass the entity class + * @param id the id of the desired object + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return a copy of the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + Object readAndCopy(Class entityClass, Object id, boolean enforceReadOnly) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given composite id, + * throwing an exception if not found. A detached copy of the entity object + * will be returned, allowing for modifications outside the current transaction, + * with the changes to be merged into a later transaction. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entityClass the entity class + * @param keys the composite id elements of the desired object + * @return a copy of the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + Object readAndCopy(Class entityClass, Object[] keys) throws DataAccessException; + + /** + * Read the entity instance of the given class with the given composite id, + * throwing an exception if not found. A detached copy of the entity object + * will be returned, allowing for modifications outside the current transaction, + * with the changes to be merged into a later transaction. + * @param entityClass the entity class + * @param keys the composite id elements of the desired object + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return a copy of the entity instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.queryframework.ReadObjectQuery#setSelectionKey(java.util.Vector) + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + Object readAndCopy(Class entityClass, Object[] keys, boolean enforceReadOnly) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for copying and refreshing objects + //------------------------------------------------------------------------- + + /** + * Create a detached copy of the given entity object, + * using TopLink's default ObjectCopyingPolicy. + * @param entity the entity object to copy + * @return the copy of the entity object + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + Object copy(Object entity) throws DataAccessException; + + /** + * Create a detached copy of the given entity object. + * @param entity the entity object to copy + * @param copyingPolicy the TopLink ObjectCopyingPolicy to apply + * @return the copy of the entity object + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#copyObject(Object, oracle.toplink.sessions.ObjectCopyingPolicy) + */ + Object copy(Object entity, ObjectCopyingPolicy copyingPolicy) throws DataAccessException; + + /** + * Create detached copies of all given entity objects, + * using TopLink's default ObjectCopyingPolicy. + * @param entities the entity objects to copy + * @return the copies of the entity objects + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + List copyAll(Collection entities) throws DataAccessException; + + /** + * Create detached copies of all given entity objects. + * @param entities the entity objects to copy + * @param copyingPolicy the TopLink ObjectCopyingPolicy to apply + * @return the copies of the entity objects + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#copyObject(Object) + */ + List copyAll(Collection entities, ObjectCopyingPolicy copyingPolicy) throws DataAccessException; + + /** + * Refresh the given entity object, returning the refreshed object. + *

The returned object will only be different from the passed-in object + * if the passed-in object is not the currently registered version of + * the corresponding entity. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entity the entity object to refresh + * @return the refreshed version of the entity object + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#refreshObject(Object) + */ + Object refresh(Object entity) throws DataAccessException; + + /** + * Refresh the given entity object, returning the refreshed object. + *

The returned object will only be different from the passed-in object + * if the passed-in object is not the currently registered version of + * the corresponding entity. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entity the entity object to refresh + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the refreshed version of the entity object + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#refreshObject(Object) + */ + Object refresh(Object entity, boolean enforceReadOnly) throws DataAccessException; + + /** + * Refresh the given entity objects, returning the corresponding refreshed objects. + *

A returned object will only be different from the corresponding passed-in + * object if the passed-in object is not the currently registered version of + * the corresponding entity. + *

Retrieves read-write objects from the TopLink UnitOfWork in case of a + * non-read-only transaction, and read-only objects else. + * @param entities the entity objects to refresh + * @return the refreshed versions of the entity objects + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#refreshObject(Object) + */ + List refreshAll(Collection entities) throws DataAccessException; + + /** + * Refresh the given entity objects, returning the corresponding refreshed objects. + *

A returned object will only be different from the corresponding passed-in + * object if the passed-in object is not the currently registered version of + * the corresponding entity. + * @param entities the entity objects to refresh + * @param enforceReadOnly whether to always retrieve read-only objects from + * the plain TopLink Session (else, read-write objects will be retrieved + * from the TopLink UnitOfWork in case of a non-read-only transaction) + * @return the refreshed versions of the entity objects + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.Session#refreshObject(Object) + */ + List refreshAll(Collection entities, boolean enforceReadOnly) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for persisting and deleting objects + //------------------------------------------------------------------------- + + /** + * Register the given (new or existing) entity with the current UnitOfWork. + *

The entity will be checked for existence, according to TopLink's + * configured existence checking policy. To avoid the (potentially costly) + * existence check, consider using the specific registerNew + * or registerExisting method. + * Do not edit the passed-in object any further afterwards. + * @param entity the entity to register + * @return the registered clone of the original object, + * which needs to be used for further editing + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#registerObject(Object) + * @see #registerNew(Object) + * @see #registerExisting(Object) + */ + Object register(Object entity); + + /** + * Register all given entities with the current UnitOfWork. + * Do not edit the passed-in objects any further afterwards. + * @param entities the entities to register + * @return the registered clones of the original objects, + * which need to be used for further editing + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#registerAllObjects(java.util.Collection) + */ + List registerAll(Collection entities); + + /** + * Register the given new entity with the current UnitOfWork. + * The passed-in object can be edited further afterwards. + * @param entity the new entity to register + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#registerNewObject(Object) + */ + void registerNew(Object entity); + + /** + * Register the given existing entity with the current UnitOfWork. + * Do not edit the passed-in object any further afterwards. + * @param entity the existing entity to register + * @return the registered clone of the original object, + * which needs to be used for further editing + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#registerExistingObject(Object) + */ + Object registerExisting(Object entity); + + /** + * Reassociate the given entity copy with the current UnitOfWork, + * using simple merging. + *

The given object will not be reassociated itself: instead, the state + * will be copied onto the persistent object with the same identifier. + * In case of a new entity, merge will copy to a registered object as well, + * but will also update the identifier of the passed-in object. + * @param entity the updated copy to merge + * @return the updated, registered persistent instance + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#mergeClone(Object) + */ + Object merge(Object entity) throws DataAccessException; + + /** + * Reassociate the given entity copy with the current UnitOfWork, + * using deep merging of all contained entities. + *

The given object will not be reassociated itself: instead, the state + * will be copied onto the persistent object with the same identifier. + * In case of a new entity, merge will register a copy as well, + * but will also update the identifier of the passed-in object. + * @param entity the updated copy to merge + * @return the updated, registered persistent instance + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#deepMergeClone(Object) + */ + Object deepMerge(Object entity) throws DataAccessException; + + /** + * Reassociate the given entity copy with the current UnitOfWork, + * using shallow merging of the entity instance. + *

The given object will not be reassociated itself: instead, the state + * will be copied onto the persistent object with the same identifier. + * In case of a new entity, merge will register a copy as well, + * but will also update the identifier of the passed-in object. + * @param entity the updated copy to merge + * @return the updated, registered persistent instance + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#shallowMergeClone(Object) + */ + Object shallowMerge(Object entity) throws DataAccessException; + + /** + * Reassociate the given entity copy with the current UnitOfWork, + * using merging with all references from this clone. + *

The given object will not be reassociated itself: instead, the state + * will be copied onto the persistent object with the same identifier. + * In case of a new entity, merge will register a copy as well, + * but will also update the identifier of the passed-in object. + * @param entity the updated copy to merge + * @return the updated, registered persistent instance + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#mergeCloneWithReferences(Object) + */ + Object mergeWithReferences(Object entity) throws DataAccessException; + + /** + * Delete the given entity. + * @param entity the entity to delete + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#deleteObject(Object) + */ + void delete(Object entity) throws DataAccessException; + + /** + * Delete all given entities. + * @param entities the entities to delete + * @throws org.springframework.dao.DataAccessException in case of TopLink errors + * @see oracle.toplink.sessions.UnitOfWork#deleteAllObjects(java.util.Collection) + */ + void deleteAll(Collection entities) throws DataAccessException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkOptimisticLockingFailureException.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkOptimisticLockingFailureException.java new file mode 100644 index 00000000000..84f1e2eac2d --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkOptimisticLockingFailureException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 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.toplink; + +import oracle.toplink.exceptions.OptimisticLockException; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +/** + * TopLink-specific subclass of ObjectOptimisticLockingFailureException. + * Converts TopLink's OptimisticLockException. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class TopLinkOptimisticLockingFailureException extends ObjectOptimisticLockingFailureException { + + public TopLinkOptimisticLockingFailureException(OptimisticLockException ex) { + super(ex.getObject() != null ? ex.getObject().getClass() : null, null, ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkQueryException.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkQueryException.java new file mode 100644 index 00000000000..90f264d2c23 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkQueryException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 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.toplink; + +import oracle.toplink.exceptions.QueryException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * TopLink-specific subclass of InvalidDataAccessResourceUsageException, + * thrown on invalid TopLink query syntax or behavior. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class TopLinkQueryException extends InvalidDataAccessResourceUsageException { + + public TopLinkQueryException(QueryException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkSystemException.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkSystemException.java new file mode 100644 index 00000000000..6a9846ba6ba --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkSystemException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * TopLink-specific subclass of UncategorizedDataAccessException, + * for TopLink system errors that do not match any concrete + * org.springframework.dao exceptions. + * + * @author Juergen Hoeller + * @since 1.2 + * @see SessionFactoryUtils#convertTopLinkAccessException + */ +public class TopLinkSystemException extends UncategorizedDataAccessException { + + public TopLinkSystemException(TopLinkException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkTemplate.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkTemplate.java new file mode 100644 index 00000000000..86a8aeca2c9 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkTemplate.java @@ -0,0 +1,515 @@ +/* + * Copyright 2002-2007 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.toplink; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.expressions.Expression; +import oracle.toplink.queryframework.Call; +import oracle.toplink.queryframework.DatabaseQuery; +import oracle.toplink.queryframework.ReadObjectQuery; +import oracle.toplink.sessions.ObjectCopyingPolicy; +import oracle.toplink.sessions.Session; +import oracle.toplink.sessions.UnitOfWork; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Helper class that simplifies TopLink data access code, and converts + * TopLinkExceptions into unchecked DataAccessExceptions, following the + * org.springframework.dao exception hierarchy. + * + *

The central method is execute, supporting TopLink access code + * implementing the {@link TopLinkCallback} interface. It provides TopLink Session + * handling such that neither the TopLinkCallback implementation nor the calling + * code needs to explicitly care about retrieving/closing TopLink Sessions, + * or handling Session lifecycle exceptions. For typical single step actions, + * there are various convenience methods (read, readAll, merge, delete, etc). + * + *

Can be used within a service implementation via direct instantiation + * with a SessionFactory reference, or get prepared in an application context + * and given to services as bean reference. Note: The SessionFactory 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. + * + *

This class can be considered as direct alternative to working with the raw + * TopLink Session API (through SessionFactoryUtils.getSession()). + * The major advantage is its automatic conversion to DataAccessExceptions, the + * major disadvantage that no checked application exceptions can get thrown from + * within data access code. Corresponding checks and the actual throwing of such + * exceptions can often be deferred to after callback execution, though. + * + *

{@link LocalSessionFactoryBean} is the preferred way of obtaining a reference + * to a specific TopLink SessionFactory. It will usually be configured to + * create ClientSessions for a ServerSession held by it, allowing for seamless + * multi-threaded execution. The Spring application context will manage its lifecycle, + * initializing and shutting down the factory as part of the application. + * + *

Thanks to Slavik Markovich for implementing the initial TopLink support prototype! + * + * @author Juergen Hoeller + * @author James Clark + * @since 1.2 + * @see #setSessionFactory + * @see TopLinkCallback + * @see oracle.toplink.sessions.Session + * @see TopLinkInterceptor + * @see LocalSessionFactoryBean + * @see TopLinkTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class TopLinkTemplate extends TopLinkAccessor implements TopLinkOperations { + + private boolean allowCreate = true; + + + /** + * Create a new TopLinkTemplate instance. + */ + public TopLinkTemplate() { + } + + /** + * Create a new TopLinkTemplate instance. + */ + public TopLinkTemplate(SessionFactory sessionFactory) { + setSessionFactory(sessionFactory); + afterPropertiesSet(); + } + + /** + * Create a new TopLinkTemplate instance. + * @param allowCreate if a new Session should be created if no thread-bound found + */ + public TopLinkTemplate(SessionFactory sessionFactory, boolean allowCreate) { + setSessionFactory(sessionFactory); + setAllowCreate(allowCreate); + afterPropertiesSet(); + } + + /** + * Set if a new Session should be created when no transactional Session + * can be found for the current thread. + *

TopLinkTemplate is aware of a corresponding Session bound to the + * current thread, for example when using TopLinkTransactionManager. + * If allowCreate is true, a new non-transactional Session will be created + * if none found, which needs to be closed at the end of the operation. + * If false, an IllegalStateException will get thrown in this case. + * @see SessionFactoryUtils#getSession(SessionFactory, boolean) + */ + public void setAllowCreate(boolean allowCreate) { + this.allowCreate = allowCreate; + } + + /** + * Return if a new Session should be created if no thread-bound found. + */ + public boolean isAllowCreate() { + return this.allowCreate; + } + + + public Object execute(TopLinkCallback action) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + Session session = SessionFactoryUtils.getSession(getSessionFactory(), this.allowCreate); + try { + return action.doInTopLink(session); + } + catch (TopLinkException ex) { + throw convertTopLinkAccessException(ex); + } + catch (RuntimeException ex) { + // callback code threw application exception + throw ex; + } + finally { + SessionFactoryUtils.releaseSession(session, getSessionFactory()); + } + } + + public List executeFind(TopLinkCallback action) throws DataAccessException { + Object result = execute(action); + if (result != null && !(result instanceof List)) { + throw new InvalidDataAccessApiUsageException( + "Result object returned from TopLinkCallback isn't a List: [" + result + "]"); + } + return (List) result; + } + + + //------------------------------------------------------------------------- + // Convenience methods for executing generic queries + //------------------------------------------------------------------------- + + public Object executeNamedQuery(Class entityClass, String queryName) throws DataAccessException { + return executeNamedQuery(entityClass, queryName, null, false); + } + + public Object executeNamedQuery(Class entityClass, String queryName, boolean enforceReadOnly) + throws DataAccessException { + + return executeNamedQuery(entityClass, queryName, null, enforceReadOnly); + } + + public Object executeNamedQuery(Class entityClass, String queryName, Object[] args) + throws DataAccessException { + + return executeNamedQuery(entityClass, queryName, args, false); + } + + public Object executeNamedQuery( + final Class entityClass, final String queryName, final Object[] args, final boolean enforceReadOnly) + throws DataAccessException { + + return execute(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + if (args != null) { + return session.executeQuery(queryName, entityClass, new Vector(Arrays.asList(args))); + } + else { + return session.executeQuery(queryName, entityClass, new Vector()); + } + } + }); + } + + public Object executeQuery(DatabaseQuery query) throws DataAccessException { + return executeQuery(query, null, false); + } + + public Object executeQuery(DatabaseQuery query, boolean enforceReadOnly) throws DataAccessException { + return executeQuery(query, null, enforceReadOnly); + } + + public Object executeQuery(DatabaseQuery query, Object[] args) throws DataAccessException { + return executeQuery(query, args, false); + } + + public Object executeQuery(final DatabaseQuery query, final Object[] args, final boolean enforceReadOnly) + throws DataAccessException { + + return execute(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + if (args != null) { + return session.executeQuery(query, new Vector(Arrays.asList(args))); + } + else { + return session.executeQuery(query); + } + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience methods for reading a specific set of objects + //------------------------------------------------------------------------- + + public List readAll(Class entityClass) throws DataAccessException { + return readAll(entityClass, false); + } + + public List readAll(final Class entityClass, final boolean enforceReadOnly) throws DataAccessException { + return executeFind(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + return session.readAllObjects(entityClass); + } + }); + } + + public List readAll(Class entityClass, Expression expression) throws DataAccessException { + return readAll(entityClass, expression, false); + } + + public List readAll(final Class entityClass, final Expression expression, final boolean enforceReadOnly) + throws DataAccessException { + + return executeFind(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + return session.readAllObjects(entityClass, expression); + } + }); + } + + public List readAll(Class entityClass, Call call) throws DataAccessException { + return readAll(entityClass, call, false); + } + + public List readAll(final Class entityClass, final Call call, final boolean enforceReadOnly) + throws DataAccessException { + + return executeFind(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + return session.readAllObjects(entityClass, call); + } + }); + } + + public Object read(Class entityClass, Expression expression) throws DataAccessException { + return read(entityClass, expression, false); + } + + public Object read(final Class entityClass, final Expression expression, final boolean enforceReadOnly) + throws DataAccessException { + + return execute(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + return session.readObject(entityClass, expression); + } + }); + } + + public Object read(Class entityClass, Call call) throws DataAccessException { + return read(entityClass, call, false); + } + + public Object read(final Class entityClass, final Call call, final boolean enforceReadOnly) + throws DataAccessException { + + return execute(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + return session.readObject(entityClass, call); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience methods for reading an individual object by id + //------------------------------------------------------------------------- + + public Object readById(Class entityClass, Object id) throws DataAccessException { + return readById(entityClass, id, false); + } + + public Object readById(Class entityClass, Object id, boolean enforceReadOnly) throws DataAccessException { + return readById(entityClass, new Object[] {id}, enforceReadOnly); + } + + public Object readById(Class entityClass, Object[] keys) throws DataAccessException { + return readById(entityClass, keys, false); + } + + public Object readById(final Class entityClass, final Object[] keys, final boolean enforceReadOnly) + throws DataAccessException { + + Assert.isTrue(keys != null && keys.length > 0, "Non-empty keys or id is required"); + + ReadObjectQuery query = new ReadObjectQuery(entityClass); + query.setSelectionKey(new Vector(Arrays.asList(keys))); + Object result = executeQuery(query, enforceReadOnly); + + if (result == null) { + Object identifier = (keys.length == 1 ? keys[0] : StringUtils.arrayToCommaDelimitedString(keys)); + throw new ObjectRetrievalFailureException(entityClass, identifier); + } + return result; + } + + public Object readAndCopy(Class entityClass, Object id) throws DataAccessException { + return readAndCopy(entityClass, id, false); + } + + public Object readAndCopy(Class entityClass, Object id, boolean enforceReadOnly) + throws DataAccessException { + + Object entity = readById(entityClass, id, enforceReadOnly); + return copy(entity); + } + + public Object readAndCopy(Class entityClass, Object[] keys) throws DataAccessException { + return readAndCopy(entityClass, keys, false); + } + + public Object readAndCopy(Class entityClass, Object[] keys, boolean enforceReadOnly) + throws DataAccessException { + + Object entity = readById(entityClass, keys, enforceReadOnly); + return copy(entity); + } + + + //------------------------------------------------------------------------- + // Convenience methods for copying and refreshing objects + //------------------------------------------------------------------------- + + public Object copy(Object entity) throws DataAccessException { + ObjectCopyingPolicy copyingPolicy = new ObjectCopyingPolicy(); + copyingPolicy.cascadeAllParts(); + copyingPolicy.setShouldResetPrimaryKey(false); + return copy(entity, copyingPolicy); + } + + public Object copy(final Object entity, final ObjectCopyingPolicy copyingPolicy) + throws DataAccessException { + + return execute(new TopLinkCallback() { + public Object doInTopLink(Session session) throws TopLinkException { + return session.copyObject(entity, copyingPolicy); + } + }); + } + + public List copyAll(Collection entities) throws DataAccessException { + ObjectCopyingPolicy copyingPolicy = new ObjectCopyingPolicy(); + copyingPolicy.cascadeAllParts(); + copyingPolicy.setShouldResetPrimaryKey(false); + return copyAll(entities, copyingPolicy); + } + + public List copyAll(final Collection entities, final ObjectCopyingPolicy copyingPolicy) + throws DataAccessException { + + return (List) execute(new TopLinkCallback() { + public Object doInTopLink(Session session) throws TopLinkException { + List result = new ArrayList(entities.size()); + for (Iterator it = entities.iterator(); it.hasNext();) { + Object entity = it.next(); + result.add(session.copyObject(entity, copyingPolicy)); + } + return result; + } + }); + } + + public Object refresh(Object entity) throws DataAccessException { + return refresh(entity, false); + } + + public Object refresh(final Object entity, final boolean enforceReadOnly) throws DataAccessException { + return execute(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + return session.refreshObject(entity); + } + }); + } + + public List refreshAll(Collection entities) throws DataAccessException { + return refreshAll(entities, false); + } + + public List refreshAll(final Collection entities, final boolean enforceReadOnly) throws DataAccessException { + return (List) execute(new SessionReadCallback(enforceReadOnly) { + protected Object readFromSession(Session session) throws TopLinkException { + List result = new ArrayList(entities.size()); + for (Iterator it = entities.iterator(); it.hasNext();) { + Object entity = it.next(); + result.add(session.refreshObject(entity)); + } + return result; + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience methods for persisting and deleting objects + //------------------------------------------------------------------------- + + public Object register(final Object entity) { + return execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.registerObject(entity); + } + }); + } + + public List registerAll(final Collection entities) { + return (List) execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.registerAllObjects(entities); + } + }); + } + + public void registerNew(final Object entity) { + execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.registerNewObject(entity); + } + }); + } + + public Object registerExisting(final Object entity) { + return execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.registerExistingObject(entity); + } + }); + } + + public Object merge(final Object entity) throws DataAccessException { + return execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.mergeClone(entity); + } + }); + } + + public Object deepMerge(final Object entity) throws DataAccessException { + return execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.deepMergeClone(entity); + } + }); + } + + public Object shallowMerge(final Object entity) throws DataAccessException { + return execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.shallowMergeClone(entity); + } + }); + } + + public Object mergeWithReferences(final Object entity) throws DataAccessException { + return execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.mergeCloneWithReferences(entity); + } + }); + } + + public void delete(final Object entity) throws DataAccessException { + execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + return unitOfWork.deleteObject(entity); + } + }); + } + + public void deleteAll(final Collection entities) throws DataAccessException { + execute(new UnitOfWorkCallback() { + protected Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException { + unitOfWork.deleteAllObjects(entities); + return null; + } + }); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkTransactionManager.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkTransactionManager.java new file mode 100644 index 00000000000..5fc29b48fdd --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/TopLinkTransactionManager.java @@ -0,0 +1,480 @@ +/* + * Copyright 2002-2008 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.toplink; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import oracle.toplink.exceptions.DatabaseException; +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.internal.databaseaccess.Accessor; +import oracle.toplink.internal.databaseaccess.DatabaseAccessor; +import oracle.toplink.sessions.Session; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} implementation + * for a single TopLink {@link SessionFactory}. Binds a TopLink Session from the + * specified factory to the thread, potentially allowing for one thread-bound Session + * per factory. {@link SessionFactoryUtils} and {@link TopLinkTemplate} are aware + * of thread-bound Sessions and participate in such transactions automatically. + * Using either of those or going through Session.getActiveUnitOfWork() is + * required for TopLink access code supporting this transaction handling mechanism. + * + *

This transaction manager is appropriate for applications that use a single + * TopLink SessionFactory for transactional data access. JTA (usually through + * {@link org.springframework.transaction.jta.JtaTransactionManager}) is necessary + * for accessing multiple transactional resources within the same transaction. + * Note that you need to configure TopLink with an appropriate external transaction + * controller in order to make it participate in JTA transactions. + * + *

This transaction manager also supports direct DataSource access within a transaction + * (i.e. plain JDBC code working with the same DataSource), but only for transactions + * that are not marked as read-only. This allows for mixing services which + * access TopLink and services which use plain JDBC (without being aware of TopLink)! + * Application code needs to stick to the same simple Connection lookup pattern as + * with {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} + * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection} + * or going through a + * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}). + * + *

Note: To be able to register a DataSource's Connection for plain JDBC code, + * this instance needs to be aware of the DataSource ({@link #setDataSource}). + * The given DataSource should obviously match the one used by the given TopLink + * SessionFactory. + * + *

On JDBC 3.0, this transaction manager supports nested transactions via JDBC 3.0 + * Savepoints. The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} + * flag defaults to "false", though, as nested transactions will just apply to the + * JDBC Connection, not to the TopLink PersistenceManager and its cached objects. + * You can manually set the flag to "true" if you want to use nested transactions + * for JDBC access code which participates in TopLink transactions (provided that + * your JDBC driver supports Savepoints). Note that TopLink itself does not + * support nested transactions! Hence, do not expect TopLink access code to + * semantically participate in a nested transaction. + * + *

Thanks to Slavik Markovich for implementing the initial TopLink support prototype! + * + * @author Juergen Hoeller + * @author James Clark + * @since 1.2 + * @see #setSessionFactory + * @see #setDataSource + * @see LocalSessionFactoryBean + * @see SessionFactoryUtils#getSession + * @see SessionFactoryUtils#releaseSession + * @see TopLinkTemplate + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class TopLinkTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private SessionFactory sessionFactory; + + private DataSource dataSource; + + private boolean lazyDatabaseTransaction = false; + + private SQLExceptionTranslator jdbcExceptionTranslator; + + + /** + * Create a new TopLinkTransactionManager instance. + * A SessionFactory has to be specified to be able to use it. + * @see #setSessionFactory + */ + public TopLinkTransactionManager() { + } + + /** + * Create a new TopLinkTransactionManager instance. + * @param sessionFactory the TopLink SessionFactory to manage transactions for + */ + public TopLinkTransactionManager(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + afterPropertiesSet(); + } + + /** + * Set the the TopLink SessionFactory to manage transactions for. + * This will usually be a ServerSessionFactory. + *

The passed-in SessionFactory will be asked for a plain Session + * in case of a read-only transaction (where no active UnitOfWork is + * supposed to be available), and for a managed Session else (with an + * active UnitOfWork that will be committed by this transaction manager). + * @see ServerSessionFactory + * @see SessionFactory#createSession() + * @see SessionFactory#createManagedClientSession() + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the SessionFactory that this instance should manage transactions for. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + * The DataSource should match the one used by the TopLink SessionFactory: + * for example, you could specify the same JNDI DataSource for both. + *

A transactional JDBC Connection for this DataSource will be provided to + * application code accessing this DataSource directly via DataSourceUtils + * or JdbcTemplate. The Connection will be taken from the TopLink Session. + * This will only happen for transactions that are not marked + * as read-only. TopLink does not support database transactions for pure + * read-only operations on a Session (that is, without a UnitOfWork). + *

Note that you need to use a TopLink Session with a DatabaseAccessor + * to allow for exposing TopLink transactions as JDBC transactions. This is + * the case of all standard TopLink configurations. + *

The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy + * @see org.springframework.jdbc.datasource.DataSourceUtils + * @see org.springframework.jdbc.core.JdbcTemplate + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set whether to lazily start a database transaction within a TopLink + * transaction. + *

By default, database transactions are started early. This allows + * for reusing the same JDBC Connection throughout an entire transaction, + * including read operations, and also for exposing TopLink transactions + * to JDBC access code (working on the same DataSource). + *

It is only recommended to switch this flag to "true" when no JDBC access + * code is involved in any of the transactions, and when it is acceptable to + * perform read operations outside of the transactional JDBC Connection. + * @see #setDataSource(javax.sql.DataSource) + * @see oracle.toplink.sessions.UnitOfWork#beginEarlyTransaction() + */ + public void setLazyDatabaseTransaction(boolean lazyDatabaseTransaction) { + this.lazyDatabaseTransaction = lazyDatabaseTransaction; + } + + /** + * Return whether to lazily start a database transaction within a TopLink + * transaction. + */ + public boolean isLazyDatabaseTransaction() { + return this.lazyDatabaseTransaction; + } + + /** + * Set the JDBC exception translator for this transaction manager. + *

Applied to any SQLException root cause of a TopLink DatabaseException + * that is thrown on commit. The default is to rely on TopLink's native + * exception translation. + * @param jdbcExceptionTranslator the exception translator + * @see oracle.toplink.exceptions.DatabaseException + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + * @see #setDataSource(javax.sql.DataSource) + */ + public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) { + this.jdbcExceptionTranslator = jdbcExceptionTranslator; + } + + /** + * Return the JDBC exception translator for this transaction manager, if any. + */ + public SQLExceptionTranslator getJdbcExceptionTranslator() { + return this.jdbcExceptionTranslator; + } + + public void afterPropertiesSet() { + if (getSessionFactory() == null) { + throw new IllegalArgumentException("Property 'sessionFactory' is required"); + } + } + + + public Object getResourceFactory() { + return getSessionFactory(); + } + + protected Object doGetTransaction() { + TopLinkTransactionObject txObject = new TopLinkTransactionObject(); + SessionHolder sessionHolder = (SessionHolder) + TransactionSynchronizationManager.getResource(this.sessionFactory); + txObject.setSessionHolder(sessionHolder); + return txObject; + } + + protected boolean isExistingTransaction(Object transaction) { + TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction; + return (txObject.getSessionHolder() != null); + } + + protected void doBegin(Object transaction, TransactionDefinition definition) { + Session session = null; + + try { + if (!definition.isReadOnly()) { + logger.debug("Creating managed TopLink Session with active UnitOfWork for read-write transaction"); + session = getSessionFactory().createManagedClientSession(); + } + else { + logger.debug("Creating plain TopLink Session without active UnitOfWork for read-only transaction"); + session = getSessionFactory().createSession(); + } + + if (logger.isDebugEnabled()) { + logger.debug("Opened new session [" + session + "] for TopLink transaction"); + } + + TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction; + txObject.setSessionHolder(new SessionHolder(session)); + txObject.getSessionHolder().setSynchronizedWithTransaction(true); + + // Register transaction timeout. + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getSessionHolder().setTimeoutInSeconds(timeout); + } + + // Enforce early database transaction for TopLink read-write transaction, + // unless we are explicitly told to use lazy transactions. + if (!definition.isReadOnly() && !isLazyDatabaseTransaction()) { + session.getActiveUnitOfWork().beginEarlyTransaction(); + } + + // Register the TopLink Session's JDBC Connection for the DataSource, if set. + if (getDataSource() != null) { + Session mostSpecificSession = (!definition.isReadOnly() ? session.getActiveUnitOfWork() : session); + Connection con = getJdbcConnection(mostSpecificSession); + if (con != null) { + ConnectionHolder conHolder = new ConnectionHolder(con); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + conHolder.setTimeoutInSeconds(timeout); + } + if (logger.isDebugEnabled()) { + logger.debug("Exposing TopLink transaction as JDBC transaction [" + con + "]"); + } + TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); + txObject.setConnectionHolder(conHolder); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Not exposing TopLink transaction [" + session + + "] as JDBC transaction because no JDBC Connection could be retrieved from it"); + } + } + } + + // Bind the session holder to the thread. + TransactionSynchronizationManager.bindResource(getSessionFactory(), txObject.getSessionHolder()); + } + + catch (Exception ex) { + SessionFactoryUtils.releaseSession(session, getSessionFactory()); + throw new CannotCreateTransactionException("Could not open TopLink Session for transaction", ex); + } + } + + /** + * Extract the underlying JDBC Connection from the given TopLink Session. + *

Default implementation casts to oracle.toplink.publicinterface.Session + * and fetches the Connection from the DatabaseAccessor exposed there. + * @param session the current TopLink Session + * @return the underlying JDBC Connection, or null if none found + * @see oracle.toplink.publicinterface.Session#getAccessor() + * @see oracle.toplink.internal.databaseaccess.DatabaseAccessor#getConnection() + */ + protected Connection getJdbcConnection(Session session) { + if (!(session instanceof oracle.toplink.publicinterface.Session)) { + if (logger.isDebugEnabled()) { + logger.debug("TopLink Session [" + session + + "] does not derive from [oracle.toplink.publicinterface.Session]"); + } + return null; + } + Accessor accessor = ((oracle.toplink.publicinterface.Session) session).getAccessor(); + if (!(accessor instanceof DatabaseAccessor)) { + if (logger.isDebugEnabled()) { + logger.debug("TopLink Accessor [" + accessor + + "] does not derive from [oracle.toplink.internal.databaseaccess.DatabaseAccessor]"); + } + return null; + } + return ((DatabaseAccessor) accessor).getConnection(); + } + + protected Object doSuspend(Object transaction) { + TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction; + txObject.setSessionHolder(null); + return TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + + protected void doResume(Object transaction, Object suspendedResources) { + SessionHolder sessionHolder = (SessionHolder) suspendedResources; + if (TransactionSynchronizationManager.hasResource(getSessionFactory())) { + // From non-transactional code running in active transaction synchronization + // -> can be safely removed, will be closed on transaction completion. + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); + } + + protected void doCommit(DefaultTransactionStatus status) { + TopLinkTransactionObject txObject = (TopLinkTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Committing TopLink transaction on session [" + + txObject.getSessionHolder().getSession() + "]"); + } + try { + if (!status.isReadOnly()) { + txObject.getSessionHolder().getSession().getActiveUnitOfWork().commit(); + } + txObject.getSessionHolder().clear(); + } + catch (TopLinkException ex) { + throw convertTopLinkAccessException(ex); + } + } + + protected void doRollback(DefaultTransactionStatus status) { + TopLinkTransactionObject txObject = (TopLinkTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Not committing TopLink transaction on session [" + + txObject.getSessionHolder().getSession() + "]"); + } + txObject.getSessionHolder().clear(); + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + TopLinkTransactionObject txObject = (TopLinkTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting TopLink transaction on session [" + + txObject.getSessionHolder().getSession() + "] rollback-only"); + } + txObject.getSessionHolder().setRollbackOnly(); + } + + protected void doCleanupAfterCompletion(Object transaction) { + TopLinkTransactionObject txObject = (TopLinkTransactionObject) transaction; + + // Remove the session holder from the thread. + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + + // Remove the JDBC connection holder from the thread, if exposed. + if (txObject.hasConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(getDataSource()); + } + + Session session = txObject.getSessionHolder().getSession(); + if (logger.isDebugEnabled()) { + logger.debug("Releasing TopLink Session [" + session + "] after transaction"); + } + try { + session.release(); + } + catch (Throwable ex) { + // just log it, to keep a transaction-related exception + logger.debug("Could not release TopLink Session after transaction", ex); + } + } + + /** + * Convert the given TopLinkException to an appropriate exception from the + * org.springframework.dao hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * TopLink DatabaseException, else rely on TopLink's default translation. + * @param ex TopLinkException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertTopLinkAccessException + * @see #setJdbcExceptionTranslator + */ + protected DataAccessException convertTopLinkAccessException(TopLinkException ex) { + if (getJdbcExceptionTranslator() != null && ex instanceof DatabaseException) { + Throwable internalEx = ex.getInternalException(); + // Should always be a SQLException inside a DatabaseException. + if (internalEx instanceof SQLException) { + return getJdbcExceptionTranslator().translate( + "TopLink commit: " + ex.getMessage(), null, (SQLException) internalEx); + } + } + return SessionFactoryUtils.convertTopLinkAccessException(ex); + } + + + /** + * TopLink transaction object, representing a SessionHolder. + * Used as transaction object by TopLinkTransactionManager. + */ + private static class TopLinkTransactionObject extends JdbcTransactionObjectSupport { + + private SessionHolder sessionHolder; + + public void setSessionHolder(SessionHolder sessionHolder) { + this.sessionHolder = sessionHolder; + } + + public SessionHolder getSessionHolder() { + return this.sessionHolder; + } + + public boolean isRollbackOnly() { + return getSessionHolder().isRollbackOnly(); + } + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/UnitOfWorkCallback.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/UnitOfWorkCallback.java new file mode 100644 index 00000000000..7505f2d03ab --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/UnitOfWorkCallback.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2005 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.toplink; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; +import oracle.toplink.sessions.UnitOfWork; + +/** + * Convenient abstract implementation of the TopLinkCallback interface, + * exposing a UnitOfWork to perform write operations on. + * + *

The exposed UnitOfWork will either be be the active UnitOfWork of + * the current transaction, if any, or a temporarily acquired UnitOfWork + * that will be committed at the end of the operation. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #doInUnitOfWork(oracle.toplink.sessions.UnitOfWork) + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + */ +public abstract class UnitOfWorkCallback implements TopLinkCallback { + + /** + * Determines the UnitOfWork to work on (either the active UnitOfWork or a + * temporarily acquired UnitOfWork) and delegates to doInUnitOfWork. + * @see #doInUnitOfWork(oracle.toplink.sessions.UnitOfWork) + */ + public final Object doInTopLink(Session session) throws TopLinkException { + // Fetch active UnitOfWork or acquire temporary UnitOfWork. + UnitOfWork unitOfWork = session.getActiveUnitOfWork(); + boolean newUnitOfWork = false; + if (unitOfWork == null) { + unitOfWork = session.acquireUnitOfWork(); + newUnitOfWork = true; + } + + // Perform callback operation, committing the UnitOfWork unless + // it is the active UnitOfWork of an externally managed transaction. + try { + Object result = doInUnitOfWork(unitOfWork); + if (newUnitOfWork) { + unitOfWork.commit(); + } + return result; + } + finally { + if (newUnitOfWork) { + unitOfWork.release(); + } + } + } + + /** + * Called with a UnitOfWork to work on, either the active UnitOfWork or a + * temporarily acquired UnitOfWork (as determined by the transaction status). + * @param unitOfWork the TopLink UnitOfWork to perform write operations on + * @return a result object, or null if none + * @throws TopLinkException in case of TopLink errors + */ + protected abstract Object doInUnitOfWork(UnitOfWork unitOfWork) throws TopLinkException; + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/package.html new file mode 100644 index 00000000000..dfb35de0d86 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/package.html @@ -0,0 +1,13 @@ + + + +Package providing integration of +Oracle TopLink +with Spring concepts. + +

Contains SessionFactory helper classes, a template plus callback +for TopLink access, and an implementation of Spring's transaction SPI +for local TopLink transactions. + + + diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/CommonsLoggingSessionLog.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/CommonsLoggingSessionLog.java new file mode 100644 index 00000000000..10ca4347d95 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/CommonsLoggingSessionLog.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2007 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.toplink.support; + +import java.lang.reflect.Method; + +import oracle.toplink.internal.databaseaccess.Accessor; +import oracle.toplink.logging.AbstractSessionLog; +import oracle.toplink.logging.SessionLogEntry; +import oracle.toplink.publicinterface.Session; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ReflectionUtils; + +/** + * TopLink 10.1.3+ SessionLog implementation that logs through Commons Logging. + * + *

The namespace used is "oracle.toplink.xxx", with the latter part being + * the TopLink log category ("sql"/"transaction"/etc). In case of no category + * given, "session" will be used as default. This allows for fine-grained + * filtering of log messages, for example through Log4J configuration. + * + *

Maps TopLink's SEVERE level to CL ERROR, TopLink's WARNING to CL WARN, + * TopLink's INFO to CL INFO, TopLink's CONFIG/FINE/FINER to CL DEBUG, + * and TopLink's FINEST to CL TRACE. This results in common CL log behavior: + * INFO logging only at startup; operation logging at DEBUG level. Debug logging + * can be further filtered according to categories: for example, activate Log4J + * DEBUG logging for category "oracle.toplink.sql" to see the generated SQL. + * + *

Note: This implementation will only work on TopLink 10.1.3 or higher, + * as it is built against TopLink's new SessionLog facilities in the + * oracle.toplink.logging package, supporting log categories. + * + * @author Juergen Hoeller + * @since 1.2 + * @see CommonsLoggingSessionLog904 + * @see oracle.toplink.logging.JavaLog + * @see org.springframework.orm.toplink.LocalSessionFactoryBean#setSessionLog + */ +public class CommonsLoggingSessionLog extends AbstractSessionLog { + + public static final String NAMESPACE_PREFIX = "oracle.toplink."; + + public static final String DEFAULT_NAMESPACE = "session"; + + public static final String DEFAULT_SEPARATOR = "--"; + + + private static Method getSessionMethod; + + private static Method getExceptionMethod; + + static { + try { + getSessionMethod = SessionLogEntry.class.getMethod("getSession", new Class[0]); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Could not find method SessionLogEntry.getSession()"); + } + try { + getExceptionMethod = SessionLogEntry.class.getMethod("getException", new Class[0]); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Could not find method SessionLogEntry.getException()"); + } + } + + + private String separator = DEFAULT_SEPARATOR; + + + /** + * Specify the separator between TopLink's supplemental details + * (session, connection) and the log message itself. Default is "--". + */ + public void setSeparator(String separator) { + this.separator = separator; + } + + /** + * Return the separator between TopLink's supplemental details + * (session, connection) and the log message itself. Default is "--". + */ + public String getSeparator() { + return this.separator; + } + + + public void log(SessionLogEntry entry) { + Log logger = LogFactory.getLog(getCategory(entry)); + switch (entry.getLevel()) { + case SEVERE: + if (logger.isErrorEnabled()) { + if (entry.hasException()) { + logger.error(getMessageString(entry), getException(entry)); + } + else { + logger.error(getMessageString(entry)); + } + } + break; + case WARNING: + if (logger.isWarnEnabled()) { + if (entry.hasException()) { + logger.warn(getMessageString(entry), getException(entry)); + } + else { + logger.warn(getMessageString(entry)); + } + } + break; + case INFO: + if (logger.isInfoEnabled()) { + if (entry.hasException()) { + logger.info(getMessageString(entry), getException(entry)); + } + else { + logger.info(getMessageString(entry)); + } + } + break; + case CONFIG: + case FINE: + case FINER: + if (logger.isDebugEnabled()) { + if (entry.hasException()) { + logger.debug(getMessageString(entry), getException(entry)); + } + else { + logger.debug(getMessageString(entry)); + } + } + break; + case FINEST: + if (logger.isTraceEnabled()) { + if (entry.hasException()) { + logger.trace(getMessageString(entry), getException(entry)); + } + else { + logger.trace(getMessageString(entry)); + } + } + break; + } + } + + /** + * Determine the log category for the given log entry. + *

If the entry carries a name space value, it will be appended + * to the "oracle.toplink." prefix; else, "oracle.toplink.session" + * will be used. + */ + protected String getCategory(SessionLogEntry entry) { + String namespace = entry.getNameSpace(); + return NAMESPACE_PREFIX + (namespace != null ? namespace : DEFAULT_NAMESPACE); + } + + /** + * Build the message String for the given log entry, including the + * supplemental details (session, connection) and the formatted message. + * @see #getSessionString(oracle.toplink.sessions.Session) + * @see #getConnectionString(oracle.toplink.internal.databaseaccess.Accessor) + * @see #formatMessage(oracle.toplink.logging.SessionLogEntry) + * @see #getSeparator() + */ + protected String getMessageString(SessionLogEntry entry) { + StringBuffer buf = new StringBuffer(); + Session session = getSession(entry); + if (session != null) { + buf.append(getSessionString(session)); + buf.append(getSeparator()); + } + Accessor connection = entry.getConnection(); + if (connection != null) { + buf.append(getConnectionString(connection)); + buf.append(getSeparator()); + } + buf.append(formatMessage(entry)); + return buf.toString(); + } + + /** + * Extract the exception from the given log entry. + *

The default implementation calls SessionLogEntry.getSession + * via reflection: The return type varies between TopLink 10.1.3 and 11 + * (Session vs AbstractSession, respectively). + */ + protected Session getSession(SessionLogEntry entry) { + return (Session) ReflectionUtils.invokeMethod(getSessionMethod, entry); + } + + /** + * Extract the exception from the given log entry. + *

The default implementation calls SessionLogEntry.getException + * via reflection: The return type varies between TopLink 9.0.4 and 10.1.3 + * (Exception vs Throwable, respectively). + */ + protected Throwable getException(SessionLogEntry entry) { + return (Throwable) ReflectionUtils.invokeMethod(getExceptionMethod, entry); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/CommonsLoggingSessionLog904.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/CommonsLoggingSessionLog904.java new file mode 100644 index 00000000000..bbc8f1684a9 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/CommonsLoggingSessionLog904.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2005 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.toplink.support; + +import oracle.toplink.sessions.DefaultSessionLog; +import oracle.toplink.sessions.Session; +import oracle.toplink.sessions.SessionLogEntry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * TopLink 9.0.4 SessionLog implementation that logs through Commons Logging. + * + *

The namespace used is "oracle.toplink.session". Fine-grained filtering + * of log messages, for example through Log4J configuration, is not + * available on TopLink 9.0.4: Consider upgrading to TopLink 10.1.3 and + * using the CommonsLoggingSessionLog class instead. + * + *

TopLink log entries with exceptions are logged at CL WARN level, + * TopLink debug log entries at CL TRACE level, and any other log entry + * at CL DEBUG level. Finer-grained mapping to log levels is unfortunately + * not possible on TopLink 9.0.4. + * + *

Note: This implementation will only actually work on TopLink 9.0.4, + * as it is built against TopLink's old SessionLog facilities in the + * oracle.toplink.sessions package, which are effectively + * obsolete (deprecated and bypassed) as of TopLink 10.1.3. + * + * @author Juergen Hoeller + * @since 1.2 + * @see CommonsLoggingSessionLog + * @see oracle.toplink.sessions.DefaultSessionLog + * @see org.springframework.orm.toplink.LocalSessionFactoryBean#setSessionLog + */ +public class CommonsLoggingSessionLog904 extends DefaultSessionLog { + + public static final String NAMESPACE = "oracle.toplink.session"; + + public static final String DEFAULT_SEPARATOR = "--"; + + + private final Log logger = LogFactory.getLog(NAMESPACE); + + private String separator = DEFAULT_SEPARATOR; + + + /** + * Specify the separator between TopLink's supplemental details + * (session, connection) and the log message itself. Default is "--". + */ + public void setSeparator(String separator) { + this.separator = separator; + } + + /** + * Return the separator between TopLink's supplemental details + * (session, connection) and the log message itself. Default is "--". + */ + public String getSeparator() { + return separator; + } + + + public void log(SessionLogEntry entry) { + if (entry.hasException()) { + if (shouldLogExceptions() && logger.isWarnEnabled()) { + this.logger.warn(getMessageString(entry), entry.getException()); + } + } + else if (entry.isDebug()) { + if (shouldLogDebug() && logger.isTraceEnabled()) { + this.logger.trace(getMessageString(entry)); + } + } + else { + if (logger.isDebugEnabled()) { + this.logger.debug(getMessageString(entry)); + } + } + } + + /** + * Build the message String for the given log entry, including the + * supplemental details (session, connection) and the message text. + * @see #getSeparator() + */ + protected String getMessageString(SessionLogEntry entry) { + StringBuffer buf = new StringBuffer(); + if (shouldPrintSession()) { + buf.append(getSessionName(entry.getSession())); + buf.append("("); + buf.append(String.valueOf(System.identityHashCode(entry.getSession()))); + buf.append(")"); + buf.append(getSeparator()); + } + if (shouldPrintConnection() && entry.getConnection() != null) { + buf.append("Connection"); + buf.append("("); + buf.append(String.valueOf(System.identityHashCode(entry.getConnection()))); + buf.append(")"); + buf.append(getSeparator()); + } + buf.append(entry.getMessage()); + return buf.toString(); + } + + /** + * Return the name to be used for the given Session + * ("UnitOfWork"/"ServerSession"/"ClientSession"/etc). + */ + protected String getSessionName(Session session) { + if (session.isUnitOfWork()) { + return "UnitOfWork"; + } + if (session.isServerSession()) { + return "ServerSession"; + } + if (session.isClientSession()) { + return "ClientSession"; + } + if (session.isSessionBroker()) { + return "SessionBroker"; + } + if (session.isRemoteSession()) { + return "RemoteSession"; + } + if (session.isDatabaseSession()) { + return "DatabaseSession"; + } + return "Session"; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/TopLinkDaoSupport.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/TopLinkDaoSupport.java new file mode 100644 index 00000000000..26197500f28 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/TopLinkDaoSupport.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2008 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.toplink.support; + +import oracle.toplink.exceptions.TopLinkException; +import oracle.toplink.sessions.Session; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.support.DaoSupport; +import org.springframework.orm.toplink.SessionFactory; +import org.springframework.orm.toplink.SessionFactoryUtils; +import org.springframework.orm.toplink.TopLinkTemplate; + +/** + * Convenient super class for TopLink data access objects. + * + *

Requires a SessionFactory to be set, providing a TopLinkTemplate + * based on it to subclasses. Can alternatively be initialized directly with + * a TopLinkTemplate, to reuse the latter's settings such as the SessionFactory, + * exception translator, etc. + * + *

This base class is mainly intended for TopLinkTemplate usage + * but can also be used when working with SessionFactoryUtils directly, + * for example in combination with TopLinkInterceptor-managed Sessions. + * Convenience getSession and releaseSession + * methods are provided for that usage style. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setSessionFactory + * @see #setTopLinkTemplate + * @see #getSession + * @see #releaseSession + * @see org.springframework.orm.toplink.TopLinkTemplate + * @see org.springframework.orm.toplink.TopLinkInterceptor + */ +public abstract class TopLinkDaoSupport extends DaoSupport { + + private TopLinkTemplate topLinkTemplate; + + + /** + * Set the TopLink SessionFactory to be used by this DAO. + * Will automatically create a TopLinkTemplate for the given SessionFactory. + * @see #createTopLinkTemplate + * @see #setTopLinkTemplate + */ + public final void setSessionFactory(SessionFactory sessionFactory) { + if (this.topLinkTemplate == null || sessionFactory != this.topLinkTemplate.getSessionFactory()) { + this.topLinkTemplate = createTopLinkTemplate(sessionFactory); + } + } + + /** + * Create a TopLinkTemplate for the given SessionFactory. + * Only invoked if populating the DAO with a SessionFactory reference! + *

Can be overridden in subclasses to provide a TopLinkTemplate instance + * with different configuration, or a custom TopLinkTemplate subclass. + * @param sessionFactory the TopLink SessionFactory to create a TopLinkTemplate for + * @return the new TopLinkTemplate instance + * @see #setSessionFactory + */ + protected TopLinkTemplate createTopLinkTemplate(SessionFactory sessionFactory) { + return new TopLinkTemplate(sessionFactory); + } + + /** + * Return the TopLink SessionFactory used by this DAO. + */ + public final SessionFactory getSessionFactory() { + return (this.topLinkTemplate != null ? this.topLinkTemplate.getSessionFactory() : null); + } + + /** + * Set the TopLinkTemplate for this DAO explicitly, + * as an alternative to specifying a SessionFactory. + * @see #setSessionFactory + */ + public final void setTopLinkTemplate(TopLinkTemplate topLinkTemplate) { + this.topLinkTemplate = topLinkTemplate; + } + + /** + * Return the TopLinkTemplate for this DAO, + * pre-initialized with the SessionFactory or set explicitly. + */ + public final TopLinkTemplate getTopLinkTemplate() { + return topLinkTemplate; + } + + protected final void checkDaoConfig() { + if (this.topLinkTemplate == null) { + throw new IllegalArgumentException("sessionFactory or topLinkTemplate is required"); + } + } + + + /** + * Get a TopLink Session, either from the current transaction or a new one. + * The latter is only allowed if the "allowCreate" setting of this bean's + * TopLinkTemplate is true. + *

Note that this is not meant to be invoked from TopLinkTemplate code + * but rather just in plain TopLink code. Either rely on a thread-bound + * Session (via TopLinkInterceptor), or use it in combination with + * releaseSession. + *

In general, it is recommended to use TopLinkTemplate, either with + * the provided convenience operations or with a custom TopLinkCallback + * that provides you with a Session to work on. TopLinkTemplate will care + * for all resource management and for proper exception conversion. + * @return the TopLink Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and allowCreate false + * @see TopLinkTemplate + * @see org.springframework.orm.toplink.SessionFactoryUtils#getSession(SessionFactory, boolean) + * @see org.springframework.orm.toplink.TopLinkInterceptor + * @see org.springframework.orm.toplink.TopLinkTemplate + * @see org.springframework.orm.toplink.TopLinkCallback + */ + protected final Session getSession() + throws DataAccessResourceFailureException, IllegalStateException { + + return getSession(this.topLinkTemplate.isAllowCreate()); + } + + /** + * Get a TopLink Session, either from the current transaction or a new one. + * The latter is only allowed if "allowCreate" is true. + *

Note that this is not meant to be invoked from TopLinkTemplate code + * but rather just in plain TopLink code. Either rely on a thread-bound + * Session (via TopLinkInterceptor), or use it in combination with + * releaseSession. + *

In general, it is recommended to use TopLinkTemplate, either with + * the provided convenience operations or with a custom TopLinkCallback + * that provides you with a Session to work on. TopLinkTemplate will care + * for all resource management and for proper exception conversion. + * @param allowCreate if a new Session should be created if no thread-bound found + * @return the TopLink Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @throws IllegalStateException if no thread-bound Session found and allowCreate false + * @see org.springframework.orm.toplink.SessionFactoryUtils#getSession(SessionFactory, boolean) + * @see org.springframework.orm.toplink.TopLinkInterceptor + * @see org.springframework.orm.toplink.TopLinkTemplate + * @see org.springframework.orm.toplink.TopLinkCallback + */ + protected final Session getSession(boolean allowCreate) + throws DataAccessResourceFailureException, IllegalStateException { + + return SessionFactoryUtils.getSession(this.getSessionFactory(), allowCreate); + } + + /** + * Convert the given TopLinkException to an appropriate exception from the + * org.springframework.dao hierarchy. Will automatically detect + * wrapped SQLExceptions and convert them accordingly. + *

Delegates to the convertTopLinkAccessException method of this + * DAO's TopLinkTemplate. + * @param ex TopLinkException that occured + * @return the corresponding DataAccessException instance + * @see #setTopLinkTemplate + * @see org.springframework.orm.toplink.TopLinkTemplate#convertTopLinkAccessException + */ + protected final DataAccessException convertTopLinkAccessException(TopLinkException ex) { + return this.topLinkTemplate.convertTopLinkAccessException(ex); + } + + /** + * Close the given TopLink Session, created via this DAO's SessionFactory, + * if it isn't bound to the thread. + * @param session the TopLink Session to close + * @see org.springframework.orm.toplink.SessionFactoryUtils#releaseSession + */ + protected final void releaseSession(Session session) { + SessionFactoryUtils.releaseSession(session, getSessionFactory()); + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/TransactionAwareSessionAdapter.java b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/TransactionAwareSessionAdapter.java new file mode 100644 index 00000000000..e12f65cd700 --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/TransactionAwareSessionAdapter.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2005 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.toplink.support; + +import oracle.toplink.sessions.Session; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.orm.toplink.SessionFactory; + +/** + * This adapter FactoryBean takes a TopLink SessionFactory and exposes a + * corresponding transaction-aware TopLink Session as bean reference. + * + *

This adapter bean will usually be defined in front of a Spring + * LocalSessionFactoryBean, to allow for passing Session references to DAOs + * that expect to work on a raw TopLink Session. Your DAOs can then, + * for example, access the currently active Session and UnitOfWork via + * Session.getActiveSession() and + * Session.getActiveUnitOfWork(), respectively. + * + *

The main advantage of this proxy is that it allows DAOs to work with a + * plain TopLink Session reference, while still participating in Spring's + * (or a J2EE server's) resource and transaction management. DAOs will only + * rely on the TopLink API in such a scenario, without any Spring dependencies. + * + *

It is usually preferable to write your TopLink-based DAOs with Spring's + * TopLinkTemplate, offering benefits such as consistent data access exceptions + * instead of TopLinkExceptions at the DAO layer. However, Spring's resource + * and transaction management (and Dependency Injection) will work for DAOs + * written against the plain TopLink API too. + * + *

Of course, you can still access the target TopLink SessionFactory + * even when your DAOs go through this adapter, by defining a bean reference + * that points directly at your target SessionFactory bean. + * + *

Note that the actual creation of a transaction-aware TopLink Session + * is available on the TopLink SessionFactory itself. This adapter FactoryBean + * is just a convenient way to expose such a Session in a declarative fashion. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.orm.toplink.LocalSessionFactoryBean + * @see org.springframework.orm.toplink.SessionFactory#createTransactionAwareSession() + * @see oracle.toplink.sessions.Session#getActiveSession() + * @see oracle.toplink.sessions.Session#getActiveUnitOfWork() + */ +public class TransactionAwareSessionAdapter implements FactoryBean { + + private Session session; + + + /** + * Set the SessionFactory that this adapter is supposed to expose a + * transaction-aware TopLink Session for. This should be the raw + * SessionFactory, as accessed by TopLinkTransactionManager. + * @see org.springframework.orm.toplink.TopLinkTransactionManager + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.session = sessionFactory.createTransactionAwareSession(); + } + + + public Object getObject() { + return this.session; + } + + public Class getObjectType() { + return Session.class; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/package.html b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/package.html new file mode 100644 index 00000000000..cee7e65060a --- /dev/null +++ b/org.springframework.orm/src/main/java/org/springframework/orm/toplink/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.orm.toplink package. +Contains a DAO base class for TopLinkTemplate usage. + + + diff --git a/org.springframework.orm/src/main/java/overview.html b/org.springframework.orm/src/main/java/overview.html new file mode 100644 index 00000000000..1eb7a2e8c19 --- /dev/null +++ b/org.springframework.orm/src/main/java/overview.html @@ -0,0 +1,7 @@ + + +

+The Spring Data Binding framework, an internal library used by Spring Web Flow. +

+ + \ No newline at end of file diff --git a/org.springframework.orm/src/test/resources/log4j.xml b/org.springframework.orm/src/test/resources/log4j.xml new file mode 100644 index 00000000000..767b96d6206 --- /dev/null +++ b/org.springframework.orm/src/test/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.orm/template.mf b/org.springframework.orm/template.mf new file mode 100644 index 00000000000..6173054df7a --- /dev/null +++ b/org.springframework.orm/template.mf @@ -0,0 +1,74 @@ +Bundle-SymbolicName: org.springframework.orm +Bundle-Name: Spring ORM +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Package: + com.ibatis.sqlmap.engine.transaction.external;version="[2.3.0.677, 3.0.0)";resolution:=optional, + oracle.toplink.essentials.expressions;version="[2.0.0.b41-beta2, 3.0.0)";resolution:=optional, + org.eclipse.persistence.expressions;version="[1.0.0, 2.0.0)";resolution:=optional +Import-Template: + com.ibatis.*;version="[2.3.0.677, 3.0.0)";resolution:=optional, + javax.jdo.*;version="[2.0.0, 3.0.0)";resolution:=optional, + javax.persistence.*;version="[1.0.0, 2.0.0)";resolution:=optional, + javax.servlet.*;version="[2.4.0, 3.0.0)";resolution:=optional, + javax.transaction.*;version="[1.0.1, 2.0.0)";resolution:=optional, + oracle.toplink.essentials.*;version="[2.0.0.b41-beta2, 3.0.0)";resolution:=optional, + oracle.toplink.exceptions;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.expressions;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.internal.databaseaccess;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.jndi;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.logging;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.publicinterface;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.queryframework;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.sessionbroker;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.sessions;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.threetier;version="[10.1.3, 11.0.0)";resolution:=optional, + oracle.toplink.tools.*;version="[10.1.3, 11.0.0)";resolution:=optional, + org.aopalliance.*;version="[1.0.0, 2.0.0)", + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.apache.openjpa.persistence.*;version="[1.0.2, 2.0.0)";resolution:=optional, + org.eclipse.persistence.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.hibernate;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.cache;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.cfg;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.classic;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.connection;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.context;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.criterion;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.dialect;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.ejb;version="[3.3.0.ga, 3.4.0)";resolution:=optional, + org.hibernate.engine;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.event.*;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.exception;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.impl;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.jdbc;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.persister.entity;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.tool.hbm2ddl;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.transaction;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.type;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.usertype;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.hibernate.util;version="[3.2.6.ga, 3.3.0)";resolution:=optional, + org.springframework.aop.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.beans.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.context.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.core.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.dao.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.instrument.classloading.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.jdbc.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.jndi.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.transaction.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.util.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.web.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional, + org.springframework.ui.*;version="[2.5.5.A, 2.5.5.A]";resolution:=optional +Unversioned-Imports: + javax.naming.*, + javax.sql.*, + javax.xml.parsers.*, + org.w3c.dom.*, + org.xml.sax +Ignored-Existing-Headers: + Bnd-LastModified, + DynamicImport-Package, + Import-Package, + Export-Package, + Tool