Project

General

Profile

Download (17.2 KB) Statistics
| Branch: | Tag: | Revision:
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

    
366
        String whatStr;
367

    
368
        if(longSession == null){
369
            longSession = getNewSession();
370
            whatStr = "Creating";
371
        } else {
372
            whatStr = "Reusing";
373
        }
374
        if(logger.isDebugEnabled()){
375
            logger.debug(whatStr + " Session: [" + longSession.hashCode() + "] " + longSession);
376
        } else {
377
            logger.info(whatStr + " Session: [" + longSession.hashCode() + "] ");
378
        }
379
        return longSession;
380
    }
381

    
382
    /**
383
     * @return a new session to be managed by this conversation
384
     */
385
    private Session getNewSession() {
386

    
387
        // Interesting: http://stackoverflow.com/questions/3526556/session-connection-deprecated-on-hibernate
388
        // Also, http://blog-it.hypoport.de/2012/05/10/hibernate-4-migration/
389

    
390
        // This will create a new session which must be explicitly managed by this conversation, which includes
391
        // binding / unbinding / closing session as well as starting / committing transactions.
392
        Session session = sessionFactory.openSession();
393
        session.setFlushMode(getDefaultFlushMode());
394

    
395
        return session;
396
    }
397

    
398

    
399

    
400

    
401
    /**
402
     * @return the session factory that is bound to this conversation manager
403
     */
404
    public SessionFactory getSessionFactory() {
405
        return sessionFactory;
406
    }
407

    
408
    public void delete(Object object){
409
        this.getSession().delete(object);
410
    }
411

    
412
    /**
413
     * Facades Session.lock()
414
     */
415
    public void lock(Object persistentObject, LockMode lockMode) {
416
        getSession().lock(persistentObject, lockMode);
417
    }
418

    
419
    public void lock(String entityName, Object persistentObject, LockMode lockMode){
420
        getSession().lock(entityName, persistentObject, lockMode);
421
    }
422

    
423
    /**
424
     * @return the definition
425
     */
426
    public TransactionDefinition getDefinition() {
427
        return definition;
428
    }
429

    
430
    /**
431
     * @param definition the definition to set
432
     */
433
    public void setDefinition(TransactionDefinition definition) {
434
        this.definition = definition;
435
    }
436

    
437
    /**
438
     * Register to get updated after any interaction with the datastore
439
     */
440
    public void registerForDataStoreChanges(IConversationEnabled observer) {
441
        CdmPostDataChangeObservableListener.getDefault().register(observer);
442
    }
443

    
444
    /**
445
     * Register to get updated after any interaction with the datastore
446
     */
447
    public void unregisterForDataStoreChanges(IConversationEnabled observer) {
448
        CdmPostDataChangeObservableListener.getDefault().unregister(observer);
449
    }
450

    
451
    /**
452
     * Free resources bound to this conversationHolder
453
     */
454
    public void close(){
455
        if(getSession().isOpen()) {
456
            getSession().close();
457
            unbind();
458
        }
459
        longSession = null;
460
        sessionHolder = null;
461
        closed = true;
462
    }
463

    
464
    public boolean isClosed(){
465
        return closed;
466
    }
467

    
468
    public boolean isCompleted(){
469
        return transactionStatus == null || transactionStatus.isCompleted();
470
    }
471

    
472
    /**
473
     * @return the defaultFlushMode
474
     */
475
    public FlushMode getDefaultFlushMode() {
476
        return defaultFlushMode;
477
    }
478

    
479
    /**
480
     * @param defaultFlushMode the defaultFlushMode to set
481
     */
482
    public void setDefaultFlushMode(FlushMode defaultFlushMode) {
483
        this.defaultFlushMode = defaultFlushMode;
484
    }
485

    
486

    
487
}
(1-1/3)