#5067 Updates for remoting derivate hierachy views
[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.hibernate4.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 * - (Autowired) {@link org.hibernate.SessionFactory} mapped to the {@link org.springframework.orm.hibernate4.SessionHolder}
41 * - (Autowired) {@link javax.sql.DataSource} mapped to the {@link org.springframework.jdbc.datasource.ConnectionHolder}
42 * <p>
43 * The SessionHolder object itself contains the {@link org.hibernate.Session Session} as well as the {@link org.hibernate.Transaction object.
44 * The ConnectionHolder contains the (JDBC) {@link java.sql.Connection Connection} to the database. For every action to do with the
45 * transaction object it is required to have both entries present in the resources. Both the session as well as the connection
46 * objects must not be null and the corresponding holders must have their 'synchronizedWithTransaction' flag set to true.
47 * <p>
48 * The default behaviour of the {@link org.springframework.transaction.PlatformTransactionManager PlatformTransactionManager} which in the CDM case is autowired
49 * to {@link org.springframework.orm.hibernate4.HibernateTransactionManager HibernateTransactionManager}, is to check these entries
50 * when starting a transaction. If this entries do not exist in the resource map then they are created, implying a new session, which
51 * is in fact how hibernate implements the default 'session-per-request' pattern internally.
52 * <p>
53 * Given the above conditions, this class manages long running sessions by providing the following methods,
54 * - {@link #bind()} : binds the session owned by this conversation to the resource map
55 * - {@link #startTransaction()} : starts a transaction
56 * - {@link #commit()} : commits the current transaction, with the option of restarting a new transaction.
57 * - {@link #unbind()} : unbinds the session owned by this conversation from the resource map.
58 * - {@link #close()} : closes the session owned by this conversation
59 * <p>
60 * With the exception of {@link #unbind()} (which should be called explicitly), the above sequence must be strictly followed to
61 * maintain a consistent session state. Even though it is possible to interweave multiple conversations at the same time, for a
62 * specific conversation the above sequence must be followed.
63 *
64 * @see http://www.hibernate.org/42.html
65 *
66 * @author n.hoffmann,c.mathew
67 * @created 12.03.2009
68 * @version 1.0
69 */
70 public class ConversationHolder {
71
72 private static final Logger logger = Logger.getLogger(ConversationHolder.class);
73
74 @Autowired
75 private SessionFactory sessionFactory;
76
77 @Autowired
78 private DataSource dataSource;
79
80 @Autowired
81 private PlatformTransactionManager transactionManager;
82
83
84 /**
85 * The persistence context for this conversation
86 */
87 private Session longSession = null;
88
89 /**
90 * Spring communicates with hibernate sessions via a SessionHolder object
91 */
92 private SessionHolder sessionHolder = null;
93
94 /**
95 * @see TransactionDefinition
96 */
97 private TransactionDefinition definition;
98
99 /**
100 * This conversations transaction
101 */
102 private TransactionStatus transactionStatus;
103
104
105 private boolean closed = false;
106
107 /**
108 * Simple constructor used by Spring only
109 */
110 protected ConversationHolder(){
111 closed = false;
112 }
113
114 public ConversationHolder(DataSource dataSource, SessionFactory sessionFactory,
115 PlatformTransactionManager transactionManager) {
116 this();
117 this.dataSource = dataSource;
118 this.sessionFactory = sessionFactory;
119 this.transactionManager = transactionManager;
120
121 bind();
122
123 if(TransactionSynchronizationManager.hasResource(getDataSource())){
124 TransactionSynchronizationManager.unbindResource(getDataSource());
125 }
126
127 }
128
129 /**
130 * This method has to be called when starting a new unit-of-work. All required resources are
131 * bound so that SessionFactory.getCurrentSession() returns the right session for this conversation
132 */
133 public void bind() {
134
135 logger.info("Binding resources for ConversationHolder");
136
137 if(TransactionSynchronizationManager.isSynchronizationActive()){
138 TransactionSynchronizationManager.clearSynchronization();
139 }
140
141 try{
142
143 logger.info("Starting new Synchronization in TransactionSynchronizationManager");
144 TransactionSynchronizationManager.initSynchronization();
145
146
147 if(TransactionSynchronizationManager.hasResource(getSessionFactory())){
148 TransactionSynchronizationManager.unbindResource(getSessionFactory());
149 }
150
151 logger.info("Binding Session to TransactionSynchronizationManager: Session: " + getSessionHolder());
152 TransactionSynchronizationManager.bindResource(getSessionFactory(), getSessionHolder());
153
154
155
156 } catch(Exception e){
157 logger.error("Error binding resources for session", e);
158 }
159
160 }
161
162 /**
163 * This method has to be called when suspending the current unit of work. The conversation can be later bound again.
164 */
165 public void unbind() {
166
167 logger.info("Unbinding resources for ConversationHolder");
168
169 if(TransactionSynchronizationManager.isSynchronizationActive()){
170 TransactionSynchronizationManager.clearSynchronization();
171 }
172
173
174 if(isBound()) {
175 // unbind the current session.
176 // there is no need to bind a new session, since HibernateTransactionManager will create a new one
177 // if the resource map does not contain one (ditto for the datasource-to-connection entry).
178 TransactionSynchronizationManager.unbindResource(getSessionFactory());
179 if(TransactionSynchronizationManager.hasResource(getDataSource())){
180 TransactionSynchronizationManager.unbindResource(getDataSource());
181 }
182 }
183 }
184
185 public SessionHolder getSessionHolder(){
186 if(this.sessionHolder == null){
187 logger.info("Creating new SessionHolder");
188 this.sessionHolder = new SessionHolder(getSession());
189 }
190 return this.sessionHolder;
191 }
192
193 /**
194 * @return
195 */
196 private DataSource getDataSource() {
197 return this.dataSource;
198 }
199
200 /**
201 * @return true if this longSession is bound to the session factory.
202 */
203 public boolean isBound(){
204 //return sessionHolder != null && longSession != null && longSession.isConnected();
205 SessionHolder currentSessionHolder = (SessionHolder)TransactionSynchronizationManager.getResource(getSessionFactory());
206 return longSession != null && currentSessionHolder != null && getSessionFactory().getCurrentSession() == longSession;
207 }
208
209 /**
210 * Creates an instance of TransactionStatus and binds it to this conversation manager.
211 * At the moment we allow only one transaction per conversation holder.
212 *
213 * @return the transaction status bound to this conversation holder
214 */
215 public TransactionStatus startTransaction(){
216 if (isTransactionActive()){
217 logger.warn("We allow only one transaction at the moment but startTransaction " +
218 "was called a second time.\nReturning the transaction already associated with this " +
219 "ConversationManager");
220 }else{
221 //always safe to remove the datasource-to-connection entry since we
222 // know that HibernateTransactionManager will create a new one
223 if(TransactionSynchronizationManager.hasResource(getDataSource())){
224 TransactionSynchronizationManager.unbindResource(getDataSource());
225 }
226
227 transactionStatus = transactionManager.getTransaction(definition);
228
229 logger.info("Transaction started: " + transactionStatus);
230 }
231 return transactionStatus;
232 }
233
234 /**
235 * @return if there is a running transaction
236 */
237 public boolean isTransactionActive(){
238 return transactionStatus != null;
239 }
240
241 /* (non-Javadoc)
242 * @see org.hibernate.Session#evict(java.lang.Object object)
243 */
244 public void evict(Object object){
245 getSession().evict(object);
246 }
247
248 /* (non-Javadoc)
249 * @see org.hibernate.Session#refresh(java.lang.Object object)
250 */
251 public void refresh(Object object){
252 getSession().refresh(object);
253 }
254
255 /* (non-Javadoc)
256 * @see org.hibernate.Session#clear()
257 */
258 public void clear(){
259 getSession().clear();
260 }
261
262 /**
263 * Commit the running transaction.
264 */
265 public void commit(){
266 commit(true);
267 }
268
269 /**
270 * Commit the running transaction but optionally start a
271 * new one right away.
272 *
273 * @param restartTransaction whether to start a new transaction
274 */
275 public TransactionStatus commit(boolean restartTransaction){
276 if(isTransactionActive()){
277
278 if(getSessionHolder().isRollbackOnly()){
279 logger.error("Commiting this session will not work. It has been marked as rollback only.");
280 }
281 // if a datasource-to-connection entry already exists in the resource map
282 // then its setSynchronizedWithTransaction should be true, since hibernate has added
283 // this entry.
284 // if the datasource-to-connection entry does not exist then we need to create one
285 // and explicitly setSynchronizedWithTransaction to true.
286 TransactionSynchronizationManager.getResource(getDataSource());
287 if(!TransactionSynchronizationManager.hasResource(getDataSource())){
288 try {
289 ConnectionHolder ch = new ConnectionHolder(getDataSource().getConnection());
290 ch.setSynchronizedWithTransaction(true);
291 TransactionSynchronizationManager.bindResource(getDataSource(),ch);
292
293 } catch (IllegalStateException e) {
294 // TODO Auto-generated catch block
295 e.printStackTrace();
296 } catch (SQLException e) {
297 // TODO Auto-generated catch block
298 e.printStackTrace();
299 }
300 }
301
302 // commit the changes
303 transactionManager.commit(transactionStatus);
304 logger.info("Committing Session: " + getSessionHolder());
305 // propagate transaction end
306 CdmPostDataChangeObservableListener.getDefault().delayedNotify();
307
308 // Reset the transactionStatus.
309 transactionStatus = null;
310
311 // Committing a transaction frees all resources.
312 // Since we are in a conversation we directly rebind those resources and start a new transaction
313 bind();
314 if(restartTransaction){
315 return startTransaction();
316 }
317 }else{
318 logger.warn("No active transaction but commit was called");
319 }
320 return null;
321 }
322
323 /**
324 * @return the session associated with this conversation manager
325 */
326 public Session getSession() {
327 if(longSession == null){
328 longSession = getNewSession();
329 }
330 return longSession;
331 }
332
333 /**
334 * @return a new session to be managed by this conversation
335 */
336 private Session getNewSession() {
337
338 // Interesting: http://stackoverflow.com/questions/3526556/session-connection-deprecated-on-hibernate
339 // Also, http://blog-it.hypoport.de/2012/05/10/hibernate-4-migration/
340
341 // This will create a new session which must be explicitly managed by this conversation, which includes
342 // binding / unbinding / closing session as well as starting / committing transactions.
343 Session session = sessionFactory.openSession();
344 session.setFlushMode(FlushMode.COMMIT);
345 logger.info("Creating Session: [" + longSession + "]");
346 return session;
347 }
348
349
350
351
352 /**
353 * @return the session factory that is bound to this conversation manager
354 */
355 public SessionFactory getSessionFactory() {
356 return sessionFactory;
357 }
358
359 public void delete(Object object){
360 this.getSession().delete(object);
361 }
362
363 /**
364 * Facades Session.lock()
365 */
366 public void lock(Object persistentObject, LockMode lockMode) {
367 getSession().lock(persistentObject, lockMode);
368 }
369
370 public void lock(String entityName, Object persistentObject, LockMode lockMode){
371 getSession().lock(entityName, persistentObject, lockMode);
372 }
373
374 /**
375 * @return the definition
376 */
377 public TransactionDefinition getDefinition() {
378 return definition;
379 }
380
381 /**
382 * @param definition the definition to set
383 */
384 public void setDefinition(TransactionDefinition definition) {
385 this.definition = definition;
386 }
387
388 /**
389 * Register to get updated after any interaction with the datastore
390 */
391 public void registerForDataStoreChanges(IConversationEnabled observer) {
392 CdmPostDataChangeObservableListener.getDefault().register(observer);
393 }
394
395 /**
396 * Register to get updated after any interaction with the datastore
397 */
398 public void unregisterForDataStoreChanges(IConversationEnabled observer) {
399 CdmPostDataChangeObservableListener.getDefault().unregister(observer);
400 }
401
402 /**
403 * Free resources bound to this conversationHolder
404 */
405 public void close(){
406 if(getSession().isOpen()) {
407 getSession().close();
408 unbind();
409 }
410 longSession = null;
411 sessionHolder = null;
412 closed = true;
413 }
414
415 public boolean isClosed(){
416 return closed;
417 }
418
419 public boolean isCompleted(){
420 return transactionStatus == null || transactionStatus.isCompleted();
421 }
422
423
424 }