Project

General

Profile

Download (17 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
 * @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
}
(1-1/3)