2 * Copyright (C) 2009 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
10 package eu
.etaxonomy
.cdm
.api
.conversation
;
12 import java
.sql
.Connection
;
13 import java
.sql
.SQLException
;
14 import java
.util
.ArrayList
;
15 import java
.util
.Iterator
;
16 import java
.util
.List
;
18 import javax
.sql
.DataSource
;
20 import org
.apache
.log4j
.Logger
;
21 import org
.hibernate
.FlushMode
;
22 import org
.hibernate
.HibernateException
;
23 import org
.hibernate
.LockMode
;
24 import org
.hibernate
.Session
;
25 import org
.hibernate
.SessionFactory
;
26 import org
.hibernate
.jdbc
.Work
;
27 import org
.springframework
.beans
.factory
.annotation
.Autowired
;
28 import org
.springframework
.dao
.DataAccessResourceFailureException
;
29 import org
.springframework
.jdbc
.datasource
.ConnectionHolder
;
30 import org
.springframework
.orm
.hibernate4
.SessionFactoryUtils
;
31 import org
.springframework
.orm
.hibernate4
.SessionHolder
;
32 import org
.springframework
.transaction
.PlatformTransactionManager
;
33 import org
.springframework
.transaction
.TransactionDefinition
;
34 import org
.springframework
.transaction
.TransactionStatus
;
35 import org
.springframework
.transaction
.support
.TransactionSynchronizationManager
;
37 import eu
.etaxonomy
.cdm
.api
.service
.IService
;
38 import eu
.etaxonomy
.cdm
.api
.service
.ServiceImpl
;
39 import eu
.etaxonomy
.cdm
.persistence
.hibernate
.CdmPostDataChangeObservableListener
;
42 * This is an implementation of the session-per-conversation pattern for usage in a Spring context.
44 * The primary aim of this class is to create and maintain sessions across multiple transactions.
45 * It is important to ensure that these (long running) sessions must always behave consistently
46 * with regards to session management behaviour expected by Hibernate.
48 * This behaviour essentially revolves around the resources map in the {@link org.springframework.transaction.support.TransactionSynchronizationManager TransactionSynchronizationManager}.
49 * This resources map contains two entries of interest,
50 * - (Autowired) {@link org.hibernate.SessionFactory} mapped to the {@link org.springframework.orm.hibernate4.SessionHolder}
51 * - (Autowired) {@link javax.sql.DataSource} mapped to the {@link org.springframework.jdbc.datasource.ConnectionHolder}
53 * The SessionHolder object itself contains the {@link org.hibernate.Session Session} as well as the {@link org.hibernate.Transaction object.
54 * The ConnectionHolder contains the (JDBC) {@link java.sql.Connection Connection} to the database. For every action to do with the
55 * transaction object it is required to have both entries present in the resources. Both the session as well as the connection
56 * objects must not be null and the corresponding holders must have their 'synchronizedWithTransaction' flag set to true.
58 * The default behaviour of the {@link org.springframework.transaction.PlatformTransactionManager PlatformTransactionManager} which in the CDM case is autowired
59 * to {@link org.springframework.orm.hibernate4.HibernateTransactionManager HibernateTransactionManager}, is to check these entries
60 * when starting a transaction. If this entries do not exist in the resource map then they are created, implying a new session, which
61 * is in fact how hibernate implements the default 'session-per-request' pattern internally.
63 * Given the above conditions, this class manages long running sessions by providing the following methods,
64 * - {@link #bind()} : binds the session owned by this conversation to the resource map
65 * - {@link #startTransaction()} : starts a transaction
66 * - {@link #commit()} : commits the current transaction, with the option of restarting a new transaction.
67 * - {@link #unbind()} : unbinds the session owned by this conversation from the resource map.
68 * - {@link #close()} : closes the session owned by this conversation
70 * With the exception of {@link #unbind()} (which should be called explicitly), the above sequence must be strictly followed to
71 * maintain a consistent session state. Even though it is possible to interweave multiple conversations at the same time, for a
72 * specific conversation the above sequence must be followed.
74 * @see http://www.hibernate.org/42.html
76 * @author n.hoffmann,c.mathew
80 public class ConversationHolder
{
82 private static final Logger logger
= Logger
.getLogger(ConversationHolder
.class);
85 private SessionFactory sessionFactory
;
88 private DataSource dataSource
;
91 private PlatformTransactionManager transactionManager
;
94 private IService service
;
98 * The persistence context for this conversation
100 private Session longSession
= null;
103 * Spring communicates with hibernate sessions via a SessionHolder object
105 private SessionHolder sessionHolder
= null;
108 * @see TransactionDefinition
110 private TransactionDefinition definition
;
113 * This conversations transaction
115 private TransactionStatus transactionStatus
;
118 private boolean closed
= false;
121 * Simple constructor used by Spring only
123 private ConversationHolder(){
127 public ConversationHolder(DataSource dataSource
, SessionFactory sessionFactory
,
128 PlatformTransactionManager transactionManager
) {
130 this.dataSource
= dataSource
;
131 this.sessionFactory
= sessionFactory
;
132 this.transactionManager
= transactionManager
;
136 if(TransactionSynchronizationManager
.hasResource(getDataSource())){
137 TransactionSynchronizationManager
.unbindResource(getDataSource());
143 * This method has to be called when starting a new unit-of-work. All required resources are
144 * bound so that SessionFactory.getCurrentSession() returns the right session for this conversation
148 logger
.info("Binding resources for ConversationHolder");
150 if(TransactionSynchronizationManager
.isSynchronizationActive()){
151 TransactionSynchronizationManager
.clearSynchronization();
156 logger
.info("Starting new Synchronization in TransactionSynchronizationManager");
157 TransactionSynchronizationManager
.initSynchronization();
160 if(TransactionSynchronizationManager
.hasResource(getSessionFactory())){
161 TransactionSynchronizationManager
.unbindResource(getSessionFactory());
164 logger
.info("Binding Session to TransactionSynchronizationManager: Session: " + getSessionHolder());
165 TransactionSynchronizationManager
.bindResource(getSessionFactory(), getSessionHolder());
169 } catch(Exception e
){
170 logger
.error("Error binding resources for session", e
);
176 * This method has to be called when suspending the current unit of work. The conversation can be later bound again.
178 public void unbind() {
180 logger
.info("Unbinding resources for ConversationHolder");
182 if(TransactionSynchronizationManager
.isSynchronizationActive()){
183 TransactionSynchronizationManager
.clearSynchronization();
188 // unbind the current session.
189 // there is no need to bind a new session, since HibernateTransactionManager will create a new one
190 // if the resource map does not contain one (ditto for the datasource-to-connection entry).
191 TransactionSynchronizationManager
.unbindResource(getSessionFactory());
192 if(TransactionSynchronizationManager
.hasResource(getDataSource())){
193 TransactionSynchronizationManager
.unbindResource(getDataSource());
198 public SessionHolder
getSessionHolder(){
199 if(this.sessionHolder
== null){
200 logger
.info("Creating new SessionHolder");
201 this.sessionHolder
= new SessionHolder(getSession());
203 return this.sessionHolder
;
209 private DataSource
getDataSource() {
210 return this.dataSource
;
214 * @return true if this longSession is bound to the session factory.
216 public boolean isBound(){
217 //return sessionHolder != null && longSession != null && longSession.isConnected();
218 SessionHolder currentSessionHolder
= (SessionHolder
)TransactionSynchronizationManager
.getResource(getSessionFactory());
219 return longSession
!= null && currentSessionHolder
!= null && getSessionFactory().getCurrentSession() == longSession
;
223 * Creates an instance of TransactionStatus and binds it to this conversation manager.
224 * At the moment we allow only on transaction per conversation holder.
226 * @return the transaction status bound to this conversation holder
228 public TransactionStatus
startTransaction(){
229 if (isTransactionActive()){
230 logger
.warn("We allow only one transaction at the moment but startTransaction " +
231 "was called a second time.\nReturning the transaction already associated with this " +
232 "ConversationManager");
234 //always safe to remove the datasource-to-connection entry since we
235 // know that HibernateTransactionManager will create a new one
236 if(TransactionSynchronizationManager
.hasResource(getDataSource())){
237 TransactionSynchronizationManager
.unbindResource(getDataSource());
240 transactionStatus
= transactionManager
.getTransaction(definition
);
242 logger
.info("Transaction started: " + transactionStatus
);
244 return transactionStatus
;
248 * @return if there is a running transaction
250 public boolean isTransactionActive(){
251 return transactionStatus
!= null;
255 * @see org.hibernate.Session#evict(java.lang.Object object)
257 public void evict(Object object
){
258 getSession().evict(object
);
262 * @see org.hibernate.Session#refresh(java.lang.Object object)
264 public void refresh(Object object
){
265 getSession().refresh(object
);
269 * @see org.hibernate.Session#clear()
272 getSession().clear();
276 * Commit the running transaction.
278 public void commit(){
283 * Commit the running transaction but optionally start a
284 * new one right away.
286 * @param restartTransaction whether to start a new transaction
288 public TransactionStatus
commit(boolean restartTransaction
){
289 if(isTransactionActive()){
291 if(getSessionHolder().isRollbackOnly()){
292 logger
.error("Commiting this session will not work. It has been marked as rollback only.");
294 // if a datasource-to-connection entry already exists in the resource map
295 // then its setSynchronizedWithTransaction should be true, since hibernate has added
297 // if the datasource-to-connection entry does not exist then we need to create one
298 // and explicitly setSynchronizedWithTransaction to true.
299 TransactionSynchronizationManager
.getResource(getDataSource());
300 if(!TransactionSynchronizationManager
.hasResource(getDataSource())){
302 ConnectionHolder ch
= new ConnectionHolder(getDataSource().getConnection());
303 ch
.setSynchronizedWithTransaction(true);
304 TransactionSynchronizationManager
.bindResource(getDataSource(),ch
);
306 } catch (IllegalStateException e
) {
307 // TODO Auto-generated catch block
309 } catch (SQLException e
) {
310 // TODO Auto-generated catch block
315 // commit the changes
316 transactionManager
.commit(transactionStatus
);
318 // propagate transaction end
319 CdmPostDataChangeObservableListener
.getDefault().delayedNotify();
321 // Reset the transactionStatus.
322 transactionStatus
= null;
324 // Committing a transaction frees all resources.
325 // Since we are in a conversation we directly rebind those resources and start a new transaction
327 if(restartTransaction
){
328 return startTransaction();
331 logger
.warn("No active transaction but commit was called");
337 * @return the session associated with this conversation manager
339 public Session
getSession() {
340 if(longSession
== null){
341 longSession
= getNewSession();
347 * @return a new session to be managed by this conversation
349 private Session
getNewSession() {
351 // Interesting: http://stackoverflow.com/questions/3526556/session-connection-deprecated-on-hibernate
352 // Also, http://blog-it.hypoport.de/2012/05/10/hibernate-4-migration/
354 // This will create a new session which must be explicitly managed by this conversation, which includes
355 // binding / unbinding / closing session as well as starting / committing transactions.
356 Session session
= sessionFactory
.openSession();
357 session
.setFlushMode(FlushMode
.COMMIT
);
358 logger
.info("Creating Session: [" + longSession
+ "]");
366 * @return the session factory that is bound to this conversation manager
368 public SessionFactory
getSessionFactory() {
369 return sessionFactory
;
372 public void delete(Object object
){
373 this.getSession().delete(object
);
377 * Facades Session.lock()
379 public void lock(Object persistentObject
, LockMode lockMode
) {
380 getSession().lock(persistentObject
, lockMode
);
383 public void lock(String entityName
, Object persistentObject
, LockMode lockMode
){
384 getSession().lock(entityName
, persistentObject
, lockMode
);
388 * @return the definition
390 public TransactionDefinition
getDefinition() {
395 * @param definition the definition to set
397 public void setDefinition(TransactionDefinition definition
) {
398 this.definition
= definition
;
402 * Register to get updated after any interaction with the datastore
404 public void registerForDataStoreChanges(IConversationEnabled observer
) {
405 CdmPostDataChangeObservableListener
.getDefault().register(observer
);
409 * Register to get updated after any interaction with the datastore
411 public void unregisterForDataStoreChanges(IConversationEnabled observer
) {
412 CdmPostDataChangeObservableListener
.getDefault().unregister(observer
);
416 * Free resources bound to this conversationHolder
419 if(getSession().isOpen()) {
420 getSession().close();
424 sessionHolder
= null;
428 public boolean isClosed(){