cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aadamc...@apache.org
Subject cayenne git commit: refactoring Transaction API towards cleaner separation of responsibilities
Date Mon, 02 Jan 2017 12:54:36 GMT
Repository: cayenne
Updated Branches:
  refs/heads/master cf67e0082 -> 36e70f4e7


refactoring Transaction API towards cleaner separation of responsibilities


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/36e70f4e
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/36e70f4e
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/36e70f4e

Branch: refs/heads/master
Commit: 36e70f4e72de3506f4ffeeeeeb6702ab3fbbba16
Parents: cf67e00
Author: Andrus Adamchik <andrus@objectstyle.com>
Authored: Mon Jan 2 15:36:51 2017 +0300
Committer: Andrus Adamchik <andrus@objectstyle.com>
Committed: Mon Jan 2 15:50:49 2017 +0300

----------------------------------------------------------------------
 .../org/apache/cayenne/access/DataNode.java     | 196 ++++----
 .../access/TransactionConnectionDecorator.java  | 387 ----------------
 .../org/apache/cayenne/tx/BaseTransaction.java  | 443 ++++++++++---------
 .../apache/cayenne/tx/CayenneTransaction.java   |  11 +-
 .../java/org/apache/cayenne/tx/Transaction.java |  29 +-
 .../tx/TransactionConnectionDecorator.java      | 386 ++++++++++++++++
 .../cayenne/access/TransactionThreadIT.java     | 117 -----
 .../cayenne/access/UserTransactionIT.java       | 114 -----
 .../apache/cayenne/tx/TransactionThreadIT.java  |  66 +++
 .../apache/cayenne/tx/UserTransactionIT.java    | 113 +++++
 10 files changed, 912 insertions(+), 950 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
index 0014bdf..88b864b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataNode.java
@@ -235,7 +235,7 @@ public class DataNode implements QueryEngine {
 
 	/**
 	 * Returns a DataNode that should handle queries for all DataMap components.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	public DataNode lookupDataNode(DataMap dataMap) {
@@ -245,7 +245,7 @@ public class DataNode implements QueryEngine {
 
 	/**
 	 * Runs queries using Connection obtained from internal DataSource.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	@Override
@@ -326,7 +326,7 @@ public class DataNode implements QueryEngine {
 	 * Sets EntityResolver. DataNode relies on externally set EntityResolver, so
 	 * if the node is created outside of DataDomain stack, a valid
 	 * EntityResolver must be provided explicitly.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	public void setEntityResolver(EntityResolver entityResolver) {
@@ -338,115 +338,10 @@ public class DataNode implements QueryEngine {
 		return new ToStringBuilder(this).append("name", getName()).toString();
 	}
 
-	// a read-through DataSource that ensures returning the same connection
-	// within
-	// transaction.
-	final class TransactionDataSource implements DataSource {
-
-		final String CONNECTION_RESOURCE_PREFIX = "DataNode.Connection.";
-
-		public Connection getConnection() throws SQLException {
-			if (schemaUpdateStrategy != null) {
-				schemaUpdateStrategy.updateSchema(DataNode.this);
-			}
-			Transaction t = BaseTransaction.getThreadTransaction();
-
-			if (t != null) {
-				String key = CONNECTION_RESOURCE_PREFIX + name;
-				Connection c = t.getConnection(key);
-
-				if (c == null || c.isClosed()) {
-					c = dataSource.getConnection();
-					t.addConnection(key, 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);
-			}
-
-			return dataSource.getConnection();
-		}
-
-		public Connection getConnection(String username, String password) throws SQLException {
-			if (schemaUpdateStrategy != null) {
-				schemaUpdateStrategy.updateSchema(DataNode.this);
-			}
-			Transaction t = BaseTransaction.getThreadTransaction();
-			if (t != null) {
-				String key = CONNECTION_RESOURCE_PREFIX + name;
-				Connection c = t.getConnection(key);
-
-				if (c == null || c.isClosed()) {
-					c = dataSource.getConnection();
-					t.addConnection(key, 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);
-			}
-
-			return dataSource.getConnection(username, password);
-		}
-
-		public int getLoginTimeout() throws SQLException {
-			return dataSource.getLoginTimeout();
-		}
-
-		public PrintWriter getLogWriter() throws SQLException {
-			return dataSource.getLogWriter();
-		}
-
-		public void setLoginTimeout(int seconds) throws SQLException {
-			dataSource.setLoginTimeout(seconds);
-		}
-
-		public void setLogWriter(PrintWriter out) throws SQLException {
-			dataSource.setLogWriter(out);
-		}
-
-		/**
-		 * @since 3.0
-		 */
-		// JDBC 4 compatibility under Java 1.5
-		public boolean isWrapperFor(Class<?> iface) throws SQLException {
-			return iface.isAssignableFrom(dataSource.getClass());
-		}
-
-		/**
-		 * @since 3.0
-		 */
-		// JDBC 4 compatibility under Java 1.5
-		public <T> T unwrap(Class<T> iface) throws SQLException {
-			try {
-				return iface.cast(dataSource);
-			} catch (ClassCastException e) {
-				throw new SQLException("Not a DataSource: " + e.getMessage());
-			}
-		}
-
-		/**
-		 * @since 3.1
-		 * 
-		 *        JDBC 4.1 compatibility under Java 1.5
-		 */
-		public Logger getParentLogger() throws SQLFeatureNotSupportedException {
-			// don't throw SQLFeatureNotSupported - this will break JDK 1.5
-			// runtime
-			throw new UnsupportedOperationException();
-		}
-	}
 
 	/**
 	 * Creates a {@link RowReader} using internal {@link RowReaderFactory}.
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata queryMetadata) {
@@ -455,7 +350,7 @@ public class DataNode implements QueryEngine {
 
 	/**
 	 * Creates a {@link RowReader} using internal {@link RowReaderFactory}.
-	 * 
+	 *
 	 * @since 4.0
 	 */
 	public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
@@ -532,4 +427,85 @@ public class DataNode implements QueryEngine {
 	public void setSelectTranslatorFactory(SelectTranslatorFactory selectTranslatorFactory) {
 		this.selectTranslatorFactory = selectTranslatorFactory;
 	}
+
+    // a read-through DataSource that ensures returning the same connection
+    // within
+    // transaction.
+    final class TransactionDataSource implements DataSource {
+
+        final String CONNECTION_RESOURCE_PREFIX = "DataNode.Connection.";
+
+
+        @Override
+        public Connection getConnection() throws SQLException {
+            if (schemaUpdateStrategy != null) {
+                schemaUpdateStrategy.updateSchema(DataNode.this);
+            }
+
+            Transaction t = BaseTransaction.getThreadTransaction();
+            return (t != null) ? t.getOrCreateConnection(CONNECTION_RESOURCE_PREFIX + name, dataSource)
+                    : dataSource.getConnection();
+        }
+
+        @Override
+        public Connection getConnection(String username, String password) throws SQLException {
+            if (schemaUpdateStrategy != null) {
+                schemaUpdateStrategy.updateSchema(DataNode.this);
+            }
+
+            Transaction t = BaseTransaction.getThreadTransaction();
+            return (t != null) ? t.getOrCreateConnection(CONNECTION_RESOURCE_PREFIX + name, dataSource)
+                    : dataSource.getConnection(username, password);
+        }
+
+        @Override
+        public int getLoginTimeout() throws SQLException {
+            return dataSource.getLoginTimeout();
+        }
+
+        @Override
+        public PrintWriter getLogWriter() throws SQLException {
+            return dataSource.getLogWriter();
+        }
+
+        @Override
+        public void setLoginTimeout(int seconds) throws SQLException {
+            dataSource.setLoginTimeout(seconds);
+        }
+
+        @Override
+        public void setLogWriter(PrintWriter out) throws SQLException {
+            dataSource.setLogWriter(out);
+        }
+
+        /**
+         * @since 3.0
+         */
+        @Override
+        public boolean isWrapperFor(Class<?> iface) throws SQLException {
+            return iface.isAssignableFrom(dataSource.getClass());
+        }
+
+        /**
+         * @since 3.0
+         */
+        @Override
+        public <T> T unwrap(Class<T> iface) throws SQLException {
+            try {
+                return iface.cast(dataSource);
+            } catch (ClassCastException e) {
+                throw new SQLException("Not a DataSource: " + e.getMessage());
+            }
+        }
+
+        /**
+         * @since 3.1
+         */
+        @Override
+        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+            // don't throw SQLFeatureNotSupported - this will break JDK 1.5
+            // runtime
+            throw new UnsupportedOperationException();
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/main/java/org/apache/cayenne/access/TransactionConnectionDecorator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/TransactionConnectionDecorator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/TransactionConnectionDecorator.java
deleted file mode 100644
index 518a95e..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/TransactionConnectionDecorator.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/*****************************************************************
- *   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.access;
-
-import java.sql.Array;
-import java.sql.Blob;
-import java.sql.CallableStatement;
-import java.sql.Clob;
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.NClob;
-import java.sql.PreparedStatement;
-import java.sql.SQLClientInfoException;
-import java.sql.SQLException;
-import java.sql.SQLWarning;
-import java.sql.SQLXML;
-import java.sql.Savepoint;
-import java.sql.Statement;
-import java.sql.Struct;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.Executor;
-
-/**
- * A wrapper of a JDBC connection that is attached to a transaction. The
- * behavior of this object to delegate all method calls to the underlying
- * connection, except for the 'close' method that is implemented as noop in hope
- * that a transaction originator will close the underlying Connection object.
- * 
- * @since 1.2
- */
-class TransactionConnectionDecorator implements Connection {
-
-	Connection connection;
-
-	TransactionConnectionDecorator(Connection connection) {
-		this.connection = connection;
-	}
-
-	// the only method that is NOT delegated...
-	@Override
-	public void close() throws SQLException {
-		// noop
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public void abort(Executor executor) throws SQLException {
-
-		// do nothing; same as 'close'
-	}
-
-	@Override
-	public void clearWarnings() throws SQLException {
-		connection.clearWarnings();
-	}
-
-	@Override
-	public void commit() throws SQLException {
-		connection.commit();
-	}
-
-	@Override
-	public Statement createStatement() throws SQLException {
-		return connection.createStatement();
-	}
-
-	@Override
-	public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
-			throws SQLException {
-		return connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
-	}
-
-	@Override
-	public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
-		return connection.createStatement(resultSetType, resultSetConcurrency);
-	}
-
-	@Override
-	public boolean getAutoCommit() throws SQLException {
-		return connection.getAutoCommit();
-	}
-
-	@Override
-	public String getCatalog() throws SQLException {
-		return connection.getCatalog();
-	}
-
-	@Override
-	public int getHoldability() throws SQLException {
-		return connection.getHoldability();
-	}
-
-	@Override
-	public DatabaseMetaData getMetaData() throws SQLException {
-		return connection.getMetaData();
-	}
-
-	@Override
-	public int getTransactionIsolation() throws SQLException {
-		return connection.getTransactionIsolation();
-	}
-
-	@Override
-	public Map<String, Class<?>> getTypeMap() throws SQLException {
-		return connection.getTypeMap();
-	}
-
-	@Override
-	public SQLWarning getWarnings() throws SQLException {
-		return connection.getWarnings();
-	}
-
-	@Override
-	public boolean isClosed() throws SQLException {
-		return connection.isClosed();
-	}
-
-	@Override
-	public boolean isReadOnly() throws SQLException {
-		return connection.isReadOnly();
-	}
-
-	@Override
-	public String nativeSQL(String sql) throws SQLException {
-		return connection.nativeSQL(sql);
-	}
-
-	@Override
-	public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
-			int resultSetHoldability) throws SQLException {
-		return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
-	}
-
-	@Override
-	public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
-		return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
-	}
-
-	@Override
-	public CallableStatement prepareCall(String sql) throws SQLException {
-		return connection.prepareCall(sql);
-	}
-
-	@Override
-	public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
-			int resultSetHoldability) throws SQLException {
-		return connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
-	}
-
-	@Override
-	public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
-			throws SQLException {
-		return connection.prepareStatement(sql, resultSetType, resultSetConcurrency);
-	}
-
-	@Override
-	public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
-		return connection.prepareStatement(sql, autoGeneratedKeys);
-	}
-
-	@Override
-	public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
-		return connection.prepareStatement(sql, columnIndexes);
-	}
-
-	@Override
-	public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
-		return connection.prepareStatement(sql, columnNames);
-	}
-
-	@Override
-	public PreparedStatement prepareStatement(String sql) throws SQLException {
-		return connection.prepareStatement(sql);
-	}
-
-	@Override
-	public void releaseSavepoint(Savepoint savepoint) throws SQLException {
-		connection.releaseSavepoint(savepoint);
-	}
-
-	@Override
-	public void rollback() throws SQLException {
-		connection.rollback();
-	}
-
-	@Override
-	public void rollback(Savepoint savepoint) throws SQLException {
-		connection.rollback(savepoint);
-	}
-
-	@Override
-	public void setAutoCommit(boolean autoCommit) throws SQLException {
-		connection.setAutoCommit(autoCommit);
-	}
-
-	@Override
-	public void setCatalog(String catalog) throws SQLException {
-		connection.setCatalog(catalog);
-	}
-
-	@Override
-	public void setHoldability(int holdability) throws SQLException {
-		connection.setHoldability(holdability);
-	}
-
-	@Override
-	public void setReadOnly(boolean readOnly) throws SQLException {
-		connection.setReadOnly(readOnly);
-	}
-
-	@Override
-	public Savepoint setSavepoint() throws SQLException {
-		return connection.setSavepoint();
-	}
-
-	@Override
-	public Savepoint setSavepoint(String name) throws SQLException {
-		return connection.setSavepoint(name);
-	}
-
-	@Override
-	public void setTransactionIsolation(int level) throws SQLException {
-		connection.setTransactionIsolation(level);
-	}
-
-	@Override
-	public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
-		connection.setTypeMap(map);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
-		return connection.createArrayOf(typeName, elements);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public Blob createBlob() throws SQLException {
-		return connection.createBlob();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public Clob createClob() throws SQLException {
-		return connection.createClob();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
-		return connection.createStruct(typeName, attributes);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public Properties getClientInfo() throws SQLException {
-		return connection.getClientInfo();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public String getClientInfo(String name) throws SQLException {
-		return connection.getClientInfo(name);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public void setClientInfo(Properties properties) throws SQLClientInfoException {
-		connection.setClientInfo(properties);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public void setClientInfo(String name, String value) throws SQLClientInfoException {
-		connection.setClientInfo(name, value);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public boolean isValid(int timeout) throws SQLException {
-		return connection.isValid(timeout);
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public boolean isWrapperFor(Class<?> iface) throws SQLException {
-		// TODO...
-		throw new UnsupportedOperationException();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public <T> T unwrap(Class<T> iface) throws SQLException {
-		// TODO
-		throw new UnsupportedOperationException();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public NClob createNClob() throws SQLException {
-		return connection.createNClob();
-	}
-
-	/**
-	 * @since 3.0
-	 */
-	@Override
-	public SQLXML createSQLXML() throws SQLException {
-		return connection.createSQLXML();
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public void setSchema(String schema) throws SQLException {
-		connection.setSchema(schema);
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public String getSchema() throws SQLException {
-		return connection.getSchema();
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
-		connection.setNetworkTimeout(executor, milliseconds);
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	public int getNetworkTimeout() throws SQLException {
-		return connection.getNetworkTimeout();
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
index 8ed44fc..6281b01 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
@@ -19,226 +19,249 @@
 
 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.Iterator;
 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> currentTransaction = new InheritableThreadLocal<Transaction>();
-
-	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) {
-		currentTransaction.set(transaction);
-	}
-
-	/**
-	 * Returns a Transaction associated with the current thread, or null if
-	 * there is no such Transaction.
-	 */
-	public static Transaction getThreadTransaction() {
-		return currentTransaction.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 Connection getConnection(String name) {
-		return (connections != null) ? connections.get(name) : null;
-	}
-
-	@Override
-	public void addConnection(String connectionName, Connection connection) {
-
-		if(listeners != null) {
-			for(TransactionListener listener : listeners) {
-				listener.willAddConnection(this, connectionName, connection);
-			}
-		}
-
-		if (connections == null) {
-			connections = new HashMap<>();
-		}
-
-		if (connections.put(connectionName, connection) != connection) {
-			connectionAdded(connection);
-		}
-	}
-
-	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;
-		}
-
-		Iterator<?> it = connections.values().iterator();
-		while (it.hasNext()) {
-			try {
-
-				((Connection) it.next()).close();
-			} catch (Throwable th) {
-				// TODO: chain exceptions...
-				// ignore for now
-			}
-		}
-	}
+    /**
+     * 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) {
+            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
+            }
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
index 9c69db2..f0ab5d8 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/CayenneTransaction.java
@@ -19,13 +19,13 @@
 
 package org.apache.cayenne.tx;
 
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.log.JdbcEventLogger;
+
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.Iterator;
 
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.log.JdbcEventLogger;
-
 /**
  * Represents a Cayenne-managed local Transaction.
  * 
@@ -58,9 +58,8 @@ public class CayenneTransaction extends BaseTransaction {
 
     void fixConnectionState(Connection connection) throws SQLException {
         if (connection.getAutoCommit()) {
-            // some DBs are very particular about that, (e.g. Informix SE 7.0
-            // per
-            // CAY-179), so do a try-catch and ignore exception
+            // some DBs are very particular about that, (e.g. Informix SE 7.0 per CAY-179), so do a try-catch and
+            // ignore exception
 
             // TODO: maybe allow adapter to provide transaction instance?
             try {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java
index 789573f..f87e8c3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java
@@ -18,18 +18,21 @@
  ****************************************************************/
 package org.apache.cayenne.tx;
 
+import javax.sql.DataSource;
 import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Map;
 
 /**
- * A Cayenne Transaction interface.
- * 
+ * Cayenne Transaction interface.
+ *
  * @since 4.0
  */
 public interface Transaction {
 
     /**
-     * Starts a Transaction. If Transaction is not started explicitly, it will
-     * be started when the first connection is added.
+     * Starts a Transaction. If Transaction is not started explicitly, it will be started when the first connection is
+     * added.
      */
     void begin();
 
@@ -41,9 +44,23 @@ public interface Transaction {
 
     boolean isRollbackOnly();
 
-    Connection getConnection(String name);
+    /**
+     * Retrieves a connection for the given symbolic name. If it does not exists, creates a new connection using
+     * provided DataSource, and registers it internally.
+     *
+     * @param connectionName a symbolic name of the connection. Cayenne DataNodes generate a name in the form of
+     *                       "DataNode.Connection.nodename".
+     * @param dataSource     DataSource that provides new connections.
+     * @return a connection that participates in the current transaction.
+     */
+    Connection getOrCreateConnection(String connectionName, DataSource dataSource) throws SQLException;
 
-    void addConnection(String connectionName, Connection connection);
+    /**
+     * Returns all connections associated with the transaction.
+     *
+     * @return connections associated with the transaction.
+     */
+    Map<String, Connection> getConnections();
 
     void addListener(TransactionListener listener);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionConnectionDecorator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionConnectionDecorator.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionConnectionDecorator.java
new file mode 100644
index 0000000..32d3f0c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionConnectionDecorator.java
@@ -0,0 +1,386 @@
+/*****************************************************************
+ *   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 java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.SQLClientInfoException;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Savepoint;
+import java.sql.Statement;
+import java.sql.Struct;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+
+/**
+ * A wrapper of a JDBC connection that is attached to a transaction. The behavior of this object to delegate all method
+ * calls to the underlying connection, except for the 'close' method that is implemented as noop in hope that a
+ * transaction originator will close the underlying Connection object.
+ */
+class TransactionConnectionDecorator implements Connection {
+
+    Connection connection;
+
+    TransactionConnectionDecorator(Connection connection) {
+        this.connection = connection;
+    }
+
+    // the only method that is NOT delegated...
+    @Override
+    public void close() throws SQLException {
+        // noop
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public void abort(Executor executor) throws SQLException {
+
+        // do nothing; same as 'close'
+    }
+
+    @Override
+    public void clearWarnings() throws SQLException {
+        connection.clearWarnings();
+    }
+
+    @Override
+    public void commit() throws SQLException {
+        connection.commit();
+    }
+
+    @Override
+    public Statement createStatement() throws SQLException {
+        return connection.createStatement();
+    }
+
+    @Override
+    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
+            throws SQLException {
+        return connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
+    }
+
+    @Override
+    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
+        return connection.createStatement(resultSetType, resultSetConcurrency);
+    }
+
+    @Override
+    public boolean getAutoCommit() throws SQLException {
+        return connection.getAutoCommit();
+    }
+
+    @Override
+    public String getCatalog() throws SQLException {
+        return connection.getCatalog();
+    }
+
+    @Override
+    public int getHoldability() throws SQLException {
+        return connection.getHoldability();
+    }
+
+    @Override
+    public DatabaseMetaData getMetaData() throws SQLException {
+        return connection.getMetaData();
+    }
+
+    @Override
+    public int getTransactionIsolation() throws SQLException {
+        return connection.getTransactionIsolation();
+    }
+
+    @Override
+    public Map<String, Class<?>> getTypeMap() throws SQLException {
+        return connection.getTypeMap();
+    }
+
+    @Override
+    public SQLWarning getWarnings() throws SQLException {
+        return connection.getWarnings();
+    }
+
+    @Override
+    public boolean isClosed() throws SQLException {
+        return connection.isClosed();
+    }
+
+    @Override
+    public boolean isReadOnly() throws SQLException {
+        return connection.isReadOnly();
+    }
+
+    @Override
+    public String nativeSQL(String sql) throws SQLException {
+        return connection.nativeSQL(sql);
+    }
+
+    @Override
+    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
+                                         int resultSetHoldability) throws SQLException {
+        return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
+    }
+
+    @Override
+    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
+        return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
+    }
+
+    @Override
+    public CallableStatement prepareCall(String sql) throws SQLException {
+        return connection.prepareCall(sql);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
+                                              int resultSetHoldability) throws SQLException {
+        return connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
+            throws SQLException {
+        return connection.prepareStatement(sql, resultSetType, resultSetConcurrency);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
+        return connection.prepareStatement(sql, autoGeneratedKeys);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
+        return connection.prepareStatement(sql, columnIndexes);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
+        return connection.prepareStatement(sql, columnNames);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String sql) throws SQLException {
+        return connection.prepareStatement(sql);
+    }
+
+    @Override
+    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
+        connection.releaseSavepoint(savepoint);
+    }
+
+    @Override
+    public void rollback() throws SQLException {
+        connection.rollback();
+    }
+
+    @Override
+    public void rollback(Savepoint savepoint) throws SQLException {
+        connection.rollback(savepoint);
+    }
+
+    @Override
+    public void setAutoCommit(boolean autoCommit) throws SQLException {
+        connection.setAutoCommit(autoCommit);
+    }
+
+    @Override
+    public void setCatalog(String catalog) throws SQLException {
+        connection.setCatalog(catalog);
+    }
+
+    @Override
+    public void setHoldability(int holdability) throws SQLException {
+        connection.setHoldability(holdability);
+    }
+
+    @Override
+    public void setReadOnly(boolean readOnly) throws SQLException {
+        connection.setReadOnly(readOnly);
+    }
+
+    @Override
+    public Savepoint setSavepoint() throws SQLException {
+        return connection.setSavepoint();
+    }
+
+    @Override
+    public Savepoint setSavepoint(String name) throws SQLException {
+        return connection.setSavepoint(name);
+    }
+
+    @Override
+    public void setTransactionIsolation(int level) throws SQLException {
+        connection.setTransactionIsolation(level);
+    }
+
+    @Override
+    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+        connection.setTypeMap(map);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
+        return connection.createArrayOf(typeName, elements);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public Blob createBlob() throws SQLException {
+        return connection.createBlob();
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public Clob createClob() throws SQLException {
+        return connection.createClob();
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
+        return connection.createStruct(typeName, attributes);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public Properties getClientInfo() throws SQLException {
+        return connection.getClientInfo();
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public String getClientInfo(String name) throws SQLException {
+        return connection.getClientInfo(name);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public void setClientInfo(Properties properties) throws SQLClientInfoException {
+        connection.setClientInfo(properties);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public void setClientInfo(String name, String value) throws SQLClientInfoException {
+        connection.setClientInfo(name, value);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public boolean isValid(int timeout) throws SQLException {
+        return connection.isValid(timeout);
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public boolean isWrapperFor(Class<?> iface) throws SQLException {
+        return iface.isAssignableFrom(connection.getClass());
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public <T> T unwrap(Class<T> iface) throws SQLException {
+        try {
+            return iface.cast(connection);
+        } catch (ClassCastException e) {
+            throw new SQLException("Not a Connection: " + e.getMessage());
+        }
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public NClob createNClob() throws SQLException {
+        return connection.createNClob();
+    }
+
+    /**
+     * @since 3.0
+     */
+    @Override
+    public SQLXML createSQLXML() throws SQLException {
+        return connection.createSQLXML();
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public void setSchema(String schema) throws SQLException {
+        connection.setSchema(schema);
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public String getSchema() throws SQLException {
+        return connection.getSchema();
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
+        connection.setNetworkTimeout(executor, milliseconds);
+    }
+
+    /**
+     * @since 3.1
+     */
+    public int getNetworkTimeout() throws SQLException {
+        return connection.getNetworkTimeout();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java
deleted file mode 100644
index 50368e2..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*****************************************************************
- *   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.access;
-
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.query.SelectQuery;
-import org.apache.cayenne.testdo.testmap.Artist;
-import org.apache.cayenne.tx.BaseTransaction;
-import org.apache.cayenne.tx.CayenneTransaction;
-import org.apache.cayenne.tx.Transaction;
-import org.apache.cayenne.tx.TransactionListener;
-import org.apache.cayenne.unit.di.server.CayenneProjects;
-import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.Test;
-
-import java.sql.Connection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class TransactionThreadIT extends ServerCase {
-
-    @Inject
-    private DataContext context;
-
-    @Inject
-    private JdbcEventLogger logger;
-
-    @Test
-    public void testThreadConnectionReuseOnSelect() throws Exception {
-
-        ConnectionCounterTx t = new ConnectionCounterTx(new CayenneTransaction(logger));
-        BaseTransaction.bindThreadTransaction(t);
-
-        try {
-
-            SelectQuery q1 = new SelectQuery(Artist.class);
-            context.performQuery(q1);
-            assertEquals(1, t.connectionCount);
-
-            // delegate will fail if the second query opens a new connection
-            SelectQuery q2 = new SelectQuery(Artist.class);
-            context.performQuery(q2);
-
-        } finally {
-            BaseTransaction.bindThreadTransaction(null);
-            t.commit();
-        }
-    }
-
-    class ConnectionCounterTx implements Transaction {
-
-        private Transaction delegate;
-        int connectionCount;
-
-        ConnectionCounterTx(Transaction delegate) {
-            this.delegate = delegate;
-        }
-
-        public void begin() {
-            delegate.begin();
-        }
-
-        public void commit() {
-            delegate.commit();
-        }
-
-        public void rollback() {
-            delegate.rollback();
-        }
-
-        public void setRollbackOnly() {
-            delegate.setRollbackOnly();
-        }
-
-        public boolean isRollbackOnly() {
-            return delegate.isRollbackOnly();
-        }
-
-        public Connection getConnection(String name) {
-            return delegate.getConnection(name);
-        }
-
-        public void addConnection(String name, Connection connection) {
-            if (connectionCount++ > 0) {
-                fail("Invalid attempt to add connection");
-            }
-
-            delegate.addConnection(name, connection);
-        }
-
-        @Override
-        public void addListener(TransactionListener listener) {
-            delegate.addListener(listener);
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java
deleted file mode 100644
index f644639..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*****************************************************************
- *   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.access;
-
-import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.log.JdbcEventLogger;
-import org.apache.cayenne.testdo.testmap.Artist;
-import org.apache.cayenne.tx.BaseTransaction;
-import org.apache.cayenne.tx.CayenneTransaction;
-import org.apache.cayenne.tx.Transaction;
-import org.apache.cayenne.tx.TransactionListener;
-import org.apache.cayenne.unit.di.server.CayenneProjects;
-import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.Test;
-
-import java.sql.Connection;
-
-import static org.junit.Assert.assertEquals;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class UserTransactionIT extends ServerCase {
-
-    @Inject
-    private ObjectContext context;
-
-    @Inject
-    private JdbcEventLogger logger;
-
-    @Test
-    public void testCommit() throws Exception {
-
-        Artist a = context.newObject(Artist.class);
-        a.setArtistName("AAA");
-
-        TxWrapper t = new TxWrapper(new CayenneTransaction(logger));
-        BaseTransaction.bindThreadTransaction(t);
-
-        try {
-            context.commitChanges();
-        } finally {
-            t.rollback();
-            BaseTransaction.bindThreadTransaction(null);
-        }
-
-        assertEquals(0, t.commitCount);
-        assertEquals(1, t.connectionCount);
-    }
-
-    class TxWrapper implements Transaction {
-
-        int commitCount;
-        int connectionCount;
-        private Transaction delegate;
-
-        TxWrapper(Transaction delegate) {
-            this.delegate = delegate;
-        }
-
-        public void begin() {
-            delegate.begin();
-        }
-
-        public void commit() {
-            commitCount++;
-            delegate.commit();
-        }
-
-        public void rollback() {
-            delegate.rollback();
-        }
-
-        public void setRollbackOnly() {
-            delegate.setRollbackOnly();
-        }
-
-        public boolean isRollbackOnly() {
-            return delegate.isRollbackOnly();
-        }
-
-        public Connection getConnection(String name) {
-            return delegate.getConnection(name);
-        }
-
-        public void addConnection(String name, Connection connection) {
-            connectionCount++;
-            delegate.addConnection(name, connection);
-        }
-
-        @Override
-        public void addListener(TransactionListener listener) {
-            delegate.addListener(listener);
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionThreadIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionThreadIT.java b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionThreadIT.java
new file mode 100644
index 0000000..f2ef5ac
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionThreadIT.java
@@ -0,0 +1,66 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class TransactionThreadIT extends ServerCase {
+
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private JdbcEventLogger logger;
+
+    @Test
+    public void testThreadConnectionReuseOnSelect() throws Exception {
+
+        Transaction t = new CayenneTransaction(logger);
+        BaseTransaction.bindThreadTransaction(t);
+
+        try {
+
+            SelectQuery q1 = new SelectQuery(Artist.class);
+            context.performQuery(q1);
+            assertEquals(1, t.getConnections().size());
+
+            // delegate will fail if the second query opens a new connection
+            SelectQuery q2 = new SelectQuery(Artist.class);
+            context.performQuery(q2);
+
+            assertEquals(1, t.getConnections().size());
+
+        } finally {
+            BaseTransaction.bindThreadTransaction(null);
+            t.commit();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/36e70f4e/cayenne-server/src/test/java/org/apache/cayenne/tx/UserTransactionIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/tx/UserTransactionIT.java b/cayenne-server/src/test/java/org/apache/cayenne/tx/UserTransactionIT.java
new file mode 100644
index 0000000..9d1338f
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/UserTransactionIT.java
@@ -0,0 +1,113 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class UserTransactionIT extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    private JdbcEventLogger logger;
+
+    @Test
+    public void testCommit() throws Exception {
+
+        Artist a = context.newObject(Artist.class);
+        a.setArtistName("AAA");
+
+        TxWrapper t = new TxWrapper(new CayenneTransaction(logger));
+        BaseTransaction.bindThreadTransaction(t);
+
+        try {
+            context.commitChanges();
+        } finally {
+            t.rollback();
+            BaseTransaction.bindThreadTransaction(null);
+        }
+
+        assertEquals(0, t.commitCount);
+        assertEquals(1, t.getConnections().size());
+    }
+
+    class TxWrapper implements Transaction {
+
+        int commitCount;
+        private Transaction delegate;
+
+        TxWrapper(Transaction delegate) {
+            this.delegate = delegate;
+        }
+
+        public void begin() {
+            delegate.begin();
+        }
+
+        public void commit() {
+            commitCount++;
+            delegate.commit();
+        }
+
+        public void rollback() {
+            delegate.rollback();
+        }
+
+        public void setRollbackOnly() {
+            delegate.setRollbackOnly();
+        }
+
+        public boolean isRollbackOnly() {
+            return delegate.isRollbackOnly();
+        }
+
+        @Override
+        public Connection getOrCreateConnection(String connectionName, DataSource dataSource) throws SQLException {
+            return delegate.getOrCreateConnection(connectionName, dataSource);
+        }
+
+        @Override
+        public Map<String, Connection> getConnections() {
+            return delegate.getConnections();
+        }
+
+        @Override
+        public void addListener(TransactionListener listener) {
+            delegate.addListener(listener);
+        }
+    }
+
+}


Mime
View raw message