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.PoolableConnection;
29  import org.apache.commons.pool2.KeyedObjectPool;
30  import org.apache.commons.pool2.KeyedPooledObjectFactory;
31  import org.apache.commons.pool2.PooledObject;
32  import org.apache.commons.pool2.impl.DefaultPooledObject;
33  
34  /**
35   * A {@link KeyedPooledObjectFactory} that creates {@link PoolableConnection}s.
36   *
37   * @since 2.0
38   */
39  final class KeyedCPDSConnectionFactory extends AbstractConnectionFactory
40          implements KeyedPooledObjectFactory<UserPassKey, 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      private KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> pool;
44  
45      /**
46       * Creates a new {@code KeyedCPDSConnectionFactory}.
47       *
48       * @param cpds
49       *            the ConnectionPoolDataSource from which to obtain PooledConnections
50       * @param validationQuery
51       *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
52       *            row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate
53       *            connections.
54       * @param validationQueryTimeoutDuration
55       *            The Duration to allow for the validation query to complete
56       * @param rollbackAfterValidation
57       *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
58       * @since 2.10.0
59       */
60      public KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final Duration validationQueryTimeoutDuration,
61              final boolean rollbackAfterValidation) {
62          super(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation);
63      }
64  
65      @Override
66      public void activateObject(final UserPassKey ignored, final PooledObject<PooledConnectionAndInfo> pooledObject) throws SQLException {
67          validateLifetime(pooledObject);
68      }
69  
70      /**
71       * This implementation does not fully close the KeyedObjectPool, as this would affect all users. Instead, it clears
72       * the pool associated with the given user. This method is not currently used.
73       */
74      @Override
75      public void closePool(final String userName) throws SQLException {
76          try {
77              pool.clear(new UserPassKey(userName));
78          } catch (final Exception ex) {
79              throw new SQLException("Error closing connection pool", ex);
80          }
81      }
82  
83      /**
84       * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the
85       * user calls the close() method of this connection object. What we need to do here is to release this
86       * PooledConnection from our pool...
87       */
88      @Override
89      public void connectionClosed(final ConnectionEvent event) {
90          final PooledConnection pc = (PooledConnection) event.getSource();
91          // if this event occurred because we were validating, or if this
92          // connection has been marked for removal, ignore it
93          // otherwise return the connection to the pool.
94          if (!validatingSet.contains(pc)) {
95              final PooledConnectionAndInfo pci = pcMap.get(pc);
96              if (pci == null) {
97                  throw new IllegalStateException(NO_KEY_MESSAGE);
98              }
99              try {
100                 pool.returnObject(pci.getUserPassKey(), pci);
101             } catch (final Exception e) {
102                 System.err.println("CLOSING DOWN CONNECTION AS IT COULD NOT BE RETURNED TO THE POOL");
103                 pc.removeConnectionEventListener(this);
104                 try {
105                     pool.invalidateObject(pci.getUserPassKey(), pci);
106                 } catch (final Exception e3) {
107                     System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
108                     e3.printStackTrace();
109                 }
110             }
111         }
112     }
113 
114     /**
115      * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future
116      */
117     @Override
118     public void connectionErrorOccurred(final ConnectionEvent event) {
119         final PooledConnection pc = (PooledConnection) event.getSource();
120         if (null != event.getSQLException()) {
121             System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")");
122         }
123         pc.removeConnectionEventListener(this);
124 
125         final PooledConnectionAndInfo info = pcMap.get(pc);
126         if (info == null) {
127             throw new IllegalStateException(NO_KEY_MESSAGE);
128         }
129         try {
130             pool.invalidateObject(info.getUserPassKey(), info);
131         } catch (final Exception e) {
132             System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + info);
133             e.printStackTrace();
134         }
135     }
136 
137     /**
138      * Closes the PooledConnection and stops listening for events from it.
139      */
140     @Override
141     public void destroyObject(final UserPassKey ignored, final PooledObject<PooledConnectionAndInfo> pooledObject) throws SQLException {
142         final PooledConnection pooledConnection = pooledObject.getObject().getPooledConnection();
143         pooledConnection.removeConnectionEventListener(this);
144         pcMap.remove(pooledConnection);
145         pooledConnection.close();
146     }
147 
148     /**
149      * Returns the keyed object pool used to pool connections created by this factory.
150      *
151      * @return KeyedObjectPool managing pooled connections
152      */
153     public KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> getPool() {
154         return pool;
155     }
156 
157     /**
158      * Invalidates the PooledConnection in the pool. The KeyedCPDSConnectionFactory closes the connection and pool
159      * counters are updated appropriately. Also clears any idle instances associated with the user name that was used to
160      * create the PooledConnection. Connections associated with this user are not affected, and they will not be
161      * automatically closed on return to the pool.
162      */
163     @Override
164     public void invalidate(final PooledConnection pc) throws SQLException {
165         final PooledConnectionAndInfo info = pcMap.get(pc);
166         if (info == null) {
167             throw new IllegalStateException(NO_KEY_MESSAGE);
168         }
169         final UserPassKey key = info.getUserPassKey();
170         try {
171             pool.invalidateObject(key, info); // Destroy and update pool counters
172             pool.clear(key); // Remove any idle instances with this key
173         } catch (final Exception ex) {
174             throw new SQLException("Error invalidating connection", ex);
175         }
176     }
177 
178     /**
179      * Creates a new {@code PooledConnectionAndInfo} from the given {@code UserPassKey}.
180      *
181      * @param userPassKey
182      *            {@code UserPassKey} containing user credentials
183      * @throws SQLException
184      *             if the connection could not be created.
185      * @see org.apache.commons.pool2.KeyedPooledObjectFactory#makeObject(Object)
186      */
187     @Override
188     public synchronized PooledObject<PooledConnectionAndInfo> makeObject(final UserPassKey userPassKey) throws SQLException {
189         PooledConnection pooledConnection = null;
190         final String userName = userPassKey.getUserName();
191         final String password = userPassKey.getPassword();
192         if (userName == null) {
193             pooledConnection = cpds.getPooledConnection();
194         } else {
195             pooledConnection = cpds.getPooledConnection(userName, password);
196         }
197         if (pooledConnection == null) {
198             throw new IllegalStateException("Connection pool data source returned null from getPooledConnection");
199         }
200         // should we add this object as a listener or the pool.
201         // consider the validateObject method in decision
202         pooledConnection.addConnectionEventListener(this);
203         final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pooledConnection, userPassKey);
204         pcMap.put(pooledConnection, pci);
205         return new DefaultPooledObject<>(pci);
206     }
207 
208     @Override
209     public void passivateObject(final UserPassKey ignored, final PooledObject<PooledConnectionAndInfo> pooledObject) throws SQLException {
210         validateLifetime(pooledObject);
211     }
212 
213     /**
214      * Does nothing. This factory does not cache user credentials.
215      */
216     @Override
217     public void setPassword(final char[] password) {
218         // Does nothing. This factory does not cache user credentials.
219     }
220 
221     /**
222      * Does nothing. This factory does not cache user credentials.
223      */
224     @Override
225     public void setPassword(final String password) {
226         // Does nothing. This factory does not cache user credentials.
227     }
228 
229     public void setPool(final KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> pool) {
230         this.pool = pool;
231     }
232 
233     /**
234      * Validates a pooled connection.
235      * <p>
236      * A query validation timeout greater than 0 and less than 1 second is converted to 1 second.
237      * </p>
238      *
239      * @param ignored
240      *            ignored
241      * @param pooledObject
242      *            wrapped {@code PooledConnectionAndInfo} containing the connection to validate
243      * @return true if validation succeeds
244      * @throws ArithmeticException if the query validation timeout does not fit as seconds in an int.
245      */
246     @Override
247     public boolean validateObject(final UserPassKey ignored, final PooledObject<PooledConnectionAndInfo> pooledObject) {
248         return super.validateObject(pooledObject);
249     }
250 }