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  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 int validationQueryTimeoutSeconds;
53      private final boolean rollbackAfterValidation;
54      private ObjectPool<PooledConnectionAndInfo> pool;
55      private UserPassKey userPassKey;
56      private Duration maxConnLifetime = 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 validationQueryTimeoutSeconds
78       *            Timeout in seconds 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.4.0
86       */
87      public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
88              final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation, final String userName,
89          final char[] userPassword) {
90          this.cpds = cpds;
91          this.validationQuery = validationQuery;
92          this.validationQueryTimeoutSeconds = validationQueryTimeoutSeconds;
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 validationQueryTimeoutSeconds
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      */
115     public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
116             final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation, final String userName,
117         final String userPassword) {
118         this(cpds, validationQuery, validationQueryTimeoutSeconds, rollbackAfterValidation, userName,
119             Utils.toCharArray(userPassword));
120     }
121 
122     @Override
123     public void activateObject(final PooledObject<PooledConnectionAndInfo> p) throws Exception {
124         validateLifetime(p);
125     }
126 
127     /**
128      * Verifies that the user name matches the user whose connections are being managed by this factory and closes the
129      * pool if this is the case; otherwise does nothing.
130      */
131     @Override
132     public void closePool(final String userName) throws SQLException {
133         synchronized (this) {
134             if (userName == null || !userName.equals(this.userPassKey.getUserName())) {
135                 return;
136             }
137         }
138         try {
139             pool.close();
140         } catch (final Exception ex) {
141             throw new SQLException("Error closing connection pool", ex);
142         }
143     }
144 
145     /**
146      * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the
147      * user calls the close() method of this connection object. What we need to do here is to release this
148      * PooledConnection from our pool...
149      */
150     @Override
151     public void connectionClosed(final ConnectionEvent event) {
152         final PooledConnection pc = (PooledConnection) event.getSource();
153         // if this event occurred because we were validating, ignore it
154         // otherwise return the connection to the pool.
155         if (!validatingSet.contains(pc)) {
156             final PooledConnectionAndInfo pci = pcMap.get(pc);
157             if (pci == null) {
158                 throw new IllegalStateException(NO_KEY_MESSAGE);
159             }
160 
161             try {
162                 pool.returnObject(pci);
163             } catch (final Exception e) {
164                 System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL");
165                 pc.removeConnectionEventListener(this);
166                 try {
167                     doDestroyObject(pci);
168                 } catch (final Exception e2) {
169                     System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
170                     e2.printStackTrace();
171                 }
172             }
173         }
174     }
175 
176     /**
177      * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future
178      */
179     @Override
180     public void connectionErrorOccurred(final ConnectionEvent event) {
181         final PooledConnection pc = (PooledConnection) event.getSource();
182         if (null != event.getSQLException()) {
183             System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")");
184         }
185         pc.removeConnectionEventListener(this);
186 
187         final PooledConnectionAndInfo pci = pcMap.get(pc);
188         if (pci == null) {
189             throw new IllegalStateException(NO_KEY_MESSAGE);
190         }
191         try {
192             pool.invalidateObject(pci);
193         } catch (final Exception e) {
194             System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
195             e.printStackTrace();
196         }
197     }
198 
199     /**
200      * Closes the PooledConnection and stops listening for events from it.
201      */
202     @Override
203     public void destroyObject(final PooledObject<PooledConnectionAndInfo> p) throws Exception {
204         doDestroyObject(p.getObject());
205     }
206 
207     private void doDestroyObject(final PooledConnectionAndInfo pci) throws Exception {
208         final PooledConnection pc = pci.getPooledConnection();
209         pc.removeConnectionEventListener(this);
210         pcMap.remove(pc);
211         pc.close();
212     }
213 
214     /**
215      * (Testing API) Gets the value of password for the default user.
216      *
217      * @return value of password.
218      */
219     char[] getPasswordCharArray() {
220         return userPassKey.getPasswordCharArray();
221     }
222 
223     /**
224      * Returns the object pool used to pool connections created by this factory.
225      *
226      * @return ObjectPool managing pooled connections
227      */
228     public ObjectPool<PooledConnectionAndInfo> getPool() {
229         return pool;
230     }
231 
232     /**
233      * Invalidates the PooledConnection in the pool. The CPDSConnectionFactory closes the connection and pool counters
234      * are updated appropriately. Also closes the pool. This ensures that all idle connections are closed and
235      * connections that are checked out are closed on return.
236      */
237     @Override
238     public void invalidate(final PooledConnection pc) throws SQLException {
239         final PooledConnectionAndInfo pci = pcMap.get(pc);
240         if (pci == null) {
241             throw new IllegalStateException(NO_KEY_MESSAGE);
242         }
243         try {
244             pool.invalidateObject(pci); // Destroy instance and update pool counters
245             pool.close(); // Clear any other instances in this pool and kill others as they come back
246         } catch (final Exception ex) {
247             throw new SQLException("Error invalidating connection", ex);
248         }
249     }
250 
251     // ***********************************************************************
252     // java.sql.ConnectionEventListener implementation
253     // ***********************************************************************
254 
255     @Override
256     public synchronized PooledObject<PooledConnectionAndInfo> makeObject() {
257         final PooledConnectionAndInfo pci;
258         try {
259             PooledConnection pc = null;
260             if (userPassKey.getUserName() == null) {
261                 pc = cpds.getPooledConnection();
262             } else {
263                 pc = cpds.getPooledConnection(userPassKey.getUserName(), userPassKey.getPassword());
264             }
265 
266             if (pc == null) {
267                 throw new IllegalStateException("Connection pool data source returned null from getPooledConnection");
268             }
269 
270             // should we add this object as a listener or the pool.
271             // consider the validateObject method in decision
272             pc.addConnectionEventListener(this);
273             pci = new PooledConnectionAndInfo(pc, userPassKey);
274             pcMap.put(pc, pci);
275         } catch (final SQLException e) {
276             throw new RuntimeException(e.getMessage());
277         }
278         return new DefaultPooledObject<>(pci);
279     }
280 
281     @Override
282     public void passivateObject(final PooledObject<PooledConnectionAndInfo> p) throws Exception {
283         validateLifetime(p);
284     }
285 
286     // ***********************************************************************
287     // PooledConnectionManager implementation
288     // ***********************************************************************
289 
290     /**
291      * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation,
292      * passivation and validation.
293      *
294      * @param maxConnLifetime
295      *            A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds.
296      * @since 2.9.0
297      */
298     public void setMaxConnLifetime(final Duration maxConnLifetime) {
299         this.maxConnLifetime = maxConnLifetime;
300     }
301 
302     /**
303      * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation,
304      * passivation and validation.
305      *
306      * @param maxConnLifetimeMillis
307      *            A value of zero or less indicates an infinite lifetime. The default value is -1.
308      * @deprecated Use {@link #setMaxConnLifetime(Duration)}.
309      */
310     @Deprecated
311     public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) {
312         setMaxConnLifetime(Duration.ofMillis(maxConnLifetimeMillis));
313     }
314 
315     /**
316      * Sets the database password used when creating new connections.
317      *
318      * @param userPassword
319      *            new password
320      */
321     public synchronized void setPassword(final char[] userPassword) {
322         this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword);
323     }
324 
325     /**
326      * Sets the database password used when creating new connections.
327      *
328      * @param userPassword
329      *            new password
330      */
331     @Override
332     public synchronized void setPassword(final String userPassword) {
333         this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword);
334     }
335 
336     /**
337      *
338      * @param pool
339      *            the {@link ObjectPool} in which to pool those {@link Connection}s
340      */
341     public void setPool(final ObjectPool<PooledConnectionAndInfo> pool) {
342         this.pool = pool;
343     }
344 
345     /**
346      * @since 2.6.0
347      */
348     @Override
349     public synchronized String toString() {
350         final StringBuilder builder = new StringBuilder(super.toString());
351         builder.append("[cpds=");
352         builder.append(cpds);
353         builder.append(", validationQuery=");
354         builder.append(validationQuery);
355         builder.append(", validationQueryTimeoutSeconds=");
356         builder.append(validationQueryTimeoutSeconds);
357         builder.append(", rollbackAfterValidation=");
358         builder.append(rollbackAfterValidation);
359         builder.append(", pool=");
360         builder.append(pool);
361         builder.append(", maxConnLifetimeMillis=");
362         builder.append(maxConnLifetime);
363         builder.append(", validatingSet=");
364         builder.append(validatingSet);
365         builder.append(", pcMap=");
366         builder.append(pcMap);
367         builder.append("]");
368         return builder.toString();
369     }
370 
371     private void validateLifetime(final PooledObject<PooledConnectionAndInfo> pooledObject) throws Exception {
372         if (maxConnLifetime.compareTo(Duration.ZERO) > 0) {
373             final long lifetimeMillis = System.currentTimeMillis() - pooledObject.getCreateTime();
374             if (lifetimeMillis > maxConnLifetime.toMillis()) {
375                 throw new Exception(
376                     Utils.getMessage("connectionFactory.lifetimeExceeded", lifetimeMillis, maxConnLifetime));
377             }
378         }
379     }
380 
381     @Override
382     public boolean validateObject(final PooledObject<PooledConnectionAndInfo> p) {
383         try {
384             validateLifetime(p);
385         } catch (final Exception e) {
386             return false;
387         }
388         boolean valid = false;
389         final PooledConnection pconn = p.getObject().getPooledConnection();
390         Connection conn = null;
391         validatingSet.add(pconn);
392         if (null == validationQuery) {
393             int timeoutSeconds = validationQueryTimeoutSeconds;
394             if (timeoutSeconds < 0) {
395                 timeoutSeconds = 0;
396             }
397             try {
398                 conn = pconn.getConnection();
399                 valid = conn.isValid(timeoutSeconds);
400             } catch (final SQLException e) {
401                 valid = false;
402             } finally {
403                 Utils.closeQuietly(conn);
404                 validatingSet.remove(pconn);
405             }
406         } else {
407             Statement stmt = null;
408             ResultSet rset = null;
409             // logical Connection from the PooledConnection must be closed
410             // before another one can be requested and closing it will
411             // generate an event. Keep track so we know not to return
412             // the PooledConnection
413             validatingSet.add(pconn);
414             try {
415                 conn = pconn.getConnection();
416                 stmt = conn.createStatement();
417                 rset = stmt.executeQuery(validationQuery);
418                 valid = rset.next();
419                 if (rollbackAfterValidation) {
420                     conn.rollback();
421                 }
422             } catch (final Exception e) {
423                 valid = false;
424             } finally {
425                 Utils.closeQuietly(rset);
426                 Utils.closeQuietly(stmt);
427                 Utils.closeQuietly(conn);
428                 validatingSet.remove(pconn);
429             }
430         }
431         return valid;
432     }
433 }