ref #6241 replaced @created by @since in cdmlib
[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 * @since 12.03.2009
72 */
73 public class ConversationHolder {
74
75 private static final Logger logger = Logger.getLogger(ConversationHolder.class);
76
77 @Autowired
78 private SessionFactory sessionFactory;
79
80 @Autowired
81 private DataSource dataSource;
82
83 @Autowired
84 private PlatformTransactionManager transactionManager;
85
86
87 /**
88 * The persistence context for this conversation
89 */
90 private Session longSession = null;
91
92 /**
93 * Spring communicates with hibernate sessions via a SessionHolder object
94 */
95 private SessionHolder sessionHolder = null;
96
97 /**
98 * @see TransactionDefinition
99 */
100 private TransactionDefinition definition;
101
102 /**
103 * This conversations transaction
104 */
105 private TransactionStatus transactionStatus;
106
107
108 private boolean closed = false;
109
110 private FlushMode defaultFlushMode = FlushMode.COMMIT;
111
112 /**
113 * Simple constructor used by Spring only
114 */
115 protected ConversationHolder(){
116 closed = false;
117 }
118
119 /**
120 * Create a new Conversation holder and bind it immediately.
121 *
122 * @param dataSource
123 * @param sessionFactory
124 * @param transactionManager
125 */
126 public ConversationHolder(DataSource dataSource, SessionFactory sessionFactory,
127 PlatformTransactionManager transactionManager) {
128 this(dataSource, sessionFactory, transactionManager, true);
129 }
130
131 /**
132 * Create a new Conversation holder and optionally bind it immediately.
133 *
134 * @param dataSource
135 * @param sessionFactory
136 * @param transactionManager
137 */
138 public ConversationHolder(DataSource dataSource, SessionFactory sessionFactory,
139 PlatformTransactionManager transactionManager, boolean bindNow) {
140 this();
141 this.dataSource = dataSource;
142 this.sessionFactory = sessionFactory;
143 this.transactionManager = transactionManager;
144
145 if(bindNow) {
146 bind();
147 if(TransactionSynchronizationManager.hasResource(getDataSource())){
148 TransactionSynchronizationManager.unbindResource(getDataSource());
149 }
150 }
151
152 }
153
154 /**
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
157 */
158 public void bind() {
159
160 logger.info("Binding resources for ConversationHolder");
161
162 if(TransactionSynchronizationManager.isSynchronizationActive()){
163 logger.trace("Clearing active transaction synchronization");
164 TransactionSynchronizationManager.clearSynchronization();
165 }
166
167 try{
168
169 logger.info("Starting new Synchronization in TransactionSynchronizationManager");
170 TransactionSynchronizationManager.initSynchronization();
171
172
173 if(TransactionSynchronizationManager.hasResource(getSessionFactory())){
174 logger.trace("Unbinding resource from TransactionSynchronizationManager with key: " + getSessionFactory());
175 TransactionSynchronizationManager.unbindResource(getSessionFactory());
176 }
177
178 if(logger.isTraceEnabled()){
179 logger.trace("Binding Session to TransactionSynchronizationManager:" + getSessionHolder() + " Session [" + getSessionHolder().getSession().hashCode() + "] with key: " + getSessionFactory());
180 } else {
181 logger.info("Binding Session to TransactionSynchronizationManager: Session: " + getSessionHolder());
182 }
183 TransactionSynchronizationManager.bindResource(getSessionFactory(), getSessionHolder());
184
185
186
187 } catch(Exception e){
188 logger.error("Error binding resources for session", e);
189 }
190
191 }
192
193 /**
194 * This method has to be called when suspending the current unit of work. The conversation can be later bound again.
195 */
196 public void unbind() {
197
198 logger.info("Unbinding resources for ConversationHolder");
199
200 if(TransactionSynchronizationManager.isSynchronizationActive()){
201 TransactionSynchronizationManager.clearSynchronization();
202 }
203
204
205 if(isBound()) {
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() + "]");
211 }
212 TransactionSynchronizationManager.unbindResource(getSessionFactory());
213 if(TransactionSynchronizationManager.hasResource(getDataSource())){
214 if(logger.isTraceEnabled()){
215 logger.trace("Unbinding DataSource [" + getDataSource().hashCode() + "]");
216 }
217 TransactionSynchronizationManager.unbindResource(getDataSource());
218 }
219 }
220 }
221
222 public SessionHolder getSessionHolder(){
223 if(this.sessionHolder == null){
224 this.sessionHolder = new SessionHolder(getSession());
225 logger.info("Creating new SessionHolder:" + sessionHolder);
226 }
227 return this.sessionHolder;
228 }
229
230 /**
231 * @return
232 */
233 private DataSource getDataSource() {
234 return this.dataSource;
235 }
236
237 /**
238 * @return true if this longSession is bound to the session factory.
239 */
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);
244 }
245
246 /**
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.
249 *
250 * @return the transaction status bound to this conversation holder
251 */
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");
257 }else{
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());
262 }
263
264 transactionStatus = transactionManager.getTransaction(definition);
265
266 logger.info("Transaction started: " + transactionStatus);
267 }
268 return transactionStatus;
269 }
270
271 /**
272 * @return if there is a running transaction
273 */
274 public boolean isTransactionActive(){
275 return transactionStatus != null && !transactionStatus.isCompleted();
276 }
277
278 public void evict(Object object){
279 getSession().evict(object);
280 }
281
282 public void refresh(Object object){
283 getSession().refresh(object);
284 }
285
286 public void clear(){
287 getSession().clear();
288 }
289
290 /**
291 * Commit the running transaction.
292 */
293 public void commit(){
294 commit(true);
295 }
296
297 /**
298 * Commit the running transaction but optionally start a
299 * new one right away.
300 *
301 * @param restartTransaction whether to start a new transaction
302 */
303 public TransactionStatus commit(boolean restartTransaction){
304 if(isTransactionActive()){
305
306 if(getSessionHolder().isRollbackOnly()){
307 logger.error("Commiting this session will not work. It has been marked as rollback only.");
308 }
309 // if a datasource-to-connection entry already exists in the resource map
310 // then its setSynchronizedWithTransaction should be true, since hibernate has added
311 // this entry.
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())){
316 try {
317 ConnectionHolder ch = new ConnectionHolder(getDataSource().getConnection());
318 ch.setSynchronizedWithTransaction(true);
319 TransactionSynchronizationManager.bindResource(getDataSource(),ch);
320
321 } catch (IllegalStateException e) {
322 // TODO Auto-generated catch block
323 e.printStackTrace();
324 } catch (SQLException e) {
325 // TODO Auto-generated catch block
326 e.printStackTrace();
327 }
328 }
329
330 // commit the changes
331 transactionManager.commit(transactionStatus);
332 logger.info("Committing Session: " + getSessionHolder());
333 // propagate transaction end
334 CdmPostDataChangeObservableListener.getDefault().delayedNotify();
335
336 // Reset the transactionStatus.
337 transactionStatus = null;
338
339 // Committing a transaction frees all resources.
340 // Since we are in a conversation we directly rebind those resources and start a new transaction
341 bind();
342 if(restartTransaction){
343 return startTransaction();
344 }
345 }else{
346 logger.warn("No active transaction but commit was called");
347 }
348 return null;
349 }
350
351 /**
352 * @return the session associated with this conversation manager
353 */
354 public Session getSession() {
355
356 String whatStr;
357
358 if(longSession == null){
359 longSession = getNewSession();
360 whatStr = "Creating";
361 } else {
362 whatStr = "Reusing";
363 }
364 if(logger.isDebugEnabled()){
365 logger.debug(whatStr + " Session: [" + longSession.hashCode() + "] " + longSession);
366 } else {
367 logger.info(whatStr + " Session: [" + longSession.hashCode() + "] ");
368 }
369 return longSession;
370 }
371
372 /**
373 * @return a new session to be managed by this conversation
374 */
375 private Session getNewSession() {
376
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/
379
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());
384
385 return session;
386 }
387
388
389
390
391 /**
392 * @return the session factory that is bound to this conversation manager
393 */
394 public SessionFactory getSessionFactory() {
395 return sessionFactory;
396 }
397
398 public void delete(Object object){
399 this.getSession().delete(object);
400 }
401
402 /**
403 * Facades Session.lock()
404 */
405 public void lock(Object persistentObject, LockMode lockMode) {
406 getSession().lock(persistentObject, lockMode);
407 }
408
409 public void lock(String entityName, Object persistentObject, LockMode lockMode){
410 getSession().lock(entityName, persistentObject, lockMode);
411 }
412
413 /**
414 * @return the definition
415 */
416 public TransactionDefinition getDefinition() {
417 return definition;
418 }
419
420 /**
421 * @param definition the definition to set
422 */
423 public void setDefinition(TransactionDefinition definition) {
424 this.definition = definition;
425 }
426
427 /**
428 * Register to get updated after any interaction with the datastore
429 */
430 public void registerForDataStoreChanges(IConversationEnabled observer) {
431 CdmPostDataChangeObservableListener.getDefault().register(observer);
432 }
433
434 /**
435 * Register to get updated after any interaction with the datastore
436 */
437 public void unregisterForDataStoreChanges(IConversationEnabled observer) {
438 CdmPostDataChangeObservableListener.getDefault().unregister(observer);
439 }
440
441 /**
442 * Free resources bound to this conversationHolder
443 */
444 public void close(){
445 if(getSession().isOpen()) {
446 getSession().close();
447 unbind();
448 }
449 longSession = null;
450 sessionHolder = null;
451 closed = true;
452 }
453
454 public boolean isClosed(){
455 return closed;
456 }
457
458 public boolean isCompleted(){
459 return transactionStatus == null || transactionStatus.isCompleted();
460 }
461
462 /**
463 * @return the defaultFlushMode
464 */
465 public FlushMode getDefaultFlushMode() {
466 return defaultFlushMode;
467 }
468
469 /**
470 * @param defaultFlushMode the defaultFlushMode to set
471 */
472 public void setDefaultFlushMode(FlushMode defaultFlushMode) {
473 this.defaultFlushMode = defaultFlushMode;
474 }
475
476
477 }