/* * Copyright 2010-2017 Norwegian Agency for Public Management and eGovernment (Difi) * * Licensed under the EUPL, Version 1.1 or – as soon they * will be approved by the European Commission - subsequent * versions of the EUPL (the "Licence"); * * You may not use this work except in compliance with the Licence. * * You may obtain a copy of the Licence at: * * https://joinup.ec.europa.eu/community/eupl/og_page/eupl * * Unless required by applicable law or agreed to in * writing, software distributed under the Licence is * distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. * See the Licence for the specific language governing * permissions and limitations under the Licence. */ package no.difi.oxalis.persistence.aop; import com.google.inject.Inject; import no.difi.oxalis.persistence.api.JdbcTxManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; /** * Implementation of a transaction manager, which is responsible * for handling a Connection object which is placed into ThreadLocal. * <p> * It is responsible for fetching Connection objects from a DataSource, and setting * them up so that they can be transactional (autoCommit --> false). * <p> * It also can be used to rollback programatically an existing transaction. */ public class JdbcTxManagerImpl implements JdbcTxManager { private static Logger log = LoggerFactory.getLogger(JdbcTxManagerImpl.class); private static int instances = 0; /** * Used to track problems with multiple instances being created. */ private int id; /** * Stores a thread local copy of the current connection */ private final ThreadLocal<JdbcTransaction> threadLocalJdbcTransaction = new ThreadLocal<>(); private final DataSource dataSource; @Inject public JdbcTxManagerImpl(DataSource dataSource) { if (dataSource == null) { throw new IllegalArgumentException("DataSource not supplied in constructor"); } this.id = instances; instances++; trace("new instance"); this.dataSource = dataSource; } @Override public boolean isTransaction() { try { final Connection connection = getThreadLocalConnection(); return connection != null && !connection.getAutoCommit(); } catch (SQLException e) { throw new IllegalStateException("Unable to check if a transaction has been started", e); } } @Override public boolean isConnection() { final Connection connection = getThreadLocalConnection(); return connection != null; } @Override public void newConnection(boolean autoCommit) { try { //only allowed to create a new transaction if the old one is commited. if (isTransaction()) { final String message = "Unable to start a new transaction existing connection is not commited"; trace(message); throw new IllegalStateException(message); } //fetches the connection from the datasource. final Connection connection = dataSource.getConnection(); //sets whether or not the connection should autocommit. connection.setAutoCommit(autoCommit); //adds the connection to the current thread final JdbcTransaction jdbcTransaction = new JdbcTransaction(connection); threadLocalJdbcTransaction.set(jdbcTransaction); } catch (SQLException e) { final String message = "Unable to get a connection from the provided datasource"; trace(message); throw new IllegalStateException(message, e); } } @Override public void commit() { try { if (!isTransaction()) { final String message = "Unable to commit transaction connection, no transaction exists"; trace(message); throw new IllegalStateException(message); } final JdbcTransaction jdbcTransaction = threadLocalJdbcTransaction.get(); //if the transaction has been marked for rollback, rollback the transaction. if (jdbcTransaction.isRollback()) { trace("Not commiting - Transaction marked for rollback"); rollback(); } else { //Commits the transaction... connection cannot be null as the isTransaction method tests for that trace("Commiting transaction"); jdbcTransaction.getConnection().commit(); } } catch (SQLException e) { final String message = "Unable to commit the transaction"; trace(message); throw new IllegalStateException(message, e); } } @Override public void rollback() { try { if (!isTransaction()) { final String message = "Unable to rollback transaction, no transaction exists"; trace(message); throw new IllegalStateException(message); } getThreadLocalConnection().rollback(); } catch (SQLException e) { final String message = "Unable to rollback the transaction"; trace(message); throw new IllegalStateException(message, e); } } @Override public void cleanUp() { try { //closes the connection final Connection connection = getThreadLocalConnection(); if (connection != null) { trace("closing connection"); connection.close(); } } catch (SQLException e) { final String message = "Unable to close the connection"; trace(message); throw new IllegalStateException(message, e); } finally { //Essential that we remove the reference to thread local to avoid memory leaks trace("Removing transaction manager"); threadLocalJdbcTransaction.set(null); threadLocalJdbcTransaction.remove(); // Ensures we don't get memory leaks } } /** * Gets the connection * * @return */ @Override public Connection getConnection() { final Connection connection = getThreadLocalConnection(); if (connection == null) { final String message = "Unable to get the connection. Did you forget to annotate the method with @Transactional or the repository with @Repository?"; trace(message); throw new IllegalStateException(message); } try { if (connection.isClosed()) { throw new IllegalStateException("Connection is closed!"); } } catch (SQLException e) { throw new IllegalStateException("Unable to inspect connection: " + e.getMessage(), e); } return connection; } /** * marks the transaction to be rollbacked */ @Override public void setRollbackOnly() { final JdbcTransaction jdbcTransaction = threadLocalJdbcTransaction.get(); if (jdbcTransaction == null) { final String message = "Unable to mark the transaction as rollbackOnly. Did you forget to annotate the method with @Transactional or the repository with @Repository?"; trace(message); throw new IllegalStateException(message); } trace("Transaction marked for rollback"); jdbcTransaction.setRollback(true); } /** * Helper method for null safe fetching of the JDBC Connection. * * @return */ private Connection getThreadLocalConnection() { final JdbcTransaction jdbcTransaction = threadLocalJdbcTransaction.get(); return jdbcTransaction == null ? null : jdbcTransaction.getConnection(); } /** * logs a debug message with the current transaction object * * @param message */ @Override public void trace(String message) { if (log.isDebugEnabled()) { JdbcTransaction jdbcTransaction = threadLocalJdbcTransaction.get(); final String transaction = jdbcTransaction == null ? "" : ("" + jdbcTransaction.hashCode()); log.debug(String.format("Trace %s:%s\t>>\t%s", id, transaction, message)); } } /** * Helper class that holds a Connection object and whether or not the transaction should be rolled back. */ private class JdbcTransaction { private final Connection connection; private boolean rollback = false; private JdbcTransaction(Connection connection) { this.connection = connection; } public Connection getConnection() { return connection; } public void setRollback(boolean rollback) { this.rollback = rollback; } public boolean isRollback() { return rollback; } } }