Merge branch 'release/4.9.0'
[cdmlib.git] / cdmlib-services / src / main / java / eu / etaxonomy / cdm / api / conversation / ConversationHolder.java
1 /**
2 * Copyright (C) 2009 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
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.
8 */
9
10 package eu.etaxonomy.cdm.api.conversation;
11
12 import java.sql.SQLException;
13
14 import javax.sql.DataSource;
15
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;
28
29 import eu.etaxonomy.cdm.persistence.hibernate.CdmPostDataChangeObservableListener;
30
31 /**
32 * This is an implementation of the session-per-conversation pattern for usage in a Spring context.
33 *
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.
37 * <p>
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,
40 * <ul>
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>
43 * </ul>
44 * <p>
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.
49 * <p>
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.
54 * <p>
55 * Given the above conditions, this class manages long running sessions by providing the following methods,
56 * <ul>
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>
62 * </ul>
63 * <p>
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.
67 *
68 * @see http://www.hibernate.org/42.html
69 *
70 * @author n.hoffmann,c.mathew
71 * @created 12.03.2009
72 * @version 1.0
73 */
74 public class ConversationHolder {
75
76 private static final Logger logger = Logger.getLogger(ConversationHolder.class);
77
78 @Autowired
79 private SessionFactory sessionFactory;
80
81 @Autowired
82 private DataSource dataSource;
83
84 @Autowired
85 private PlatformTransactionManager transactionManager;
86
87
88 /**
89 * The persistence context for this conversation
90 */
91 private Session longSession = null;
92
93 /**
94 * Spring communicates with hibernate sessions via a SessionHolder object
95 */
96 private SessionHolder sessionHolder = null;
97
98 /**
99 * @see TransactionDefinition
100 */
101 private TransactionDefinition definition;
102
103 /**
104 * This conversations transaction
105 */
106 private TransactionStatus transactionStatus;
107
108
109 private boolean closed = false;
110
111 private FlushMode defaultFlushMode = FlushMode.COMMIT;
112
113 /**
114 * Simple constructor used by Spring only
115 */
116 protected ConversationHolder(){
117 closed = false;
118 }
119
120 /**
121 * Create a new Conversation holder and bind it immediately.
122 *
123 * @param dataSource
124 * @param sessionFactory
125 * @param transactionManager
126 */
127 public ConversationHolder(DataSource dataSource, SessionFactory sessionFactory,
128 PlatformTransactionManager transactionManager) {
129 this(dataSource, sessionFactory, transactionManager, true);
130 }
131
132 /**
133 * Create a new Conversation holder and optionally bind it immediately.
134 *
135 * @param dataSource
136 * @param sessionFactory
137 * @param transactionManager
138 */
139 public ConversationHolder(DataSource dataSource, SessionFactory sessionFactory,
140 PlatformTransactionManager transactionManager, boolean bindNow) {
141 this();
142 this.dataSource = dataSource;
143 this.sessionFactory = sessionFactory;
144 this.transactionManager = transactionManager;
145
146 if(bindNow) {
147 bind();
148 if(TransactionSynchronizationManager.hasResource(getDataSource())){
149 TransactionSynchronizationManager.unbindResource(getDataSource());
150 }
151 }
152
153 }
154
155 /**
156 * This method has to be called when starting a new unit-of-work. All required resources are
157 * bound so that SessionFactory.getCurrentSession() returns the right session for this conversation
158 */
159 public void bind() {
160
161 logger.info("Binding resources for ConversationHolder");
162
163 if(TransactionSynchronizationManager.isSynchronizationActive()){
164 logger.trace("Clearing active transaction synchronization");
165 TransactionSynchronizationManager.clearSynchronization();
166 }
167
168 try{
169
170 logger.info("Starting new Synchronization in TransactionSynchronizationManager");
171 TransactionSynchronizationManager.initSynchronization();
172
173
174 if(TransactionSynchronizationManager.hasResource(getSessionFactory())){
175 logger.trace("Unbinding resource from TransactionSynchronizationManager with key: " + getSessionFactory());
176 TransactionSynchronizationManager.unbindResource(getSessionFactory());
177 }
178
179 if(logger.isTraceEnabled()){
180 logger.trace("Binding Session to TransactionSynchronizationManager:" + getSessionHolder() + " Session [" + getSessionHolder().getSession().hashCode() + "] with key: " + getSessionFactory());
181 } else {
182 logger.info("Binding Session to TransactionSynchronizationManager: Session: " + getSessionHolder());
183 }
184 TransactionSynchronizationManager.bindResource(getSessionFactory(), getSessionHolder());
185
186
187
188 } catch(Exception e){
189 logger.error("Error binding resources for session", e);
190 }
191
192 }
193
194 /**
195 * This method has to be called when suspending the current unit of work. The conversation can be later bound again.
196 */
197 public void unbind() {
198
199 logger.info("Unbinding resources for ConversationHolder");
200
201 if(TransactionSynchronizationManager.isSynchronizationActive()){
202 TransactionSynchronizationManager.clearSynchronization();
203 }
204
205
206 if(isBound()) {
207 // unbind the current session.
208 // there is no need to bind a new session, since HibernateTransactionManager will create a new one
209 // if the resource map does not contain one (ditto for the datasource-to-connection entry).
210 if(logger.isTraceEnabled()){
211 logger.trace("Unbinding SessionFactory [" + getSessionFactory().hashCode() + "]");
212 }
213 TransactionSynchronizationManager.unbindResource(getSessionFactory());
214 if(TransactionSynchronizationManager.hasResource(getDataSource())){
215 if(logger.isTraceEnabled()){
216 logger.trace("Unbinding DataSource [" + getDataSource().hashCode() + "]");
217 }
218 TransactionSynchronizationManager.unbindResource(getDataSource());
219 }
220 }
221 }
222
223 public SessionHolder getSessionHolder(){
224 if(this.sessionHolder == null){
225 this.sessionHolder = new SessionHolder(getSession());
226 logger.info("Creating new SessionHolder:" + sessionHolder);
227 }
228 return this.sessionHolder;
229 }
230
231 /**
232 * @return
233 */
234 private DataSource getDataSource() {
235 return this.dataSource;
236 }
237
238 /**
239 * @return true if this longSession is bound to the session factory.
240 */
241 public boolean isBound(){
242 //return sessionHolder != null && longSession != null && longSession.isConnected();
243 SessionHolder currentSessionHolder = (SessionHolder)TransactionSynchronizationManager.getResource(getSessionFactory());
244 return longSession != null && currentSessionHolder != null && getSessionFactory().getCurrentSession().equals(longSession);
245 }
246
247 /**
248 * Creates an instance of TransactionStatus and binds it to this conversation manager.
249 * At the moment we allow only one transaction per conversation holder.
250 *
251 * @return the transaction status bound to this conversation holder
252 */
253 public TransactionStatus startTransaction(){
254 if (isTransactionActive()){
255 logger.warn("We allow only one transaction at the moment but startTransaction " +
256 "was called a second time.\nReturning the transaction already associated with this " +
257 "ConversationManager");
258 }else{
259 //always safe to remove the datasource-to-connection entry since we
260 // know that HibernateTransactionManager will create a new one
261 if(TransactionSynchronizationManager.hasResource(getDataSource())){
262 TransactionSynchronizationManager.unbindResource(getDataSource());
263 }
264
265 transactionStatus = transactionManager.getTransaction(definition);
266
267 logger.info("Transaction started: " + transactionStatus);
268 }
269 return transactionStatus;
270 }
271
272 /**
273 * @return if there is a running transaction
274 */
275 public boolean isTransactionActive(){
276 return transactionStatus != null;
277 }
278
279 /* (non-Javadoc)
280 * @see org.hibernate.Session#evict(java.lang.Object object)
281 */
282 public void evict(Object object){
283 getSession().evict(object);
284 }
285
286 /* (non-Javadoc)
287 * @see org.hibernate.Session#refresh(java.lang.Object object)
288 */
289 public void refresh(Object object){
290 getSession().refresh(object);
291 }
292
293 /* (non-Javadoc)
294 * @see org.hibernate.Session#clear()
295 */
296 public void clear(){
297 getSession().clear();
298 }
299
300 /**
301 * Commit the running transaction.
302 */
303 public void commit(){
304 commit(true);
305 }
306
307 /**
308 * Commit the running transaction but optionally start a
309 * new one right away.
310 *
311 * @param restartTransaction whether to start a new transaction
312 */
313 public TransactionStatus commit(boolean restartTransaction){
314 if(isTransactionActive()){
315
316 if(getSessionHolder().isRollbackOnly()){
317 logger.error("Commiting this session will not work. It has been marked as rollback only.");
318 }
319 // if a datasource-to-connection entry already exists in the resource map
320 // then its setSynchronizedWithTransaction should be true, since hibernate has added
321 // this entry.
322 // if the datasource-to-connection entry does not exist then we need to create one
323 // and explicitly setSynchronizedWithTransaction to true.
324 TransactionSynchronizationManager.getResource(getDataSource());
325 if(!TransactionSynchronizationManager.hasResource(getDataSource())){
326 try {
327 ConnectionHolder ch = new ConnectionHolder(getDataSource().getConnection());
328 ch.setSynchronizedWithTransaction(true);
329 TransactionSynchronizationManager.bindResource(getDataSource(),ch);
330
331 } catch (IllegalStateException e) {
332 // TODO Auto-generated catch block
333 e.printStackTrace();
334 } catch (SQLException e) {
335 // TODO Auto-generated catch block
336 e.printStackTrace();
337 }
338 }
339
340 // commit the changes
341 transactionManager.commit(transactionStatus);
342 logger.info("Committing Session: " + getSessionHolder());
343 // propagate transaction end
344 CdmPostDataChangeObservableListener.getDefault().delayedNotify();
345
346 // Reset the transactionStatus.
347 transactionStatus = null;
348
349 // Committing a transaction frees all resources.
350 // Since we are in a conversation we directly rebind those resources and start a new transaction
351 bind();
352 if(restartTransaction){
353 return startTransaction();
354 }
355 }else{
356 logger.warn("No active transaction but commit was called");
357 }
358 return null;
359 }
360
361 /**
362 * @return the session associated with this conversation manager
363 */
364 public Session getSession() {
365 if(longSession == null){
366 longSession = getNewSession();
367 logger.info("Creating Session: [" + longSession.hashCode() + "] " + longSession);
368 } else {
369 logger.info("Reusing Session: [" + longSession.hashCode() + "] " + longSession);
370 }
371 return longSession;
372 }
373
374 /**
375 * @return a new session to be managed by this conversation
376 */
377 private Session getNewSession() {
378
379 // Interesting: http://stackoverflow.com/questions/3526556/session-connection-deprecated-on-hibernate
380 // Also, http://blog-it.hypoport.de/2012/05/10/hibernate-4-migration/
381
382 // This will create a new session which must be explicitly managed by this conversation, which includes
383 // binding / unbinding / closing session as well as starting / committing transactions.
384 Session session = sessionFactory.openSession();
385 session.setFlushMode(getDefaultFlushMode());
386
387 return session;
388 }
389
390
391
392
393 /**
394 * @return the session factory that is bound to this conversation manager
395 */
396 public SessionFactory getSessionFactory() {
397 return sessionFactory;
398 }
399
400 public void delete(Object object){
401 this.getSession().delete(object);
402 }
403
404 /**
405 * Facades Session.lock()
406 */
407 public void lock(Object persistentObject, LockMode lockMode) {
408 getSession().lock(persistentObject, lockMode);
409 }
410
411 public void lock(String entityName, Object persistentObject, LockMode lockMode){
412 getSession().lock(entityName, persistentObject, lockMode);
413 }
414
415 /**
416 * @return the definition
417 */
418 public TransactionDefinition getDefinition() {
419 return definition;
420 }
421
422 /**
423 * @param definition the definition to set
424 */
425 public void setDefinition(TransactionDefinition definition) {
426 this.definition = definition;
427 }
428
429 /**
430 * Register to get updated after any interaction with the datastore
431 */
432 public void registerForDataStoreChanges(IConversationEnabled observer) {
433 CdmPostDataChangeObservableListener.getDefault().register(observer);
434 }
435
436 /**
437 * Register to get updated after any interaction with the datastore
438 */
439 public void unregisterForDataStoreChanges(IConversationEnabled observer) {
440 CdmPostDataChangeObservableListener.getDefault().unregister(observer);
441 }
442
443 /**
444 * Free resources bound to this conversationHolder
445 */
446 public void close(){
447 if(getSession().isOpen()) {
448 getSession().close();
449 unbind();
450 }
451 longSession = null;
452 sessionHolder = null;
453 closed = true;
454 }
455
456 public boolean isClosed(){
457 return closed;
458 }
459
460 public boolean isCompleted(){
461 return transactionStatus == null || transactionStatus.isCompleted();
462 }
463
464 /**
465 * @return the defaultFlushMode
466 */
467 public FlushMode getDefaultFlushMode() {
468 return defaultFlushMode;
469 }
470
471 /**
472 * @param defaultFlushMode the defaultFlushMode to set
473 */
474 public void setDefaultFlushMode(FlushMode defaultFlushMode) {
475 this.defaultFlushMode = defaultFlushMode;
476 }
477
478
479 }