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
.SQLException
;
14 import javax
.sql
.DataSource
;
16 import org
.apache
.log4j
.Logger
;
17 import org
.hibernate
.FlushMode
;
18 import org
.hibernate
.LockMode
;
19 import org
.hibernate
.Session
;
20 import org
.hibernate
.SessionFactory
;
21 import org
.springframework
.beans
.factory
.annotation
.Autowired
;
22 import org
.springframework
.jdbc
.datasource
.ConnectionHolder
;
23 import org
.springframework
.orm
.hibernate5
.SessionHolder
;
24 import org
.springframework
.transaction
.PlatformTransactionManager
;
25 import org
.springframework
.transaction
.TransactionDefinition
;
26 import org
.springframework
.transaction
.TransactionStatus
;
27 import org
.springframework
.transaction
.support
.TransactionSynchronizationManager
;
29 import eu
.etaxonomy
.cdm
.persistence
.hibernate
.CdmPostDataChangeObservableListener
;
32 * This is an implementation of the session-per-conversation pattern for usage in a Spring context.
34 * The primary aim of this class is to create and maintain sessions across multiple transactions.
35 * It is important to ensure that these (long running) sessions must always behave consistently
36 * with regards to session management behaviour expected by Hibernate.
38 * This behaviour essentially revolves around the resources map in the {@link org.springframework.transaction.support.TransactionSynchronizationManager TransactionSynchronizationManager}.
39 * This resources map contains two entries of interest,
41 * <li>(Autowired) {@link org.hibernate.SessionFactory} mapped to the {@link org.springframework.orm.hibernate5.SessionHolder}</li>
42 * <li>(Autowired) {@link javax.sql.DataSource} mapped to the {@link org.springframework.jdbc.datasource.ConnectionHolder}</li>
45 * The SessionHolder object itself contains the {@link org.hibernate.Session Session} as well as the {@link org.hibernate.Transaction object.
46 * The ConnectionHolder contains the (JDBC) {@link java.sql.Connection Connection} to the database. For every action to do with the
47 * transaction object it is required to have both entries present in the resources. Both the session as well as the connection
48 * objects must not be null and the corresponding holders must have their 'synchronizedWithTransaction' flag set to true.
50 * The default behaviour of the {@link org.springframework.transaction.PlatformTransactionManager PlatformTransactionManager} which in the CDM case is autowired
51 * to {@link org.springframework.orm.hibernate5.HibernateTransactionManager HibernateTransactionManager}, is to check these entries
52 * when starting a transaction. If this entries do not exist in the resource map then they are created, implying a new session, which
53 * is in fact how hibernate implements the default 'session-per-request' pattern internally.
55 * Given the above conditions, this class manages long running sessions by providing the following methods,
57 * <li>{@link #bind()} : binds the session owned by this conversation to the resource map.</li>
58 * <li>{@link #startTransaction()} : starts a transaction.</li>
59 * <li>{@link #commit()} : commits the current transaction, with the option of restarting a new transaction.</li>
60 * <li>{@link #unbind()} : unbinds the session owned by this conversation from the resource map.</li>
61 * <li>{@link #close()} : closes the session owned by this conversation.</li>
64 * With the exception of {@link #unbind()} (which should be called explicitly), the above sequence must be strictly followed to
65 * maintain a consistent session state. Even though it is possible to interweave multiple conversations at the same time, for a
66 * specific conversation the above sequence must be followed.
68 * @see http://www.hibernate.org/42.html
70 * @author n.hoffmann,c.mathew
73 public class ConversationHolder
{
75 private static final Logger logger
= Logger
.getLogger(ConversationHolder
.class);
78 private SessionFactory sessionFactory
;
81 private DataSource dataSource
;
84 private PlatformTransactionManager transactionManager
;
88 * The persistence context for this conversation
90 private Session longSession
= null;
93 * Spring communicates with hibernate sessions via a SessionHolder object
95 private SessionHolder sessionHolder
= null;
98 * @see TransactionDefinition
100 private TransactionDefinition definition
;
103 * This conversations transaction
105 private TransactionStatus transactionStatus
;
108 private boolean closed
= false;
110 private FlushMode defaultFlushMode
= FlushMode
.COMMIT
;
113 * Simple constructor used by Spring only
115 protected ConversationHolder(){
120 * Create a new Conversation holder and bind it immediately.
123 * @param sessionFactory
124 * @param transactionManager
126 public ConversationHolder(DataSource dataSource
, SessionFactory sessionFactory
,
127 PlatformTransactionManager transactionManager
) {
128 this(dataSource
, sessionFactory
, transactionManager
, true);
132 * Create a new Conversation holder and optionally bind it immediately.
135 * @param sessionFactory
136 * @param transactionManager
138 public ConversationHolder(DataSource dataSource
, SessionFactory sessionFactory
,
139 PlatformTransactionManager transactionManager
, boolean bindNow
) {
141 this.dataSource
= dataSource
;
142 this.sessionFactory
= sessionFactory
;
143 this.transactionManager
= transactionManager
;
147 if(TransactionSynchronizationManager
.hasResource(getDataSource())){
148 TransactionSynchronizationManager
.unbindResource(getDataSource());
155 * This method has to be called when starting a new unit-of-work. All required resources are
156 * bound so that SessionFactory.getCurrentSession() returns the right session for this conversation
160 logger
.info("Binding resources for ConversationHolder");
162 if(TransactionSynchronizationManager
.isSynchronizationActive()){
163 logger
.trace("Clearing active transaction synchronization");
164 TransactionSynchronizationManager
.clearSynchronization();
169 logger
.info("Starting new Synchronization in TransactionSynchronizationManager");
170 TransactionSynchronizationManager
.initSynchronization();
173 if(TransactionSynchronizationManager
.hasResource(getSessionFactory())){
174 logger
.trace("Unbinding resource from TransactionSynchronizationManager with key: " + getSessionFactory());
175 TransactionSynchronizationManager
.unbindResource(getSessionFactory());
178 if(logger
.isTraceEnabled()){
179 logger
.trace("Binding Session to TransactionSynchronizationManager:" + getSessionHolder() + " Session [" + getSessionHolder().getSession().hashCode() + "] with key: " + getSessionFactory());
181 logger
.info("Binding Session to TransactionSynchronizationManager: Session: " + getSessionHolder());
183 TransactionSynchronizationManager
.bindResource(getSessionFactory(), getSessionHolder());
187 } catch(Exception e
){
188 logger
.error("Error binding resources for session", e
);
194 * This method has to be called when suspending the current unit of work. The conversation can be later bound again.
196 public void unbind() {
198 logger
.info("Unbinding resources for ConversationHolder");
200 if(TransactionSynchronizationManager
.isSynchronizationActive()){
201 TransactionSynchronizationManager
.clearSynchronization();
206 // unbind the current session.
207 // there is no need to bind a new session, since HibernateTransactionManager will create a new one
208 // if the resource map does not contain one (ditto for the datasource-to-connection entry).
209 if(logger
.isTraceEnabled()){
210 logger
.trace("Unbinding SessionFactory [" + getSessionFactory().hashCode() + "]");
212 TransactionSynchronizationManager
.unbindResource(getSessionFactory());
213 if(TransactionSynchronizationManager
.hasResource(getDataSource())){
214 if(logger
.isTraceEnabled()){
215 logger
.trace("Unbinding DataSource [" + getDataSource().hashCode() + "]");
217 TransactionSynchronizationManager
.unbindResource(getDataSource());
222 public SessionHolder
getSessionHolder(){
223 if(this.sessionHolder
== null){
224 this.sessionHolder
= new SessionHolder(getSession());
225 logger
.info("Creating new SessionHolder:" + sessionHolder
);
227 return this.sessionHolder
;
233 private DataSource
getDataSource() {
234 return this.dataSource
;
238 * @return true if this longSession is bound to the session factory.
240 public boolean isBound(){
241 //return sessionHolder != null && longSession != null && longSession.isConnected();
242 SessionHolder currentSessionHolder
= (SessionHolder
)TransactionSynchronizationManager
.getResource(getSessionFactory());
243 return longSession
!= null && currentSessionHolder
!= null && getSessionFactory().getCurrentSession().equals(longSession
);
247 * Creates an instance of TransactionStatus and binds it to this conversation manager.
248 * At the moment we allow only one transaction per conversation holder.
250 * @return the transaction status bound to this conversation holder
252 public TransactionStatus
startTransaction(){
253 if (isTransactionActive()){
254 logger
.warn("We allow only one transaction at the moment but startTransaction " +
255 "was called a second time.\nReturning the transaction already associated with this " +
256 "ConversationManager");
258 //always safe to remove the datasource-to-connection entry since we
259 // know that HibernateTransactionManager will create a new one
260 if(TransactionSynchronizationManager
.hasResource(getDataSource())){
261 TransactionSynchronizationManager
.unbindResource(getDataSource());
264 transactionStatus
= transactionManager
.getTransaction(definition
);
266 logger
.info("Transaction started: " + transactionStatus
);
268 return transactionStatus
;
272 * @return if there is a running transaction
274 public boolean isTransactionActive(){
275 return transactionStatus
!= null && !transactionStatus
.isCompleted();
278 public void evict(Object object
){
279 getSession().evict(object
);
282 public void refresh(Object object
){
283 getSession().refresh(object
);
287 getSession().clear();
291 * Commit the running transaction.
293 public void commit(){
298 * Commit the running transaction but optionally start a
299 * new one right away.
301 * @param restartTransaction whether to start a new transaction
303 public TransactionStatus
commit(boolean restartTransaction
){
304 if(isTransactionActive()){
306 if(getSessionHolder().isRollbackOnly()){
307 logger
.error("Commiting this session will not work. It has been marked as rollback only.");
309 // if a datasource-to-connection entry already exists in the resource map
310 // then its setSynchronizedWithTransaction should be true, since hibernate has added
312 // if the datasource-to-connection entry does not exist then we need to create one
313 // and explicitly setSynchronizedWithTransaction to true.
314 TransactionSynchronizationManager
.getResource(getDataSource());
315 if(!TransactionSynchronizationManager
.hasResource(getDataSource())){
317 ConnectionHolder ch
= new ConnectionHolder(getDataSource().getConnection());
318 ch
.setSynchronizedWithTransaction(true);
319 TransactionSynchronizationManager
.bindResource(getDataSource(),ch
);
321 } catch (IllegalStateException e
) {
322 // TODO Auto-generated catch block
324 } catch (SQLException e
) {
325 // TODO Auto-generated catch block
330 // commit the changes
331 transactionManager
.commit(transactionStatus
);
332 logger
.info("Committing Session: " + getSessionHolder());
333 // propagate transaction end
334 CdmPostDataChangeObservableListener
.getDefault().delayedNotify();
336 // Reset the transactionStatus.
337 transactionStatus
= null;
339 // Committing a transaction frees all resources.
340 // Since we are in a conversation we directly rebind those resources and start a new transaction
342 if(restartTransaction
){
343 return startTransaction();
346 logger
.warn("No active transaction but commit was called");
352 * @return the session associated with this conversation manager
354 public Session
getSession() {
358 if(longSession
== null){
359 longSession
= getNewSession();
360 whatStr
= "Creating";
364 if(logger
.isDebugEnabled()){
365 logger
.debug(whatStr
+ " Session: [" + longSession
.hashCode() + "] " + longSession
);
367 logger
.info(whatStr
+ " Session: [" + longSession
.hashCode() + "] ");
373 * @return a new session to be managed by this conversation
375 private Session
getNewSession() {
377 // Interesting: http://stackoverflow.com/questions/3526556/session-connection-deprecated-on-hibernate
378 // Also, http://blog-it.hypoport.de/2012/05/10/hibernate-4-migration/
380 // This will create a new session which must be explicitly managed by this conversation, which includes
381 // binding / unbinding / closing session as well as starting / committing transactions.
382 Session session
= sessionFactory
.openSession();
383 session
.setFlushMode(getDefaultFlushMode());
392 * @return the session factory that is bound to this conversation manager
394 public SessionFactory
getSessionFactory() {
395 return sessionFactory
;
398 public void delete(Object object
){
399 this.getSession().delete(object
);
403 * Facades Session.lock()
405 public void lock(Object persistentObject
, LockMode lockMode
) {
406 getSession().lock(persistentObject
, lockMode
);
409 public void lock(String entityName
, Object persistentObject
, LockMode lockMode
){
410 getSession().lock(entityName
, persistentObject
, lockMode
);
414 * @return the definition
416 public TransactionDefinition
getDefinition() {
421 * @param definition the definition to set
423 public void setDefinition(TransactionDefinition definition
) {
424 this.definition
= definition
;
428 * Register to get updated after any interaction with the datastore
430 public void registerForDataStoreChanges(IConversationEnabled observer
) {
431 CdmPostDataChangeObservableListener
.getDefault().register(observer
);
435 * Register to get updated after any interaction with the datastore
437 public void unregisterForDataStoreChanges(IConversationEnabled observer
) {
438 CdmPostDataChangeObservableListener
.getDefault().unregister(observer
);
442 * Free resources bound to this conversationHolder
445 if(getSession().isOpen()) {
446 getSession().close();
450 sessionHolder
= null;
454 public boolean isClosed(){
458 public boolean isCompleted(){
459 return transactionStatus
== null || transactionStatus
.isCompleted();
463 * @return the defaultFlushMode
465 public FlushMode
getDefaultFlushMode() {
466 return defaultFlushMode
;
470 * @param defaultFlushMode the defaultFlushMode to set
472 public void setDefaultFlushMode(FlushMode defaultFlushMode
) {
473 this.defaultFlushMode
= defaultFlushMode
;