View Javadoc
1   /*
2    *
3    * The DbUnit Database Testing Framework
4    * Copyright (C)2002-2026, DbUnit.org
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 2.1 of the License, or (at your option) any later version.
10   *
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, write to the Free Software
18   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19   *
20   */
21  package org.dbunit.junit.jupiter;
22  
23  import java.io.PrintWriter;
24  import java.lang.reflect.InvocationHandler;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Proxy;
27  import java.sql.Connection;
28  import java.sql.DriverManager;
29  import java.sql.SQLException;
30  import java.util.concurrent.atomic.AtomicInteger;
31  import java.util.logging.Logger;
32  
33  import javax.sql.DataSource;
34  
35  import org.dbunit.DatabaseProfile;
36  
37  /**
38   * A test {@link DataSource} that opens real {@link DriverManager} connections against a
39   * {@link DatabaseProfile} and accounts for every one: how many are open at once (peak), and
40   * whether they all get closed. Each connection it hands back is a proxy whose {@code close()}
41   * closes the real connection and frees its slot.
42   *
43   * <p>When constructed with a {@code maxConcurrent} cap, {@code getConnection()} throws rather
44   * than block once that many are already open, so a runtime that tries to hold more connections
45   * than expected fails fast instead of deadlocking the test.
46   *
47   * <p>Sequential use only - no synchronization beyond the atomic counters.
48   */
49  final class CountingDataSource implements DataSource
50  {
51      private final DatabaseProfile profile;
52      private final int maxConcurrent;
53      private final AtomicInteger inUse = new AtomicInteger();
54      private final AtomicInteger peak = new AtomicInteger();
55      private final AtomicInteger opened = new AtomicInteger();
56      private final AtomicInteger closed = new AtomicInteger();
57  
58      CountingDataSource(final DatabaseProfile profile)
59      {
60          this(profile, 0);
61      }
62  
63      CountingDataSource(final DatabaseProfile profile, final int maxConcurrent)
64      {
65          this.profile = profile;
66          this.maxConcurrent = maxConcurrent;
67      }
68  
69      int peakConcurrent()
70      {
71          return peak.get();
72      }
73  
74      int opened()
75      {
76          return opened.get();
77      }
78  
79      int leaked()
80      {
81          return opened.get() - closed.get();
82      }
83  
84      @Override
85      public Connection getConnection() throws SQLException
86      {
87          final int now = inUse.incrementAndGet();
88          if (maxConcurrent > 0 && now > maxConcurrent)
89          {
90              inUse.decrementAndGet();
91              throw new SQLException("CountingDataSource capped at " + maxConcurrent
92                      + " concurrent connections; connection " + now + " was requested while "
93                      + (now - 1) + " were still open");
94          }
95          peak.accumulateAndGet(now, Math::max);
96          final Connection real;
97          try
98          {
99              real = DriverManager.getConnection(profile.getConnectionUrl(), profile.getUser(),
100                     profile.getPassword());
101         }
102         catch (final SQLException e)
103         {
104             inUse.decrementAndGet();
105             throw e;
106         }
107         opened.incrementAndGet();
108         return slotFreeingProxy(real);
109     }
110 
111     private Connection slotFreeingProxy(final Connection real)
112     {
113         final InvocationHandler handler = (proxy, method, args) ->
114         {
115             if ("close".equals(method.getName()) && (args == null || args.length == 0))
116             {
117                 if (!real.isClosed())
118                 {
119                     real.close();
120                     closed.incrementAndGet();
121                     inUse.decrementAndGet();
122                 }
123                 return null;
124             }
125             try
126             {
127                 return method.invoke(real, args);
128             }
129             catch (final InvocationTargetException e)
130             {
131                 throw e.getCause();
132             }
133         };
134         return (Connection) Proxy.newProxyInstance(getClass().getClassLoader(),
135                 new Class<?>[] {Connection.class}, handler);
136     }
137 
138     @Override
139     public Connection getConnection(final String username, final String password)
140             throws SQLException
141     {
142         return getConnection();
143     }
144 
145     @Override
146     public PrintWriter getLogWriter()
147     {
148         return null;
149     }
150 
151     @Override
152     public void setLogWriter(final PrintWriter out)
153     {
154     }
155 
156     @Override
157     public void setLoginTimeout(final int seconds)
158     {
159     }
160 
161     @Override
162     public int getLoginTimeout()
163     {
164         return 0;
165     }
166 
167     @Override
168     public Logger getParentLogger()
169     {
170         return Logger.getLogger("");
171     }
172 
173     @Override
174     public <T> T unwrap(final Class<T> iface)
175     {
176         throw new UnsupportedOperationException();
177     }
178 
179     @Override
180     public boolean isWrapperFor(final Class<?> iface)
181     {
182         return false;
183     }
184 }