/**
 * This file is part of mycollab-jackrabbit.
 *
 * mycollab-jackrabbit is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * mycollab-jackrabbit is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with mycollab-jackrabbit.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.springframework.extensions.jcr;

import java.io.IOException;

import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.InvalidSerializedDataException;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.LoginException;
import javax.jcr.MergeException;
import javax.jcr.NamespaceException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.PathNotFoundException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.version.VersionException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

/**
 * FactoryBean for instantiating a Java Content Repository. This abstract class adds custom functionality
 * subclasses handling only the configuration issues.
 * @author Costin Leau
 * @author Sergio Bossa
 * @author Salvatore Incandela
 */
public abstract class SessionFactoryUtils {

    private static final Logger LOG = LoggerFactory.getLogger(SessionFactoryUtils.class);

    /**
     * Get a JCR Session for the given Repository. Is aware of and will return any existing corresponding
     * Session bound to the current thread, for example when using JcrTransactionManager. Same as
     * <code>getSession</code> but throws the original Repository.
     * @param sessionFactory Jcr Repository to create session with
     * @param allowCreate if a non-transactional Session should be created when no transactional Session can
     *            be found for the current thread
     * @throws RepositoryException
     * @return
     */
    public static Session doGetSession(SessionFactory sessionFactory, boolean allowCreate) throws RepositoryException {
        Assert.notNull(sessionFactory, "No sessionFactory specified");

        // check if there is any transaction going on
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        if (sessionHolder != null && sessionHolder.getSession() != null)
            return sessionHolder.getSession();

        if (!allowCreate && !TransactionSynchronizationManager.isSynchronizationActive()) {
            throw new IllegalStateException("No session bound to thread, " + "and configuration does not allow creation of non-transactional one here");
        }

        LOG.debug("Opening JCR Session");
        Session session = sessionFactory.getSession();

        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            LOG.debug("Registering transaction synchronization for JCR session");
            // Use same session for further JCR actions within the transaction
            // thread object will get removed by synchronization at transaction
            // completion.
            sessionHolder = sessionFactory.getSessionHolder(session);
            sessionHolder.setSynchronizedWithTransaction(true);
            TransactionSynchronizationManager.registerSynchronization(new JcrSessionSynchronization(sessionHolder, sessionFactory));
            TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
        }

        return session;
    }

    /**
     * Get a JCR Session for the given Repository. Is aware of and will return any existing corresponding
     * Session bound to the current thread, for example when using JcrTransactionManager. Will create a new
     * Session otherwise, if allowCreate is true. This is the getSession method used by typical data access
     * code, in combination with releaseSession called when done with the Session. Note that JcrTemplate
     * allows to write data access code without caring about such resource handling. Supports synchronization
     * with both Spring-managed JTA transactions (i.e. JtaTransactionManager) and non-Spring JTA transactions
     * (i.e. plain JTA or EJB CMT).
     * @param sessionFactory JCR Repository to create session with
     * @param allowCreate if a non-transactional Session should be created when no transactional Session can
     *            be found for the current thread
     * @throws DataAccessException
     * @return
     */
    public static Session getSession(SessionFactory sessionFactory, boolean allowCreate) throws DataAccessException {
        try {
            return doGetSession(sessionFactory, allowCreate);
        } catch (RepositoryException ex) {
            throw new DataAccessResourceFailureException("Could not open Jcr Session", ex);
        }
    }

    /**
     * Return whether the given JCR Session is thread-bound that is, bound to the current thread by Spring's
     * transaction facilities (which is used as a thread-bounding utility class).
     * @param session the JCR Session to check
     * @param sessionFactory the JCR SessionFactory that the Session was created with (can be null)
     * @return whether the Session is transactional
     */
    public static boolean isSessionThreadBound(Session session, SessionFactory sessionFactory) {
        if (sessionFactory == null) {
            return false;
        }
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
        return (sessionHolder != null && session == sessionHolder.getSession());
    }

    /**
     * Close the given Session, created via the given repository, if it is not managed externally (i.e. not
     * bound to the thread).
     * @param session the Jcr Session to close
     * @param sessionFactory JcrSessionFactory that the Session was created with (can be null)
     */
    public static void releaseSession(Session session, SessionFactory sessionFactory) {
        if (session == null) {
            return;
        }
        // Only close non thread bound Sessions.
        if (!isSessionThreadBound(session, sessionFactory)) {
            LOG.debug("Closing JCR Session");
            session.logout();
        }
    }

    /**
     * Jcr exception translator - it converts specific JSR-170 checked exceptions into unchecked Spring DA
     * exception.
     * @author Guillaume Bort <[email protected]>
     * @author Costin Leau
     * @author Sergio Bossa
     * @author Salvatore Incandela
     * @param ex
     * @return
     */
    public static DataAccessException translateException(RepositoryException ex) {
        if (ex instanceof AccessDeniedException) {
            return new DataRetrievalFailureException("Access denied to this data", ex);
        }
        if (ex instanceof ConstraintViolationException) {
            return new DataIntegrityViolationException("Constraint has been violated", ex);
        }
        if (ex instanceof InvalidItemStateException) {
            return new ConcurrencyFailureException("Invalid item state", ex);
        }
        if (ex instanceof InvalidQueryException) {
            return new DataRetrievalFailureException("Invalid query", ex);
        }
        if (ex instanceof InvalidSerializedDataException) {
            return new DataRetrievalFailureException("Invalid serialized data", ex);
        }
        if (ex instanceof ItemExistsException) {
            return new DataIntegrityViolationException("An item already exists", ex);
        }
        if (ex instanceof ItemNotFoundException) {
            return new DataRetrievalFailureException("Item not found", ex);
        }
        if (ex instanceof LoginException) {
            return new DataAccessResourceFailureException("Bad login", ex);
        }
        if (ex instanceof LockException) {
            return new ConcurrencyFailureException("Item is locked", ex);
        }
        if (ex instanceof MergeException) {
            return new DataIntegrityViolationException("Merge failed", ex);
        }
        if (ex instanceof NamespaceException) {
            return new InvalidDataAccessApiUsageException("Namespace not registred", ex);
        }
        if (ex instanceof NoSuchNodeTypeException) {
            return new InvalidDataAccessApiUsageException("No such node type", ex);
        }
        if (ex instanceof NoSuchWorkspaceException) {
            return new DataAccessResourceFailureException("Workspace not found", ex);
        }
        if (ex instanceof PathNotFoundException) {
            return new DataRetrievalFailureException("Path not found", ex);
        }
        if (ex instanceof ReferentialIntegrityException) {
            return new DataIntegrityViolationException("Referential integrity violated", ex);
        }
        if (ex instanceof UnsupportedRepositoryOperationException) {
            return new InvalidDataAccessApiUsageException("Unsupported operation", ex);
        }
        if (ex instanceof ValueFormatException) {
            return new InvalidDataAccessApiUsageException("Incorrect value format", ex);
        }
        if (ex instanceof VersionException) {
            return new DataIntegrityViolationException("Invalid version graph operation", ex);
        }
        // fallback
        return new JcrSystemException(ex);
    }

    /**
     * Jcr exception translator - it converts specific JSR-170 checked exceptions into unchecked Spring DA
     * exception.
     * @param ex
     * @return
     */
    public static DataAccessException translateException(IOException ex) {
        return new DataAccessResourceFailureException("I/O failure", ex);
    }

    /**
     * Callback for resource cleanup at the end of a non-JCR transaction (e.g. when participating in a
     * JtaTransactionManager transaction).
     * @see org.springframework.transaction.jta.JtaTransactionManager
     */
    private static class JcrSessionSynchronization extends TransactionSynchronizationAdapter {

        private final SessionHolder sessionHolder;

        private final SessionFactory sessionFactory;

        private boolean holderActive = true;

        /**
         * @param sessionFactory
         * @param holder
         */
        public JcrSessionSynchronization(SessionHolder holder, SessionFactory sessionFactory) {
            this.sessionFactory = sessionFactory;
            sessionHolder = holder;
        }

        public void suspend() {
            if (this.holderActive) {
                TransactionSynchronizationManager.unbindResource(this.sessionFactory);
            }
        }

        public void resume() {
            if (this.holderActive) {
                TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder);
            }
        }

        public void beforeCompletion() {
            TransactionSynchronizationManager.unbindResource(this.sessionFactory);
            this.holderActive = false;
            releaseSession(this.sessionHolder.getSession(), this.sessionFactory);
        }
    }

}