org.springframework.data.neo4j.transaction.Neo4jTransactionManager.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.neo4j.transaction.Neo4jTransactionManager.java

Source

/*
 * Copyright 2011-2019 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
 *
 *      https://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.springframework.data.neo4j.transaction;

import static java.util.Collections.*;

import java.util.Collection;

import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.neo4j.ogm.transaction.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.dao.DataAccessException;
import org.springframework.data.neo4j.bookmark.BookmarkInfo;
import org.springframework.data.neo4j.bookmark.BookmarkManager;
import org.springframework.data.neo4j.bookmark.BookmarkSupport;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.InvalidIsolationLevelException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.ResourceTransactionManager;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * {@link PlatformTransactionManager} implementation for a single Neo4j OGM {@link SessionFactory}. Binds a Neo4j OGM
 * Session from the specified factory to the thread, potentially allowing for one thread-bound Session per factory.
 * {@link SharedSessionCreator} is aware of thread-bound session and participates in such transactions automatically. It
 * is required for Neo4j OGM access code supporting this transaction management mechanism.
 * <p>
 * This transaction manager is appropriate for applications that use a single Neo4j OGM SessionFactory for transactional
 * data access. JTA (usually through {@link org.springframework.transaction.jta.JtaTransactionManager}) has not been
 * tested or considered at the moment.
 * <p>
 * This transaction manager does not support nested transactions or requires new propagation.
 *
 * @author Mark Angrish
 * @see #setSessionFactory
 */
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManager, BeanFactoryAware, InitializingBean {

    private static final Logger logger = LoggerFactory.getLogger(Neo4jTransactionManager.class);

    private SessionFactory sessionFactory;
    private BookmarkManager bookmarkManager;

    /**
     * Create a new Neo4jTransactionManager instance.
     * <p>
     * An SessionFactory has to be set to be able to use it.
     *
     * @see #setSessionFactory(SessionFactory)
     */
    public Neo4jTransactionManager() {
        this(null);
    }

    /**
     * Create a new Neo4jTransactionManager instance.
     *
     * @param sessionFactory SessionFactory to manage transactions for
     */
    public Neo4jTransactionManager(SessionFactory sessionFactory) {

        setTransactionSynchronization(SYNCHRONIZATION_ON_ACTUAL_TRANSACTION);
        this.sessionFactory = sessionFactory;
    }

    /**
     * Set the SessionFactory that this instance should manage transactions for.
     * <p>
     * By default, a default SessionFactory will be retrieved by finding a single unique bean of type SessionFactory in
     * the containing BeanFactory.
     */
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    /**
     * Return the SessionFactory that this instance should manage transactions for.
     */
    public SessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    /**
     * Retrieves a default SessionFactory bean.
     */
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (getSessionFactory() == null) {
            setSessionFactory(beanFactory.getBean(SessionFactory.class));
        }
        try {
            bookmarkManager = beanFactory.getBean(BookmarkManager.class);
            logger.debug("Found BookmarkManager {}", bookmarkManager);
        } catch (NoSuchBeanDefinitionException e) {
            logger.debug("No BookmarkManager bean found, bookmark management not enabled");
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (getSessionFactory() == null) {
            throw new IllegalArgumentException("'sessionFactory' is required");
        }
    }

    @Override
    public Object getResourceFactory() {
        return getSessionFactory();
    }

    @Override
    protected Object doGetTransaction() {
        Neo4jTransactionObject txObject = new Neo4jTransactionObject();

        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
                .getResource(getSessionFactory());
        if (sessionHolder != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Found thread-bound Session [" + sessionHolder.getSession()
                        + "] for Neo4j OGM transaction");
            }
            txObject.setSessionHolder(sessionHolder, false);
        }
        return txObject;
    }

    @Override
    protected boolean isExistingTransaction(Object transaction) {
        return ((Neo4jTransactionObject) transaction).hasTransaction();
    }

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
        Neo4jTransactionObject txObject = (Neo4jTransactionObject) transaction;

        try {
            if (txObject.getSessionHolder() == null
                    || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
                Session session = sessionFactory.openSession();
                if (logger.isDebugEnabled()) {
                    logger.debug("Opened new Session [" + session + "] for Neo4j OGM transaction");
                }
                txObject.setSessionHolder(new SessionHolder(session), true);
            }

            Session session = txObject.getSessionHolder().getSession();

            if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
                // We should set a specific isolation level but are not allowed to...
                throw new InvalidIsolationLevelException(
                        "Neo4jTransactionManager is not allowed to support custom isolation levels.");
            }

            if (definition.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRED) {
                throw new IllegalTransactionStateException(
                        "Neo4jTransactionManager only supports 'required' propagation.");
            }

            Transaction.Type type = getTransactionType(definition, txObject);
            Transaction transactionData = session.beginTransaction(type, getBookmarks());

            txObject.setTransactionData(transactionData);
            if (logger.isDebugEnabled()) {
                logger.debug("Beginning Transaction [" + transactionData + "] on Session [" + session + "]");
            }

            // Bind the session holder to the thread.
            if (txObject.isNewSessionHolder()) {
                TransactionSynchronizationManager.bindResource(getSessionFactory(), txObject.getSessionHolder());
            }

            if (definition.isReadOnly()) {
                TransactionSynchronizationManager.setCurrentTransactionReadOnly(true);
            }

            txObject.getSessionHolder().setSynchronizedWithTransaction(true);
        } catch (TransactionException ex) {
            closeSessionAfterFailedBegin(txObject);
            throw ex;
        } catch (Throwable ex) {
            closeSessionAfterFailedBegin(txObject);
            throw new CannotCreateTransactionException("Could not open Neo4j Session for transaction", ex);
        }
    }

    private Transaction.Type getTransactionType(TransactionDefinition definition, Neo4jTransactionObject txObject) {
        Transaction.Type type;
        if (definition.isReadOnly() && txObject.isNewSessionHolder()) {
            type = Transaction.Type.READ_ONLY;
        } else if (txObject.transactionData != null) {
            type = txObject.transactionData.type();
        } else {
            type = Transaction.Type.READ_WRITE;
        }
        return type;
    }

    private Iterable<String> getBookmarks() {
        BookmarkInfo bookmarkInfo = BookmarkSupport.currentBookmarkInfo();
        if (bookmarkInfo != null && bookmarkInfo.shouldUseBookmark()) {
            if (bookmarkManager != null) {
                Collection<String> bookmarks = bookmarkManager.getBookmarks();
                bookmarkInfo.setBookmarks(bookmarks);
                return bookmarks;

            } else {
                throw new IllegalStateException("Configured @UseBookmark but no BookmarkManager bean found.");
            }
        } else {
            return emptySet();
        }
    }

    /**
     * Close the current transaction's Session. Called after a transaction begin attempt failed.
     *
     * @param txObject the current transaction
     */
    private void closeSessionAfterFailedBegin(Neo4jTransactionObject txObject) {
        if (txObject.isNewSessionHolder()) {
            Session session = txObject.getSessionHolder().getSession();
            try {
                if (session.getTransaction() != null) {
                    session.getTransaction().rollback();
                }
            } catch (Throwable ex) {
                logger.debug("Could not rollback Session after failed transaction begin", ex);
            } finally {
                txObject.setSessionHolder(null, false);
            }
        }
    }

    @Override
    protected Object doSuspend(Object transaction) {
        Neo4jTransactionObject txObject = (Neo4jTransactionObject) transaction;
        txObject.setSessionHolder(null, false);
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
                .unbindResource(getSessionFactory());
        return new SuspendedResourcesHolder(sessionHolder);
    }

    @Override
    protected void doResume(Object transaction, Object suspendedResources) {
        SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources;
        if (TransactionSynchronizationManager.hasResource(getSessionFactory())) {
            // From non-transactional code running in active transaction synchronization
            // -> can be safely removed, will be closed on transaction completion.
            TransactionSynchronizationManager.unbindResource(getSessionFactory());
        }
        TransactionSynchronizationManager.bindResource(getSessionFactory(), resourcesHolder.getSessionHolder());
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {

        Neo4jTransactionObject txObject = (Neo4jTransactionObject) status.getTransaction();
        Session session = txObject.getSessionHolder().getSession();

        try (Transaction tx = session.getTransaction()) {

            if (status.isDebug()) {
                logger.debug("Committing Neo4j OGM transaction [" + tx + "] on Session [" + session + "]");
            }

            if (tx != null) {
                tx.commit();

                if (bookmarkManager != null) {
                    String lastBookmark = session.getLastBookmark();

                    BookmarkInfo bookmarkInfo = BookmarkSupport.currentBookmarkInfo();
                    Collection<String> bookmarks;
                    if (bookmarkInfo != null && bookmarkInfo.getBookmarks() != null) {
                        bookmarks = bookmarkInfo.getBookmarks();
                    } else {
                        bookmarks = emptySet();
                    }
                    logger.debug("Found bookmarks {}, replacing with {}", bookmarks, lastBookmark);
                    bookmarkManager.storeBookmark(lastBookmark, bookmarks);
                }
            }
        } catch (RuntimeException ex) {
            DataAccessException dae = SessionFactoryUtils.convertOgmAccessException(ex);
            throw (dae != null ? dae : ex);
        }
    }

    @Override
    protected void doRollback(DefaultTransactionStatus status) {
        Neo4jTransactionObject txObject = (Neo4jTransactionObject) status.getTransaction();
        Session session = txObject.getSessionHolder().getSession();

        try (Transaction tx = session.getTransaction()) {

            if (status.isDebug()) {
                logger.debug("Rolling back Neo4j transaction [" + tx + "] on Session [" + session + "]");
            }

            if (tx != null) {
                tx.rollback();
            }
        } catch (RuntimeException ex) {
            DataAccessException dae = SessionFactoryUtils.convertOgmAccessException(ex);
            throw (dae != null ? dae : ex);
        } finally {
            if (!txObject.isNewSessionHolder()) {
                // Clear all pending inserts/updates/deletes in the Session.
                session.clear();
            }
        }
    }

    @Override
    protected void doSetRollbackOnly(DefaultTransactionStatus status) {
        Neo4jTransactionObject txObject = (Neo4jTransactionObject) status.getTransaction();
        if (status.isDebug()) {
            logger.debug("Setting Neo4j OGM transaction on Session [" + txObject.getSessionHolder().getSession()
                    + "] rollback-only");
        }
        status.setRollbackOnly();
    }

    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        Neo4jTransactionObject txObject = (Neo4jTransactionObject) transaction;

        // Remove the session holder from the thread, if still there.
        if (txObject.isNewSessionHolder()) {
            TransactionSynchronizationManager.unbindResourceIfPossible(getSessionFactory());
        }

        Transaction rawTransaction = txObject.getTransactionData();

        if (rawTransaction != null && rawTransaction.status().equals(Transaction.Status.OPEN)) {
            rawTransaction.close();
        }

        // Remove the session holder from the thread.
        if (txObject.isNewSessionHolder()) {
            Session session = txObject.getSessionHolder().getSession();
            if (logger.isDebugEnabled()) {
                logger.debug("Closing Neo4j Session [" + session + "] after transaction");
            }
        } else {
            logger.debug("Not closing pre-bound Neo4j Session after transaction");
        }

        txObject.getSessionHolder().clear();
    }

    /**
     * Neo4j OGM transaction object, representing a SessionHolder. Used as transaction object by Neo4jTransactionManager.
     */
    private static class Neo4jTransactionObject {

        private SessionHolder sessionHolder;

        private boolean newSessionHolder;

        private Transaction transactionData;

        void setSessionHolder(SessionHolder sessionHolder, boolean newSessionHolder) {
            this.sessionHolder = sessionHolder;
            this.newSessionHolder = newSessionHolder;
        }

        SessionHolder getSessionHolder() {
            return this.sessionHolder;
        }

        boolean isNewSessionHolder() {
            return this.newSessionHolder;
        }

        boolean hasTransaction() {
            return (this.sessionHolder != null && this.sessionHolder.isTransactionActive());
        }

        void setTransactionData(Transaction rawTransaction) {
            this.transactionData = rawTransaction;
            this.sessionHolder.setTransactionActive(true);
        }

        Transaction getTransactionData() {
            return this.transactionData;
        }
    }

    /**
     * Holder for suspended resources. Used internally by {@code doSuspend} and {@code doResume}.
     */
    private static class SuspendedResourcesHolder {

        private final SessionHolder sessionHolder;

        private SuspendedResourcesHolder(SessionHolder sessionHolder) {
            this.sessionHolder = sessionHolder;
        }

        private SessionHolder getSessionHolder() {
            return this.sessionHolder;
        }
    }
}