Merge branch 'develop' into feature/cdm-4.7
[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 /**
112 * Simple constructor used by Spring only
113 */
114 protected ConversationHolder(){
115 closed = false;
116 }
117
118 public ConversationHolder(DataSource dataSource, SessionFactory sessionFactory,
119 PlatformTransactionManager transactionManager) {
120 this();
121 this.dataSource = dataSource;
122 this.sessionFactory = sessionFactory;
123 this.transactionManager = transactionManager;
124
125 bind();
126
127 if(TransactionSynchronizationManager.hasResource(getDataSource())){
128 TransactionSynchronizationManager.unbindResource(getDataSource());
129 }
130
131 }
132
133 /**
134 * This method has to be called when starting a new unit-of-work. All required resources are
135 * bound so that SessionFactory.getCurrentSession() returns the right session for this conversation
136 */
137 public void bind() {
138
139 logger.info("Binding resources for ConversationHolder");
140
141 if(TransactionSynchronizationManager.isSynchronizationActive()){
142 TransactionSynchronizationManager.clearSynchronization();
143 }
144
145 try{
146
147 logger.info("Starting new Synchronization in TransactionSynchronizationManager");
148 TransactionSynchronizationManager.initSynchronization();
149
150
151 if(TransactionSynchronizationManager.hasResource(getSessionFactory())){
152 TransactionSynchronizationManager.unbindResource(getSessionFactory());
153 }
154
155 logger.info("Binding Session to TransactionSynchronizationManager: Session: " + getSessionHolder());
156 TransactionSynchronizationManager.bindResource(getSessionFactory(), getSessionHolder());
157
158
159
160 } catch(Exception e){
161 logger.error("Error binding resources for session", e);
162 }
163
164 }
165
166 /**
167 * This method has to be called when suspending the current unit of work. The conversation can be later bound again.
168 */
169 public void unbind() {
170
171 logger.info("Unbinding resources for ConversationHolder");
172
173 if(TransactionSynchronizationManager.isSynchronizationActive()){
174 TransactionSynchronizationManager.clearSynchronization();
175 }
176
177
178 if(isBound()) {
179 // unbind the current session.
180 // there is no need to bind a new session, since HibernateTransactionManager will create a new one
181 // if the resource map does not contain one (ditto for the datasource-to-connection entry).
182 TransactionSynchronizationManager.unbindResource(getSessionFactory());
183 if(TransactionSynchronizationManager.hasResource(getDataSource())){
184 TransactionSynchronizationManager.unbindResource(getDataSource());
185 }
186 }
187 }
188
189 public SessionHolder getSessionHolder(){
190 if(this.sessionHolder == null){
191 logger.info("Creating new SessionHolder");
192 this.sessionHolder = new SessionHolder(getSession());
193 }
194 return this.sessionHolder;
195 }
196
197 /**
198 * @return
199 */
200 private DataSource getDataSource() {
201 return this.dataSource;
202 }
203
204 /**
205 * @return true if this longSession is bound to the session factory.
206 */
207 public boolean isBound(){
208 //return sessionHolder != null && longSession != null && longSession.isConnected();
209 SessionHolder currentSessionHolder = (SessionHolder)TransactionSynchronizationManager.getResource(getSessionFactory());
210 return longSession != null && currentSessionHolder != null && getSessionFactory().getCurrentSession() == longSession;
211 }
212
213 /**
214 * Creates an instance of TransactionStatus and binds it to this conversation manager.
215 * At the moment we allow only one transaction per conversation holder.
216 *
217 * @return the transaction status bound to this conversation holder
218 */
219 public TransactionStatus startTransaction(){
220 if (isTransactionActive()){
221 logger.warn("We allow only one transaction at the moment but startTransaction " +
222 "was called a second time.\nReturning the transaction already associated with this " +
223 "ConversationManager");
224 }else{
225 //always safe to remove the datasource-to-connection entry since we
226 // know that HibernateTransactionManager will create a new one
227 if(TransactionSynchronizationManager.hasResource(getDataSource())){
228 TransactionSynchronizationManager.unbindResource(getDataSource());
229 }
230
231 transactionStatus = transactionManager.getTransaction(definition);
232
233 logger.info("Transaction started: " + transactionStatus);
234 }
235 return transactionStatus;
236 }
237
238 /**
239 * @return if there is a running transaction
240 */
241 public boolean isTransactionActive(){
242 return transactionStatus != null;
243 }
244
245 /* (non-Javadoc)
246 * @see org.hibernate.Session#evict(java.lang.Object object)
247 */
248 public void evict(Object object){
249 getSession().evict(object);
250 }
251
252 /* (non-Javadoc)
253 * @see org.hibernate.Session#refresh(java.lang.Object object)
254 */
255 public void refresh(Object object){
256 getSession().refresh(object);
257 }
258
259 /* (non-Javadoc)
260 * @see org.hibernate.Session#clear()
261 */
262 public void clear(){
263 getSession().clear();
264 }
265
266 /**
267 * Commit the running transaction.
268 */
269 public void commit(){
270 commit(true);
271 }
272
273 /**
274 * Commit the running transaction but optionally start a
275 * new one right away.
276 *
277 * @param restartTransaction whether to start a new transaction
278 */
279 public TransactionStatus commit(boolean restartTransaction){
280 if(isTransactionActive()){
281
282 if(getSessionHolder().isRollbackOnly()){
283 logger.error("Commiting this session will not work. It has been marked as rollback only.");
284 }
285 // if a datasource-to-connection entry already exists in the resource map
286 // then its setSynchronizedWithTransaction should be true, since hibernate has added
287 // this entry.
288 // if the datasource-to-connection entry does not exist then we need to create one
289 // and explicitly setSynchronizedWithTransaction to true.
290 TransactionSynchronizationManager.getResource(getDataSource());
291 if(!TransactionSynchronizationManager.hasResource(getDataSource())){
292 try {
293 ConnectionHolder ch = new ConnectionHolder(getDataSource().getConnection());
294 ch.setSynchronizedWithTransaction(true);
295 TransactionSynchronizationManager.bindResource(getDataSource(),ch);
296
297 } catch (IllegalStateException e) {
298 // TODO Auto-generated catch block
299 e.printStackTrace();
300 } catch (SQLException e) {
301 // TODO Auto-generated catch block
302 e.printStackTrace();
303 }
304 }
305
306 // commit the changes
307 transactionManager.commit(transactionStatus);
308 logger.info("Committing Session: " + getSessionHolder());
309 // propagate transaction end
310 CdmPostDataChangeObservableListener.getDefault().delayedNotify();
311
312 // Reset the transactionStatus.
313 transactionStatus = null;
314
315 // Committing a transaction frees all resources.
316 // Since we are in a conversation we directly rebind those resources and start a new transaction
317 bind();
318 if(restartTransaction){
319 return startTransaction();
320 }
321 }else{
322 logger.warn("No active transaction but commit was called");
323 }
324 return null;
325 }
326
327 /**
328 * @return the session associated with this conversation manager
329 */
330 public Session getSession() {
331 if(longSession == null){
332 longSession = getNewSession();
333 }
334 return longSession;
335 }
336
337 /**
338 * @return a new session to be managed by this conversation
339 */
340 private Session getNewSession() {
341
342 // Interesting: http://stackoverflow.com/questions/3526556/session-connection-deprecated-on-hibernate
343 // Also, http://blog-it.hypoport.de/2012/05/10/hibernate-4-migration/
344
345 // This will create a new session which must be explicitly managed by this conversation, which includes
346 // binding / unbinding / closing session as well as starting / committing transactions.
347 Session session = sessionFactory.openSession();
348 session.setFlushMode(FlushMode.COMMIT);
349 logger.info("Creating Session: [" + longSession + "]");
350 return session;
351 }
352
353
354
355
356 /**
357 * @return the session factory that is bound to this conversation manager
358 */
359 public SessionFactory getSessionFactory() {
360 return sessionFactory;
361 }
362
363 public void delete(Object object){
364 this.getSession().delete(object);
365 }
366
367 /**
368 * Facades Session.lock()
369 */
370 public void lock(Object persistentObject, LockMode lockMode) {
371 getSession().lock(persistentObject, lockMode);
372 }
373
374 public void lock(String entityName, Object persistentObject, LockMode lockMode){
375 getSession().lock(entityName, persistentObject, lockMode);
376 }
377
378 /**
379 * @return the definition
380 */
381 public TransactionDefinition getDefinition() {
382 return definition;
383 }
384
385 /**
386 * @param definition the definition to set
387 */
388 public void setDefinition(TransactionDefinition definition) {
389 this.definition = definition;
390 }
391
392 /**
393 * Register to get updated after any interaction with the datastore
394 */
395 public void registerForDataStoreChanges(IConversationEnabled observer) {
396 CdmPostDataChangeObservableListener.getDefault().register(observer);
397 }
398
399 /**
400 * Register to get updated after any interaction with the datastore
401 */
402 public void unregisterForDataStoreChanges(IConversationEnabled observer) {
403 CdmPostDataChangeObservableListener.getDefault().unregister(observer);
404 }
405
406 /**
407 * Free resources bound to this conversationHolder
408 */
409 public void close(){
410 if(getSession().isOpen()) {
411 getSession().close();
412 unbind();
413 }
414 longSession = null;
415 sessionHolder = null;
416 closed = true;
417 }
418
419 public boolean isClosed(){
420 return closed;
421 }
422
423 public boolean isCompleted(){
424 return transactionStatus == null || transactionStatus.isCompleted();
425 }
426
427
428 }