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    *      https://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.SQLException;
21  import java.time.Duration;
22  
23  import javax.sql.ConnectionEvent;
24  import javax.sql.ConnectionEventListener;
25  import javax.sql.ConnectionPoolDataSource;
26  import javax.sql.PooledConnection;
27  
28  import org.apache.commons.dbcp2.PoolableConnectionFactory;
29  import org.apache.commons.pool2.ObjectPool;
30  import org.apache.commons.pool2.PooledObject;
31  import org.apache.commons.pool2.PooledObjectFactory;
32  import org.apache.commons.pool2.impl.DefaultPooledObject;
33  
34  /**
35   * A {@link PooledObjectFactory} that creates {@link org.apache.commons.dbcp2.PoolableConnection PoolableConnection}s.
36   *
37   * @since 2.0
38   */
39  final class CPDSConnectionFactory extends AbstractConnectionFactory
40          implements PooledObjectFactory<PooledConnectionAndInfo>, ConnectionEventListener, PooledConnectionManager {
41  
42      private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but I have no record of the underlying PooledConnection.";
43  
44      private ObjectPool<PooledConnectionAndInfo> pool;
45      private UserPassKey userPassKey;
46  
47      /**
48       * Creates a new {@link PoolableConnectionFactory}.
49       *
50       * @param cpds
51       *            the ConnectionPoolDataSource from which to obtain PooledConnection's
52       * @param validationQuery
53       *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
54       *            row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate
55       *            connections.
56       * @param validationQueryTimeoutDuration
57       *            Timeout Duration before validation fails
58       * @param rollbackAfterValidation
59       *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
60       * @param userName
61       *            The user name to use to create connections
62       * @param userPassword
63       *            The password to use to create connections
64       * @since 2.10.0
65       */
66      public CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
67              final Duration validationQueryTimeoutDuration, final boolean rollbackAfterValidation, final String userName,
68              final char[] userPassword) {
69          super(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation);
70          this.userPassKey = new UserPassKey(userName, userPassword);
71      }
72  
73      @Override
74      public void activateObject(final PooledObject<PooledConnectionAndInfo> pooledObject) throws SQLException {
75          validateLifetime(pooledObject);
76      }
77  
78      /**
79       * Verifies that the user name matches the user whose connections are being managed by this factory and closes the
80       * pool if this is the case; otherwise does nothing.
81       */
82      @Override
83      public void closePool(final String userName) throws SQLException {
84          synchronized (this) {
85              if (userName == null || !userName.equals(this.userPassKey.getUserName())) {
86                  return;
87              }
88          }
89          try {
90              pool.close();
91          } catch (final Exception ex) {
92              throw new SQLException("Error closing connection pool", ex);
93          }
94      }
95  
96      /**
97       * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the
98       * user calls the close() method of this connection object. What we need to do here is to release this
99       * PooledConnection from our pool...
100      */
101     @Override
102     public void connectionClosed(final ConnectionEvent event) {
103         final PooledConnection pc = (PooledConnection) event.getSource();
104         // if this event occurred because we were validating, ignore it
105         // otherwise return the connection to the pool.
106         if (!validatingSet.contains(pc)) {
107             final PooledConnectionAndInfo pci = pcMap.get(pc);
108             if (pci == null) {
109                 throw new IllegalStateException(NO_KEY_MESSAGE);
110             }
111 
112             try {
113                 pool.returnObject(pci);
114             } catch (final Exception e) {
115                 System.err.println("CLOSING DOWN CONNECTION AS IT COULD NOT BE RETURNED TO THE POOL");
116                 pc.removeConnectionEventListener(this);
117                 try {
118                     doDestroyObject(pci);
119                 } catch (final Exception e2) {
120                     System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
121                     e2.printStackTrace();
122                 }
123             }
124         }
125     }
126 
127     /**
128      * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future
129      */
130     @Override
131     public void connectionErrorOccurred(final ConnectionEvent event) {
132         final PooledConnection pc = (PooledConnection) event.getSource();
133         if (null != event.getSQLException()) {
134             System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")");
135         }
136         pc.removeConnectionEventListener(this);
137 
138         final PooledConnectionAndInfo pci = pcMap.get(pc);
139         if (pci == null) {
140             throw new IllegalStateException(NO_KEY_MESSAGE);
141         }
142         try {
143             pool.invalidateObject(pci);
144         } catch (final Exception e) {
145             System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
146             e.printStackTrace();
147         }
148     }
149 
150     /**
151      * Closes the PooledConnection and stops listening for events from it.
152      */
153     @Override
154     public void destroyObject(final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
155         doDestroyObject(p.getObject());
156     }
157 
158     private void doDestroyObject(final PooledConnectionAndInfo pci) throws SQLException {
159         final PooledConnection pc = pci.getPooledConnection();
160         pc.removeConnectionEventListener(this);
161         pcMap.remove(pc);
162         pc.close();
163     }
164 
165     /**
166      * (Testing API) Gets the value of password for the default user.
167      *
168      * @return value of password.
169      */
170     char[] getPasswordCharArray() {
171         return userPassKey.getPasswordCharArray();
172     }
173 
174     /**
175      * Returns the object pool used to pool connections created by this factory.
176      *
177      * @return ObjectPool managing pooled connections
178      */
179     public ObjectPool<PooledConnectionAndInfo> getPool() {
180         return pool;
181     }
182 
183     /**
184      * Invalidates the PooledConnection in the pool. The CPDSConnectionFactory closes the connection and pool counters
185      * are updated appropriately. Also closes the pool. This ensures that all idle connections are closed and
186      * connections that are checked out are closed on return.
187      */
188     @Override
189     public void invalidate(final PooledConnection pc) throws SQLException {
190         final PooledConnectionAndInfo pci = pcMap.get(pc);
191         if (pci == null) {
192             throw new IllegalStateException(NO_KEY_MESSAGE);
193         }
194         try {
195             pool.close(); // Clear any other instances in this pool and kill others as they come back
196             // Calling close before invalidate ensures that invalidate will not trigger a create attempt
197             pool.invalidateObject(pci); // Destroy instance and update pool counters
198         } catch (final Exception ex) {
199             throw new SQLException("Error invalidating connection", ex);
200         }
201     }
202 
203     @Override
204     public synchronized PooledObject<PooledConnectionAndInfo> makeObject() throws SQLException {
205         PooledConnection pc = null;
206         if (userPassKey.getUserName() == null) {
207             pc = cpds.getPooledConnection();
208         } else {
209             pc = cpds.getPooledConnection(userPassKey.getUserName(), userPassKey.getPassword());
210         }
211         if (pc == null) {
212             throw new IllegalStateException("Connection pool data source returned null from getPooledConnection");
213         }
214         // should we add this object as a listener or the pool.
215         // consider the validateObject method in decision
216         pc.addConnectionEventListener(this);
217         final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pc, userPassKey);
218         pcMap.put(pc, pci);
219         return new DefaultPooledObject<>(pci);
220     }
221 
222     @Override
223     public void passivateObject(final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
224         validateLifetime(p);
225     }
226 
227     /**
228      * Sets the database password used when creating new connections.
229      *
230      * @param userPassword
231      *            new password
232      */
233     @Override
234     public synchronized void setPassword(final char[] userPassword) {
235         this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword);
236     }
237 
238     /**
239      * Sets the database password used when creating new connections.
240      *
241      * @param userPassword
242      *            new password
243      */
244     @Override
245     public synchronized void setPassword(final String userPassword) {
246         this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword);
247     }
248 
249     /**
250      *
251      * @param pool
252      *            the {@link ObjectPool} in which to pool those {@link Connection}s
253      */
254     public void setPool(final ObjectPool<PooledConnectionAndInfo> pool) {
255         this.pool = pool;
256     }
257 
258     /**
259      * @since 2.6.0
260      */
261     @Override
262     public synchronized String toString() {
263         final StringBuilder builder = new StringBuilder(super.toString());
264         builder.append("[cpds=");
265         builder.append(cpds);
266         builder.append(", validationQuery=");
267         builder.append(validationQuery);
268         builder.append(", validationQueryTimeoutDuration=");
269         builder.append(validationQueryTimeoutDuration);
270         builder.append(", rollbackAfterValidation=");
271         builder.append(rollbackAfterValidation);
272         builder.append(", pool=");
273         builder.append(pool);
274         builder.append(", maxConnDuration=");
275         builder.append(maxConnDuration);
276         builder.append(", validatingSet=");
277         builder.append(validatingSet);
278         builder.append(", pcMap=");
279         builder.append(pcMap);
280         builder.append("]");
281         return builder.toString();
282     }
283 }