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