Project

General

Profile

Download (15 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
 *  - (Autowired) {@link org.hibernate.SessionFactory} mapped to the {@link org.springframework.orm.hibernate5.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.hibernate5.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
}
(1-1/3)