/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cayenne.tx;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
/**
* A Cayenne transaction. Currently supports managing JDBC connections.
*
* @since 4.0
*/
public abstract class BaseTransaction implements Transaction {
/**
* A ThreadLocal that stores current thread transaction.
*/
static final ThreadLocal<Transaction> CURRENT_TRANSACTION = new InheritableThreadLocal<>();
protected static final int STATUS_ACTIVE = 1;
protected static final int STATUS_COMMITTING = 2;
protected static final int STATUS_COMMITTED = 3;
protected static final int STATUS_ROLLEDBACK = 4;
protected static final int STATUS_ROLLING_BACK = 5;
protected static final int STATUS_NO_TRANSACTION = 6;
protected static final int STATUS_MARKED_ROLLEDBACK = 7;
protected Map<String, Connection> connections;
protected Collection<TransactionListener> listeners;
protected int status;
static String decodeStatus(int status) {
switch (status) {
case STATUS_ACTIVE:
return "STATUS_ACTIVE";
case STATUS_COMMITTING:
return "STATUS_COMMITTING";
case STATUS_COMMITTED:
return "STATUS_COMMITTED";
case STATUS_ROLLEDBACK:
return "STATUS_ROLLEDBACK";
case STATUS_ROLLING_BACK:
return "STATUS_ROLLING_BACK";
case STATUS_NO_TRANSACTION:
return "STATUS_NO_TRANSACTION";
case STATUS_MARKED_ROLLEDBACK:
return "STATUS_MARKED_ROLLEDBACK";
default:
return "Unknown Status - " + status;
}
}
/**
* Binds a Transaction to the current thread.
*/
public static void bindThreadTransaction(Transaction transaction) {
CURRENT_TRANSACTION.set(transaction);
}
/**
* Returns a Transaction associated with the current thread, or null if
* there is no such Transaction.
*/
public static Transaction getThreadTransaction() {
return CURRENT_TRANSACTION.get();
}
/**
* Creates new inactive transaction.
*/
protected BaseTransaction() {
this.status = STATUS_NO_TRANSACTION;
}
@Override
public void setRollbackOnly() {
this.status = STATUS_MARKED_ROLLEDBACK;
}
@Override
public boolean isRollbackOnly() {
return status == STATUS_MARKED_ROLLEDBACK;
}
@Override
public void addListener(TransactionListener listener) {
if (listeners == null) {
listeners = new LinkedHashSet<>();
}
listeners.add(listener);
}
/**
* Starts a Transaction. If Transaction is not started explicitly, it will
* be started when the first connection is added.
*/
@Override
public void begin() {
if (status != BaseTransaction.STATUS_NO_TRANSACTION) {
throw new IllegalStateException("Transaction must have 'STATUS_NO_TRANSACTION' to begin. "
+ "Current status: " + BaseTransaction.decodeStatus(status));
}
status = BaseTransaction.STATUS_ACTIVE;
}
@Override
public void commit() {
if (status == BaseTransaction.STATUS_NO_TRANSACTION) {
return;
}
if (status != BaseTransaction.STATUS_ACTIVE) {
throw new IllegalStateException("Transaction must have 'STATUS_ACTIVE' to be committed. "
+ "Current status: " + BaseTransaction.decodeStatus(status));
}
if (listeners != null) {
for (TransactionListener listener : listeners) {
listener.willCommit(this);
}
}
processCommit();
status = BaseTransaction.STATUS_COMMITTED;
close();
}
protected abstract void processCommit();
@Override
public void rollback() {
try {
if (status == BaseTransaction.STATUS_NO_TRANSACTION || status == BaseTransaction.STATUS_ROLLEDBACK
|| status == BaseTransaction.STATUS_ROLLING_BACK) {
return;
}
if (status != BaseTransaction.STATUS_ACTIVE && status != BaseTransaction.STATUS_MARKED_ROLLEDBACK) {
throw new IllegalStateException(
"Transaction must have 'STATUS_ACTIVE' or 'STATUS_MARKED_ROLLEDBACK' to be rolled back. "
+ "Current status: " + BaseTransaction.decodeStatus(status));
}
if (listeners != null) {
for (TransactionListener listener : listeners) {
listener.willRollback(this);
}
}
processRollback();
status = BaseTransaction.STATUS_ROLLEDBACK;
} finally {
close();
}
}
protected abstract void processRollback();
@Override
public Map<String, Connection> getConnections() {
return connections != null ? Collections.unmodifiableMap(connections) : Collections.<String, Connection>emptyMap();
}
@Override
public Connection getOrCreateConnection(String connectionName, DataSource dataSource) throws SQLException {
Connection c = getExistingConnection(connectionName);
if (c == null || c.isClosed()) {
c = dataSource.getConnection();
addConnection(connectionName, c);
}
// wrap transaction-attached connections in a decorator that prevents them from being closed by callers, as
// transaction should take care of them on commit or rollback.
return new TransactionConnectionDecorator(c);
}
protected Connection getExistingConnection(String name) {
return (connections != null) ? connections.get(name) : null;
}
protected Connection addConnection(String connectionName, Connection connection) {
TransactionConnectionDecorator wrapper = new TransactionConnectionDecorator(connection);
if (listeners != null) {
for (TransactionListener listener : listeners) {
listener.willAddConnection(this, connectionName, wrapper);
}
}
if (connections == null) {
// transaction is single-threaded, so using a non-concurrent map...
connections = new HashMap<>();
}
if (connections.put(connectionName, wrapper) != wrapper) {
connectionAdded(connection);
}
return wrapper;
}
protected void connectionAdded(Connection connection) {
// implicitly begin transaction
if (status == BaseTransaction.STATUS_NO_TRANSACTION) {
begin();
}
if (status != BaseTransaction.STATUS_ACTIVE) {
throw new IllegalStateException("Transaction must have 'STATUS_ACTIVE' to add a connection. "
+ "Current status: " + BaseTransaction.decodeStatus(status));
}
}
/**
* Closes all connections associated with transaction.
*/
protected void close() {
if (connections == null || connections.isEmpty()) {
return;
}
for (Connection c : connections.values()) {
try {
// make sure we unwrap TX connection before closing it, as the TX wrapper's "close" does nothing.
c.unwrap(Connection.class).close();
} catch (Throwable th) {
// TODO: chain exceptions...
// ignore for now
}
}
}
}