/*
 * Copyright 2004-2009 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.compass.gps.device.hibernate;

import javax.transaction.Status;
import javax.transaction.Synchronization;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.compass.core.CompassException;
import org.compass.core.CompassSession;
import org.compass.core.spi.InternalCompassSession;
import org.compass.core.transaction.AbstractTransaction;
import org.compass.core.transaction.TransactionException;
import org.compass.core.transaction.TransactionFactory;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.engine.SessionImplementor;

/**
 * @author kimchy
 */
public class HibernateSyncTransaction extends AbstractTransaction {

    private static final Log log = LogFactory.getLog(HibernateSyncTransaction.class);

    private SessionFactory sessionFactory;

    /**
     * Did we start the Hibernate transaction
     */
    private boolean newTransaction;

    /**
     * Is this the up most level controlling the Compass transaction
     */
    private boolean controllingNewTransaction = false;

    private InternalCompassSession session;

    private boolean commitFailed;

    private boolean commitBeforeCompletion;

    private Transaction transaction;

    public HibernateSyncTransaction(SessionFactory sessionFactory, boolean commitBeforeCompletion, TransactionFactory transactionFactory) {
        super(transactionFactory);
        this.sessionFactory = sessionFactory;
        this.commitBeforeCompletion = commitBeforeCompletion;
    }

    public void begin(InternalCompassSession session) throws CompassException {
        this.session = session;
        try {
            controllingNewTransaction = true;
            newTransaction = !((SessionImplementor) sessionFactory.getCurrentSession()).isTransactionInProgress();
            transaction = sessionFactory.getCurrentSession().getTransaction();
            if (newTransaction) {
                if (log.isDebugEnabled()) {
                    log.debug("Beginning new Hibernate transaction, and a new compass transaction on thread ["
                            + Thread.currentThread().getName() + "]");
                }
                session.getSearchEngine().begin();
                transaction.begin();
            } else {
                // joining an exisiting transaction
                session.getSearchEngine().begin();
                if (log.isDebugEnabled()) {
                    log.debug("Joining an existing Hibernate transaction, starting a new compass transaction on thread ["
                            + Thread.currentThread().getName() + "]");
                }
            }
            transaction.registerSynchronization(new HibernateTransactionSynchronization(session, transaction, newTransaction, commitBeforeCompletion, transactionFactory));
        } catch (Exception e) {
            throw new TransactionException("Begin failed with exception", e);
        }
        setBegun(true);
    }

    /**
     * Called by the factory when joining an already running compass transaction
     */
    public void join(InternalCompassSession session) throws CompassException {
        this.session = session;
        controllingNewTransaction = false;
        if (log.isDebugEnabled()) {
            log.debug("Joining an existing compass transcation on thread [" + Thread.currentThread().getName() + "]");
        }
    }

    protected void doCommit() throws CompassException {

        if (!controllingNewTransaction) {
            if (log.isDebugEnabled()) {
                log.debug("Not committing Hibernate transaction since compass does not control it on thread ["
                        + Thread.currentThread().getName() + "]");
            }
            return;
        }

        if (newTransaction) {
            if (log.isDebugEnabled()) {
                log.debug("Committing Hibernate transaction controlled by compass on thread ["
                        + Thread.currentThread().getName() + "]");
            }
            try {
                transaction.commit();
            } catch (Exception e) {
                commitFailed = true;
                // so the transaction is already rolled back, by Hibernate spec
                throw new TransactionException("Commit failed", e);
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Commit called, let Hibernate synchronization commit the transaciton on thread ["
                        + Thread.currentThread().getName() + "]");
            }
        }
    }

    protected void doRollback() throws CompassException {

        try {
            if (newTransaction) {
                if (log.isDebugEnabled()) {
                    log.debug("Rolling back Hibernate transaction controlled by compass on thread [" + Thread.currentThread().getName() + "]");
                }
                if (!commitFailed)
                    transaction.rollback();
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Marking Hibernate transaction as rolled back since compass controlls it on thread [" +
                            Thread.currentThread().getName() + "]");
                }
                // no way to mark a transaction as rolled back, assume throwing the exception will make it
                // transaction.setRollbackOnly();
            }
        } catch (Exception e) {
            throw new TransactionException("Rollback failed with exception", e);
        }
    }

    public boolean wasRolledBack() throws TransactionException {

        if (!isBegun())
            return false;
        if (commitFailed)
            return true;

        return transaction.wasRolledBack();
    }

    public boolean wasCommitted() throws TransactionException {

        if (!isBegun() || commitFailed)
            return false;

        return transaction.wasCommitted();
    }

    public CompassSession getSession() {
        return this.session;
    }

    private static class HibernateTransactionSynchronization implements Synchronization {

        private static final Log log = LogFactory.getLog(HibernateTransactionSynchronization.class);

        private InternalCompassSession session;

        private Transaction tx;

        private boolean compassControlledHibernateTransaction;

        private boolean commitBeforeCompletion;

        private TransactionFactory transactionFactory;

        public HibernateTransactionSynchronization(InternalCompassSession session, Transaction tx,
                                                   boolean compassControlledHibernateTransaction, boolean commitBeforeCompletion,
                                                   TransactionFactory transactionFactory) {
            this.transactionFactory = transactionFactory;
            this.session = session;
            this.tx = tx;
            this.compassControlledHibernateTransaction = compassControlledHibernateTransaction;
            this.commitBeforeCompletion = commitBeforeCompletion;
        }

        public void beforeCompletion() {
            if (!commitBeforeCompletion) {
                return;
            }
            if (log.isDebugEnabled()) {
                log.debug("Committing compass transaction using Hibernate synchronization beforeCompletion on thread [" +
                        Thread.currentThread().getName() + "]");
            }
            session.getSearchEngine().commit(true);
        }

        public void afterCompletion(int status) {
            try {
                if (!commitBeforeCompletion) {
                    if (status == Status.STATUS_COMMITTED) {
                        if (log.isDebugEnabled()) {
                            log.debug("Committing compass transaction using Hibernate synchronization afterCompletion on thread [" +
                                    Thread.currentThread().getName() + "]");
                        }
                        session.getSearchEngine().commit(true);
                    } else {
                        if (log.isDebugEnabled()) {
                            log.debug("Rolling back compass transaction using Hibernate synchronization afterCompletion on thread [" +
                                    Thread.currentThread().getName() + "]");
                        }
                        session.getSearchEngine().rollback();
                    }
                }
            } catch (Exception e) {
                // TODO swallow??????
                log.error("Exception occured when sync with transaction", e);
            } finally {
                session.evictAll();
                ((HibernateSyncTransactionFactory) transactionFactory).unbindSessionFromTransaction(tx, session);
                // close the session AFTER we cleared it from the transaction,
                // so it will be actually closed (and only if we are not
                // controlling the trnasction)
                if (!compassControlledHibernateTransaction) {
                    session.close();
                }
            }
        }
    }

}
@@ -1119,32 +1119,64 @@
 sactionFactory;%0A
+import org.hibernate.FlushMode;%0A
 import org.hiber
@@ -2386,26 +2386,46 @@
     
-newTransact
+SessionImplementor hibernateSess
 ion = 
-!
 ((Se
@@ -2477,16 +2477,64 @@
 ssion())
+;%0A            newTransaction = !hibernateSession
 .isTrans
@@ -2953,32 +2953,163 @@
 gine().begin();%0A
+                if (session.isReadOnly()) %7B%0A                    hibernateSession.setFlushMode(FlushMode.MANUAL);%0A                %7D%0A
                 

make read only transactions with Hibernate configured with flush mode manual