This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit d740e466c671ef8c3b70b908a77ccead5bfb3d26
Merge: 27f9d35 05eb42e
Author: Nikita Timofeev <[email protected]>
AuthorDate: Wed Jun 2 18:15:56 2021 +0300

    Merge PR #460
    CAY-2637 Allow forcing a custom Connection for a transaction

 .../org/apache/cayenne/tx/BaseTransaction.java     |  40 +++-
 .../apache/cayenne/tx/TransactionDescriptor.java   | 128 ++++++++++--
 .../org/apache/cayenne/tx/TransactionListener.java |  16 ++
 .../configuration/server/ServerRuntimeIT.java      |  32 ++-
 .../cayenne/tx/DefaultTransactionManagerIT.java    |  22 +-
 .../cayenne/tx/TransactionCustomConnectionIT.java  | 226 +++++++++++++++++++++
 .../apache/cayenne/tx/TransactionIsolationIT.java  |   8 +-
 .../tx/TransactionPropagationRollbackIT.java       |  24 +--
 8 files changed, 438 insertions(+), 58 deletions(-)

diff --cc 
cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
index a83990b,e3215b5..989dc88
--- a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
@@@ -204,7 -200,10 +204,11 @@@ public abstract class BaseTransaction i
          Connection c = getExistingConnection(connectionName);
  
          if (c == null || c.isClosed()) {
-             c = dataSource.getConnection();
 -            if(descriptor.getCustomConnectionSupplier() != null)
 -                c = descriptor.getCustomConnectionSupplier().get();
 -            else
++            if(descriptor.getConnectionSupplier() != null) {
++                c = descriptor.getConnectionSupplier().get();
++            } else {
+                 c = dataSource.getConnection();
++            }
              addConnection(connectionName, c);
          }
  
@@@ -241,6 -239,9 +244,10 @@@
              connections = new HashMap<>();
          }
  
 -        if (wrapper == null)
++        if (wrapper == null) {
+             wrapper = new TransactionConnectionDecorator(connection);
++        }
+ 
          if (connections.put(connectionName, wrapper) != wrapper) {
              connectionAdded(connection);
          }
diff --cc 
cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java
index 7dd60ce,3ced787..734132a
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java
@@@ -19,10 -19,12 +19,19 @@@
  
  package org.apache.cayenne.tx;
  
+ import java.sql.Connection;
+ import java.util.function.Supplier;
+ 
  /**
-  *
-- * Descriptor that provide desired transaction isolation level and 
propagation logic.
-- *
++ * Descriptor that allows to customize transaction logic.
++ * It provides following options:
++ * <ul>
++ *  <li> transaction isolation level
++ *  <li> transaction propagation logic.
++ *  <li> custom connection to use in a transaction
++ * </ul>
++ * @see TransactionManager#performInTransaction(TransactionalOperation, 
TransactionDescriptor)
++ * @see 
org.apache.cayenne.configuration.server.ServerRuntime#performInTransaction(TransactionalOperation,
 TransactionDescriptor)
   * @since 4.1
   */
  public class TransactionDescriptor {
@@@ -32,22 -34,27 +41,27 @@@
       */
      public static final int ISOLATION_DEFAULT = -1;
  
-     private final int isolation;
+     private int isolation;
+ 
+     private TransactionPropagation propagation;
  
-     private final TransactionPropagation propagation;
 -    private Supplier<Connection> customConnectionSupplier;
++    private Supplier<Connection> connectionSupplier;
+ 
+     private TransactionDescriptor() {
+     }
  
      /**
-      * @param isolation one of the following <code>Connection</code> 
constants:
-      *        <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
-      *        <code>Connection.TRANSACTION_READ_COMMITTED</code>,
-      *        <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
-      *        <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
-      *        <code>TransactionDescriptor.ISOLATION_DEFAULT</code>
-      *
+      * @param isolation   one of the following <code>Connection</code> 
constants:
+      *                    
<code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
+      *                    <code>Connection.TRANSACTION_READ_COMMITTED</code>,
+      *                    <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
+      *                    <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
+      *                    <code>TransactionDescriptor.ISOLATION_DEFAULT</code>
       * @param propagation transaction propagation behaviour
-      *
       * @see TransactionPropagation
 -     * @deprecated since 4.2. Use builder instead
++     * @deprecated since 4.2. Use {@link #builder()} method instead.
       */
+     @Deprecated
      public TransactionDescriptor(int isolation, TransactionPropagation 
propagation) {
          this.isolation = isolation;
          this.propagation = propagation;
@@@ -57,22 -64,23 +71,24 @@@
       * Create transaction descriptor with desired isolation level and 
<code>NESTED</code> propagation
       *
       * @param isolation one of the following <code>Connection</code> 
constants:
-      *        <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
-      *        <code>Connection.TRANSACTION_READ_COMMITTED</code>,
-      *        <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
-      *        <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
-      *        <code>TransactionDescriptor.ISOLATION_DEFAULT</code>
+      *                  <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
+      *                  <code>Connection.TRANSACTION_READ_COMMITTED</code>,
+      *                  <code>Connection.TRANSACTION_REPEATABLE_READ</code>,
+      *                  <code>Connection.TRANSACTION_SERIALIZABLE</code>, or
+      *                  <code>TransactionDescriptor.ISOLATION_DEFAULT</code>
++     * @deprecated since 4.2. Use {@link #builder()} method instead.
       */
+     @Deprecated
      public TransactionDescriptor(int isolation) {
          this(isolation, TransactionPropagation.NESTED);
      }
  
      /**
-      *
       * @param propagation transaction propagation behaviour
       * @see TransactionPropagation
 -     * @deprecated since 4.2. Use builder instead
++     * @deprecated since 4.2. Use {@link #builder()} method instead.
       */
+     @Deprecated
      public TransactionDescriptor(TransactionPropagation propagation) {
          this(ISOLATION_DEFAULT, propagation);
      }
@@@ -90,4 -98,71 +106,76 @@@
      public TransactionPropagation getPropagation() {
          return propagation;
      }
+ 
+     /**
+      * @return custom connection supplier, passed by user
+      */
 -    public Supplier<Connection> getCustomConnectionSupplier() {
 -        return customConnectionSupplier;
++    public Supplier<Connection> getConnectionSupplier() {
++        return connectionSupplier;
+     }
+ 
+     public static Builder builder(){
+         return new Builder();
+     }
+ 
+     /**
 -     * Builder class for TransactionDescriptor.
 -     * @since 4.2
++     * Builder class for the TransactionDescriptor.
+      */
+     public static class Builder {
+         private final TransactionDescriptor transactionDescriptor = new 
TransactionDescriptor();
+ 
 -        private Builder(){}
++        private Builder(){
++        }
+ 
+         /**
+          * @param isolation one of the following <code>Connection</code> 
constants:
+          *                  
<code>Connection.TRANSACTION_READ_UNCOMMITTED</code>,
+          *                  
<code>Connection.TRANSACTION_READ_COMMITTED</code>,
+          *                  
<code>Connection.TRANSACTION_REPEATABLE_READ</code>,
+          *                  <code>Connection.TRANSACTION_SERIALIZABLE</code>, 
or
+          *                  
<code>TransactionDescriptor.ISOLATION_DEFAULT</code>
+          */
+         public Builder isolation(int isolation) {
+             transactionDescriptor.isolation = isolation;
+             return this;
+         }
+ 
+         /**
++         * A custom connection provided by the TransactionDescriptor will be 
used
++         * instead of any other connection provided by tbe connection pool.
++         *
+          * @param connection custom connection
 -         * @see Connection
++         * @see #connectionSupplier(Supplier)
+          */
 -        public Builder connectionSupplier(Connection connection) {
 -            transactionDescriptor.customConnectionSupplier = () -> connection;
++        public Builder connection(Connection connection) {
++            transactionDescriptor.connectionSupplier = () -> connection;
+             return this;
+         }
+ 
+         /**
++         * A custom connection provided by the TransactionDescriptor will be 
used
++         * instead of any other connection provided by tbe connection pool.
++         *
+          * @param connectionSupplier custom connection supplier
 -         * @see Connection
 -         * @see Supplier
++         * @see #connection(Connection)
+          */
+         public Builder connectionSupplier(Supplier<Connection> 
connectionSupplier){
 -            transactionDescriptor.customConnectionSupplier = 
connectionSupplier;
++            transactionDescriptor.connectionSupplier = connectionSupplier;
+             return this;
+         }
+ 
+         /**
+          * @param propagation transaction propagation behaviour
+          * @see TransactionPropagation
+          */
+         public Builder propagation(TransactionPropagation propagation) {
+             transactionDescriptor.propagation = propagation;
+             return this;
+         }
+ 
+         public TransactionDescriptor build() {
+             return transactionDescriptor;
+         }
+     }
+ 
  }
diff --cc 
cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java
index dc230e1,b891f6b..14b55ad
--- 
a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java
+++ 
b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java
@@@ -33,4 -33,8 +33,20 @@@ public interface TransactionListener 
      void willRollback(Transaction tx);
  
      void willAddConnection(Transaction tx, String connectionName, Connection 
connection);
+ 
++    /**
++     * This method could be used to decorate or substitute
++     * new connection initiated inside a Cayenne transaction.
++     * <br/>
++     * The default implementation returns the same connection.
++     *
++     * @param tx transaction that initiated connection
++     * @param connection connection (it could be decorated by other listeners)
++     * @return connection
++     *
++     * @since 4.2
++     */
+     default Connection decorateConnection(Transaction tx, Connection 
connection){
+         return connection;
+     }
  }
diff --cc 
cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionCustomConnectionIT.java
index 0000000,90f2397..7c73a38
mode 000000,100644..100644
--- 
a/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionCustomConnectionIT.java
+++ 
b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionCustomConnectionIT.java
@@@ -1,0 -1,226 +1,226 @@@
+ /*****************************************************************
+  *   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
+  *
+  *    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.apache.cayenne.tx;
+ 
+ import org.apache.cayenne.access.DataContext;
+ import org.apache.cayenne.configuration.server.ServerRuntime;
+ import org.apache.cayenne.di.Inject;
+ import org.apache.cayenne.log.JdbcEventLogger;
+ import org.apache.cayenne.query.ObjectSelect;
+ 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.Before;
+ import org.junit.Test;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ 
+ import java.sql.Connection;
+ import java.sql.SQLException;
+ import java.util.ArrayList;
+ import java.util.List;
+ 
+ import static org.junit.Assert.*;
+ import static org.mockito.ArgumentMatchers.any;
+ import static org.mockito.Mockito.*;
+ 
+ /**
+  * Check if connection can be decorated with listeners and that default 
connection of TransactionDescriptor
+  * has major priority
+  *
+  * @see BaseTransaction,TransactionDescriptor
+  * @since 4.2
+  */
+ @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+ public class TransactionCustomConnectionIT extends ServerCase {
+ 
+     private final Logger logger = 
LoggerFactory.getLogger(TransactionIsolationIT.class);
+ 
+     @Inject
+     DataContext context;
+ 
+     @Inject
+     ServerRuntime runtime;
+ 
+     @Inject
+     private JdbcEventLogger jdbcEventLogger;
+ 
+     TransactionManager manager;
+     private static boolean firstReadonlyCondition;
+ 
+     @Before
+     public void initTransactionManager() {
+         // no binding in test container, get it from runtime
+         manager = runtime.getInjector().getInstance(TransactionManager.class);
+     }
+ 
+     /**
+      * Test depends on decoration of readonly property of the connection, but 
not every driver supports readonly setting.
+      * So this test calculated for drivers with current support, but if it is 
not supported, this test mustn't fail
+      * because it checks if readonly wasn't changed by setter and changes 
firstReadonlyCondition flag value
+      * if it is true to avoid fails. In that case test is useless, but it's 
behavior in other cases can submit
+      * right behavior of methods
+      */
+     @Test
+     public void testConnectionDecorationWithListeners() {
+         Transaction t = new CayenneTransaction(jdbcEventLogger);
+         //add listeners which will check if connection object will be changed 
after every decorate call
+         List<TransactionListener> listeners = 
addAndGetListenersWithCustomReadonlyTo(t);
+         BaseTransaction.bindThreadTransaction(t);
+         try {
+             ObjectSelect.query(Artist.class).select(context);
+ 
+             //check if the last listener set readonly property to false
+             t.getConnections().forEach((key, connection) -> {
+                 try {
+                     assertEquals(connection.isReadOnly(), 
firstReadonlyCondition);
+                 } catch (SQLException throwables) {
+                     throwables.printStackTrace();
+                 }
+             });
+ 
+             //check if every decoration from listener was called
+             for (TransactionListener transactionListener : listeners) {
+                 verify(transactionListener).decorateConnection(any(), any());
+             }
+         } finally {
+             BaseTransaction.bindThreadTransaction(null);
+             t.commit();
+         }
+     }
+ 
+     private List<TransactionListener> 
addAndGetListenersWithCustomReadonlyTo(Transaction t) {
+         Class<?>[] classes = new 
Class[]{ListenerWithFirstReadonlyDecorator.class, 
ListenerWithSecondReadonlyDecorator.class};
+         List<TransactionListener> listeners = new ArrayList<>();
+         for (Class<?> aClass : classes) {
+             TransactionListener listener = (TransactionListener) mock(aClass);
+             listeners.add(listener);
+             when(listener.decorateConnection(any(), 
any())).thenCallRealMethod();
+             t.addListener(listener);
+         }
+         return listeners;
+     }
+ 
+     //listener, which will check if readonly property of connection is false 
and set it to true
+     class ListenerWithFirstReadonlyDecorator implements TransactionListener {
+ 
+         @Override
+         public void willCommit(Transaction tx) {
+ 
+         }
+ 
+         @Override
+         public void willRollback(Transaction tx) {
+ 
+         }
+ 
+         @Override
+         public void willAddConnection(Transaction tx, String connectionName, 
Connection connection) {
+ 
+         }
+ 
+         @Override
+         public Connection decorateConnection(Transaction tx, Connection 
connection) {
+             try {
+                 firstReadonlyCondition = connection.isReadOnly();
+                 connection.setReadOnly(!firstReadonlyCondition);
+ 
+                 if (connection.isReadOnly() == firstReadonlyCondition) {
+                     firstReadonlyCondition = !connection.isReadOnly();
+                 }
+             } catch (SQLException throwables) {
+                 throwables.printStackTrace();
+             }
+             return connection;
+         }
+     }
+ 
+     //listener, which will check if readonly property of connection is true 
and set it to false
+     class ListenerWithSecondReadonlyDecorator implements TransactionListener {
+ 
+         @Override
+         public void willCommit(Transaction tx) {
+ 
+         }
+ 
+         @Override
+         public void willRollback(Transaction tx) {
+ 
+         }
+ 
+         @Override
+         public void willAddConnection(Transaction tx, String connectionName, 
Connection connection) {
+ 
+         }
+ 
+         @Override
+         public Connection decorateConnection(Transaction tx, Connection 
connection) {
+             try {
+                 assertEquals(!firstReadonlyCondition, 
connection.isReadOnly());
+                 connection.setReadOnly(!connection.isReadOnly());
+                 if (connection.isReadOnly() == !firstReadonlyCondition) {
+                     firstReadonlyCondition = !firstReadonlyCondition;
+                 }
+             } catch (SQLException throwables) {
+                 throwables.printStackTrace();
+             }
+             return connection;
+         }
+     }
+ 
+     @Test
+     public void testDefaultConnectionInDescriptor() {
+         Transaction t = new CayenneTransaction(jdbcEventLogger);
+         BaseTransaction.bindThreadTransaction(t);
+         try {
+             ObjectSelect.query(Artist.class).select(context);
+             Connection connection = 
t.getConnections().values().stream().findFirst().get();
+ 
+             try {
+                 connection.setAutoCommit(true);
+             } catch (SQLException throwables) {
+                 throwables.printStackTrace();
+             }
+ 
+             TransactionDescriptor mockDescriptor = 
mock(TransactionDescriptor.class);
 -            when(mockDescriptor.getCustomConnectionSupplier()).thenReturn(() 
-> connection);
++            when(mockDescriptor.getConnectionSupplier()).thenReturn(() -> 
connection);
+             
when(mockDescriptor.getPropagation()).thenReturn(TransactionPropagation.REQUIRES_NEW);
+             
when(mockDescriptor.getIsolation()).thenReturn(Connection.TRANSACTION_SERIALIZABLE);
+ 
+             performInTransaction(mockDescriptor);
 -            verify(mockDescriptor, times(2)).getCustomConnectionSupplier();
++            verify(mockDescriptor, times(2)).getConnectionSupplier();
+         } finally {
+             BaseTransaction.bindThreadTransaction(null);
+             t.commit();
+         }
+ 
+     }
+ 
+     private void performInTransaction(TransactionDescriptor descriptor) {
+         Artist artist = context.newObject(Artist.class);
+         artist.setArtistName("test");
+         manager.performInTransaction(() -> {
+             artist.setArtistName("test3");
+             context.commitChanges();
+             return null;
+         }, descriptor);
+     }
+ }

Reply via email to