View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.dbcp2.datasources;
18  
19  import java.sql.Connection;
20  import java.sql.ResultSet;
21  import java.sql.SQLException;
22  import java.sql.Statement;
23  import java.time.Duration;
24  import java.util.Collections;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.concurrent.ConcurrentHashMap;
28  
29  import javax.sql.ConnectionEvent;
30  import javax.sql.ConnectionEventListener;
31  import javax.sql.ConnectionPoolDataSource;
32  import javax.sql.PooledConnection;
33  
34  import org.apache.commons.dbcp2.Utils;
35  import org.apache.commons.pool2.ObjectPool;
36  import org.apache.commons.pool2.PooledObject;
37  import org.apache.commons.pool2.PooledObjectFactory;
38  import org.apache.commons.pool2.impl.DefaultPooledObject;
39  
40  /**
41   * A {@link PooledObjectFactory} that creates {@link org.apache.commons.dbcp2.PoolableConnection PoolableConnection}s.
42   *
43   * @since 2.0
44   */
45  final class CPDSConnectionFactory
46          implements PooledObjectFactory<PooledConnectionAndInfo>, ConnectionEventListener, PooledConnectionManager {
47  
48      private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but I have no record of the underlying PooledConnection.";
49  
50      private final ConnectionPoolDataSource cpds;
51      private final String validationQuery;
52      private final Duration validationQueryTimeoutDuration;
53      private final boolean rollbackAfterValidation;
54      private ObjectPool<PooledConnectionAndInfo> pool;
55      private UserPassKey userPassKey;
56      private Duration maxConnDuration = Duration.ofMillis(-1);
57  
58      /**
59       * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated.
60       */
61      private final Set<PooledConnection> validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
62  
63      /**
64       * Map of PooledConnectionAndInfo instances
65       */
66      private final Map<PooledConnection, PooledConnectionAndInfo> pcMap = new ConcurrentHashMap<>();
67  
68      /**
69       * Creates a new {@code PoolableConnectionFactory}.
70       *
71       * @param cpds
72       *            the ConnectionPoolDataSource from which to obtain PooledConnection's
73       * @param validationQuery
74       *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
75       *            row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate
76       *            connections.
77       * @param validationQueryTimeoutDuration
78       *            Timeout Duration before validation fails
79       * @param rollbackAfterValidation
80       *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
81       * @param userName
82       *            The user name to use to create connections
83       * @param userPassword
84       *            The password to use to create connections
85       * @since 2.10.0
86       */
87      public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
88              final Duration validationQueryTimeoutDuration, final boolean rollbackAfterValidation, final String userName,
89          final char[] userPassword) {
90          this.cpds = cpds;
91          this.validationQuery = validationQuery;
92          this.validationQueryTimeoutDuration = validationQueryTimeoutDuration;
93          this.userPassKey = new UserPassKey(userName, userPassword);
94          this.rollbackAfterValidation = rollbackAfterValidation;
95      }
96  
97      /**
98       * Creates a new {@code PoolableConnectionFactory}.
99       *
100      * @param cpds
101      *            the ConnectionPoolDataSource from which to obtain PooledConnection's
102      * @param validationQuery
103      *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
104      *            row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate
105      *            connections.
106      * @param validationQueryTimeoutDuration
107      *            Timeout in seconds before validation fails
108      * @param rollbackAfterValidation
109      *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
110      * @param userName
111      *            The user name to use to create connections
112      * @param userPassword
113      *            The password to use to create connections
114      * @since 2.10.0
115      */
116     public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final Duration validationQueryTimeoutDuration,
117         final boolean rollbackAfterValidation, final String userName, final String userPassword) {
118         this(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation, userName, Utils.toCharArray(userPassword));
119     }
120 
121     /**
122      * Creates a new {@code PoolableConnectionFactory}.
123      *
124      * @param cpds
125      *            the ConnectionPoolDataSource from which to obtain PooledConnection's
126      * @param validationQuery
127      *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
128      *            row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate
129      *            connections.
130      * @param validationQueryTimeoutSeconds
131      *            Timeout in seconds before validation fails
132      * @param rollbackAfterValidation
133      *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
134      * @param userName
135      *            The user name to use to create connections
136      * @param userPassword
137      *            The password to use to create connections
138      * @since 2.4.0
139      * @deprecated Use {@link #CPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean, String, char[])}.
140      */
141     @Deprecated
142     public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
143             final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation, final String userName,
144         final char[] userPassword) {
145         this.cpds = cpds;
146         this.validationQuery = validationQuery;
147         this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds);
148         this.userPassKey = new UserPassKey(userName, userPassword);
149         this.rollbackAfterValidation = rollbackAfterValidation;
150     }
151 
152     /**
153      * Creates a new {@code PoolableConnectionFactory}.
154      *
155      * @param cpds
156      *            the ConnectionPoolDataSource from which to obtain PooledConnection's
157      * @param validationQuery
158      *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
159      *            row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate
160      *            connections.
161      * @param validationQueryTimeoutSeconds
162      *            Timeout in seconds before validation fails
163      * @param rollbackAfterValidation
164      *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
165      * @param userName
166      *            The user name to use to create connections
167      * @param userPassword
168      *            The password to use to create connections
169      * @deprecated Use {@link #CPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean, String, String)}.
170      */
171     @Deprecated
172     public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final int validationQueryTimeoutSeconds,
173             final boolean rollbackAfterValidation, final String userName, final String userPassword) {
174         this(cpds, validationQuery, validationQueryTimeoutSeconds, rollbackAfterValidation, userName, Utils.toCharArray(userPassword));
175     }
176 
177     @Override
178     public void activateObject(final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
179         validateLifetime(p);
180     }
181 
182     /**
183      * Verifies that the user name matches the user whose connections are being managed by this factory and closes the
184      * pool if this is the case; otherwise does nothing.
185      */
186     @Override
187     public void closePool(final String userName) throws SQLException {
188         synchronized (this) {
189             if (userName == null || !userName.equals(this.userPassKey.getUserName())) {
190                 return;
191             }
192         }
193         try {
194             pool.close();
195         } catch (final Exception ex) {
196             throw new SQLException("Error closing connection pool", ex);
197         }
198     }
199 
200     /**
201      * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the
202      * user calls the close() method of this connection object. What we need to do here is to release this
203      * PooledConnection from our pool...
204      */
205     @Override
206     public void connectionClosed(final ConnectionEvent event) {
207         final PooledConnection pc = (PooledConnection) event.getSource();
208         // if this event occurred because we were validating, ignore it
209         // otherwise return the connection to the pool.
210         if (!validatingSet.contains(pc)) {
211             final PooledConnectionAndInfo pci = pcMap.get(pc);
212             if (pci == null) {
213                 throw new IllegalStateException(NO_KEY_MESSAGE);
214             }
215 
216             try {
217                 pool.returnObject(pci);
218             } catch (final Exception e) {
219                 System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL");
220                 pc.removeConnectionEventListener(this);
221                 try {
222                     doDestroyObject(pci);
223                 } catch (final Exception e2) {
224                     System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
225                     e2.printStackTrace();
226                 }
227             }
228         }
229     }
230 
231     /**
232      * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future
233      */
234     @Override
235     public void connectionErrorOccurred(final ConnectionEvent event) {
236         final PooledConnection pc = (PooledConnection) event.getSource();
237         if (null != event.getSQLException()) {
238             System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")");
239         }
240         pc.removeConnectionEventListener(this);
241 
242         final PooledConnectionAndInfo pci = pcMap.get(pc);
243         if (pci == null) {
244             throw new IllegalStateException(NO_KEY_MESSAGE);
245         }
246         try {
247             pool.invalidateObject(pci);
248         } catch (final Exception e) {
249             System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
250             e.printStackTrace();
251         }
252     }
253 
254     /**
255      * Closes the PooledConnection and stops listening for events from it.
256      */
257     @Override
258     public void destroyObject(final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
259         doDestroyObject(p.getObject());
260     }
261 
262     private void doDestroyObject(final PooledConnectionAndInfo pci) throws SQLException {
263         final PooledConnection pc = pci.getPooledConnection();
264         pc.removeConnectionEventListener(this);
265         pcMap.remove(pc);
266         pc.close();
267     }
268 
269     /**
270      * (Testing API) Gets the value of password for the default user.
271      *
272      * @return value of password.
273      */
274     char[] getPasswordCharArray() {
275         return userPassKey.getPasswordCharArray();
276     }
277 
278     /**
279      * Returns the object pool used to pool connections created by this factory.
280      *
281      * @return ObjectPool managing pooled connections
282      */
283     public ObjectPool<PooledConnectionAndInfo> getPool() {
284         return pool;
285     }
286 
287     /**
288      * Invalidates the PooledConnection in the pool. The CPDSConnectionFactory closes the connection and pool counters
289      * are updated appropriately. Also closes the pool. This ensures that all idle connections are closed and
290      * connections that are checked out are closed on return.
291      */
292     @Override
293     public void invalidate(final PooledConnection pc) throws SQLException {
294         final PooledConnectionAndInfo pci = pcMap.get(pc);
295         if (pci == null) {
296             throw new IllegalStateException(NO_KEY_MESSAGE);
297         }
298         try {
299             pool.invalidateObject(pci); // Destroy instance and update pool counters
300             pool.close(); // Clear any other instances in this pool and kill others as they come back
301         } catch (final Exception ex) {
302             throw new SQLException("Error invalidating connection", ex);
303         }
304     }
305 
306     @Override
307     public synchronized PooledObject<PooledConnectionAndInfo> makeObject() throws SQLException {
308         PooledConnection pc = null;
309         if (userPassKey.getUserName() == null) {
310             pc = cpds.getPooledConnection();
311         } else {
312             pc = cpds.getPooledConnection(userPassKey.getUserName(), userPassKey.getPassword());
313         }
314         if (pc == null) {
315             throw new IllegalStateException("Connection pool data source returned null from getPooledConnection");
316         }
317         // should we add this object as a listener or the pool.
318         // consider the validateObject method in decision
319         pc.addConnectionEventListener(this);
320         final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pc, userPassKey);
321         pcMap.put(pc, pci);
322         return new DefaultPooledObject<>(pci);
323     }
324 
325     @Override
326     public void passivateObject(final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
327         validateLifetime(p);
328     }
329 
330     /**
331      * Sets the maximum Duration of a connection after which the connection will always fail activation,
332      * passivation and validation.
333      *
334      * @param maxConnDuration
335      *            A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds.
336      * @since 2.10.0
337      */
338     public void setMaxConn(final Duration maxConnDuration) {
339         this.maxConnDuration = maxConnDuration;
340     }
341 
342     /**
343      * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation,
344      * passivation and validation.
345      *
346      * @param maxConnDuration
347      *            A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds.
348      * @since 2.9.0
349      * @deprecated Use {@link #setMaxConn(Duration)}.
350      */
351     @Deprecated
352     public void setMaxConnLifetime(final Duration maxConnDuration) {
353         this.maxConnDuration = maxConnDuration;
354     }
355 
356     /**
357      * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation,
358      * passivation and validation.
359      *
360      * @param maxConnLifetimeMillis
361      *            A value of zero or less indicates an infinite lifetime. The default value is -1.
362      * @deprecated Use {@link #setMaxConn(Duration)}.
363      */
364     @Deprecated
365     public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) {
366         setMaxConnLifetime(Duration.ofMillis(maxConnLifetimeMillis));
367     }
368 
369     /**
370      * Sets the database password used when creating new connections.
371      *
372      * @param userPassword
373      *            new password
374      */
375     public synchronized void setPassword(final char[] userPassword) {
376         this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword);
377     }
378 
379     /**
380      * Sets the database password used when creating new connections.
381      *
382      * @param userPassword
383      *            new password
384      */
385     @Override
386     public synchronized void setPassword(final String userPassword) {
387         this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword);
388     }
389 
390     /**
391      *
392      * @param pool
393      *            the {@link ObjectPool} in which to pool those {@link Connection}s
394      */
395     public void setPool(final ObjectPool<PooledConnectionAndInfo> pool) {
396         this.pool = pool;
397     }
398 
399     /**
400      * @since 2.6.0
401      */
402     @Override
403     public synchronized String toString() {
404         final StringBuilder builder = new StringBuilder(super.toString());
405         builder.append("[cpds=");
406         builder.append(cpds);
407         builder.append(", validationQuery=");
408         builder.append(validationQuery);
409         builder.append(", validationQueryTimeoutDuration=");
410         builder.append(validationQueryTimeoutDuration);
411         builder.append(", rollbackAfterValidation=");
412         builder.append(rollbackAfterValidation);
413         builder.append(", pool=");
414         builder.append(pool);
415         builder.append(", maxConnDuration=");
416         builder.append(maxConnDuration);
417         builder.append(", validatingSet=");
418         builder.append(validatingSet);
419         builder.append(", pcMap=");
420         builder.append(pcMap);
421         builder.append("]");
422         return builder.toString();
423     }
424 
425     private void validateLifetime(final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
426         Utils.validateLifetime(p, maxConnDuration);
427     }
428 
429     @Override
430     public boolean validateObject(final PooledObject<PooledConnectionAndInfo> p) {
431         try {
432             validateLifetime(p);
433         } catch (final Exception e) {
434             return false;
435         }
436         boolean valid = false;
437         final PooledConnection pconn = p.getObject().getPooledConnection();
438         Connection conn = null;
439         validatingSet.add(pconn);
440         if (null == validationQuery) {
441             Duration timeoutDuration = validationQueryTimeoutDuration;
442             if (timeoutDuration.isNegative()) {
443                 timeoutDuration = Duration.ZERO;
444             }
445             try {
446                 conn = pconn.getConnection();
447                 valid = conn.isValid((int) timeoutDuration.getSeconds());
448             } catch (final SQLException e) {
449                 valid = false;
450             } finally {
451                 Utils.closeQuietly((AutoCloseable) conn);
452                 validatingSet.remove(pconn);
453             }
454         } else {
455             Statement stmt = null;
456             ResultSet rset = null;
457             // logical Connection from the PooledConnection must be closed
458             // before another one can be requested and closing it will
459             // generate an event. Keep track so we know not to return
460             // the PooledConnection
461             validatingSet.add(pconn);
462             try {
463                 conn = pconn.getConnection();
464                 stmt = conn.createStatement();
465                 rset = stmt.executeQuery(validationQuery);
466                 valid = rset.next();
467                 if (rollbackAfterValidation) {
468                     conn.rollback();
469                 }
470             } catch (final Exception e) {
471                 valid = false;
472             } finally {
473                 Utils.closeQuietly((AutoCloseable) rset);
474                 Utils.closeQuietly((AutoCloseable) stmt);
475                 Utils.closeQuietly((AutoCloseable) conn);
476                 validatingSet.remove(pconn);
477             }
478         }
479         return valid;
480     }
481 }