View Javadoc
1   /*
2    * #%L
3    * This file is part of a universal JDBC Connection factory.
4    * %%
5    * Copyright (C) 2014 Michael Beiter <michael@beiter.org>
6    * %%
7    * All rights reserved.
8    * .
9    * Redistribution and use in source and binary forms, with or without
10   * modification, are permitted provided that the following conditions are met:
11   *     * Redistributions of source code must retain the above copyright
12   *       notice, this list of conditions and the following disclaimer.
13   *     * Redistributions in binary form must reproduce the above copyright
14   *       notice, this list of conditions and the following disclaimer in the
15   *       documentation and/or other materials provided with the distribution.
16   *     * Neither the name of the copyright holder nor the names of the
17   *       contributors may be used to endorse or promote products derived
18   *       from this software without specific prior written permission.
19   * .
20   * .
21   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
25   * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31   * #L%
32   */
33  package org.beiter.michael.db;
34  
35  import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
36  import org.apache.commons.dbcp2.PoolableConnection;
37  import org.apache.commons.dbcp2.PoolableConnectionFactory;
38  import org.apache.commons.dbcp2.PoolingDataSource;
39  import org.apache.commons.lang3.Validate;
40  import org.apache.commons.pool2.impl.GenericObjectPool;
41  import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  import javax.naming.Context;
46  import javax.naming.InitialContext;
47  import javax.naming.NamingException;
48  import javax.sql.DataSource;
49  import java.sql.Connection;
50  import java.sql.SQLException;
51  import java.util.HashMap;
52  import java.util.Map;
53  import java.util.Properties;
54  import java.util.concurrent.ConcurrentHashMap;
55  
56  /**
57   * This class creates and manages JDBC Connection instances from:
58   * <ul>
59   * <li>A named JNDI managed connection</li>
60   * <li>A connection pool that is maintained by this factory</li>
61   * </ul>
62   */
63  public final class ConnectionFactory {
64  
65      /**
66       * The logger object for this class
67       */
68      private static final Logger LOG = LoggerFactory.getLogger(ConnectionFactory.class);
69  
70  
71      /**
72       * This hash map stores the generated pools per connection
73       */
74      private static final ConcurrentHashMap<String, PoolingDataSource<PoolableConnection>> CONNECTION_POOLS =
75              new ConcurrentHashMap<>();
76  
77      /**
78       * A private constructor to prevent instantiation of this class
79       */
80      private ConnectionFactory() {
81      }
82  
83      /**
84       * Return a Connection instance for a JNDI managed JDBC connection.
85       *
86       * @param jndiName The JNDI connection name
87       * @return a JDBC connection
88       * @throws FactoryException When the connection cannot be retrieved from JNDI
89       */
90      public static Connection getConnection(final String jndiName)
91              throws FactoryException {
92  
93          Validate.notBlank(jndiName);
94  
95          try {
96              // the initial context is created from the provided JNDI settings
97              final Context context = new InitialContext();
98  
99              // retrieve a data source object, close the context as it is no longer needed, and return the connection
100             final Object namedObject = context.lookup(jndiName);
101             if (DataSource.class.isInstance(namedObject)) {
102                 final DataSource dataSource = (DataSource) context.lookup(jndiName);
103                 context.close();
104 
105                 return dataSource.getConnection();
106             } else {
107                 final String error = "The JNDI name '" + jndiName + "' does not reference a SQL DataSource."
108                         + " This is a configuration issue.";
109                 LOG.warn(error);
110                 throw new FactoryException(error);
111             }
112         } catch (SQLException | NamingException e) {
113             final String error = "Error retrieving JDBC connection from JNDI: " + jndiName;
114             LOG.warn(error);
115             throw new FactoryException(error, e);
116         }
117     }
118 
119 
120     /**
121      * Return a Connection instance from a pool that managed JDBC driver based connections.
122      * <p/>
123      * The driver-based connection are managed in a connection pool.
124      *
125      * @param driver         The JDBC driver class to use
126      * @param connectionSpec The connection spec to use
127      * @param poolSpec       A connection pool spec
128      * @return a JDBC connection
129      * @throws FactoryException When the connection cannot be retrieved from the pool, or the pool cannot be created
130      */
131     public static Connection getConnection(final String driver, final ConnectionSpec connectionSpec,
132                                            final ConnectionPoolSpec poolSpec)
133             throws FactoryException {
134 
135         Validate.notBlank(driver);
136         Validate.notNull(connectionSpec);
137         Validate.notNull(poolSpec);
138 
139         // no need for defensive copies of Strings
140 
141         final HashMap<String, String> properties = new HashMap<>();
142         properties.put("user", connectionSpec.getUser());
143         properties.put("password", connectionSpec.getPassword());
144 
145         return getConnection(driver, connectionSpec.getUrl(), properties, poolSpec);
146     }
147 
148     /**
149      * Return a Connection instance from a pool that manages JDBC driver based connections.
150      * <p/>
151      * The driver-based connection are managed in a connection pool. The pool is created using the provided properties
152      * for both the connection and the pool spec. Once the pool has been created, it is cached (based on URL and
153      * username), and can no longer be changed. Subsequent calls to this method will return a connection from the
154      * cached pool, and changes in the pool spec (e.g. changes to the size of the pool) will be ignored.
155      *
156      * @param driver     The JDBC driver class to use
157      * @param url        The JDBC database URL of the form <code>jdbc:subprotocol:subname</code>
158      * @param properties A list of key/value configuration parameters to pass as connection arguments. Normally at
159      *                   least a "user" and "password" property should be included
160      * @param poolSpec   A connection pool spec
161      * @return a JDBC connection
162      * @throws FactoryException When the connection cannot be retrieved from the pool, or the pool cannot be created
163      */
164     public static Connection getConnection(final String driver, final String url, final Map<String, String> properties,
165                                            final ConnectionPoolSpec poolSpec)
166             throws FactoryException {
167 
168         Validate.notBlank(driver);
169         Validate.notBlank(url);
170         Validate.notNull(properties);
171         Validate.notNull(poolSpec);
172 
173         // no need for defensive copies of Strings
174 
175         // Load the database driver (if not already done)
176         loadDriver(driver);
177 
178         // CHECKSTYLE:OFF
179         // this particular set of inline conditions is easy to read :-)
180         final String username = properties.get("user") == null ? "" : properties.get("user");
181         // CHECKSTYLE:OFF
182 
183         // we keep a separate pool per connection
184         // a connection is identified by the URL, the username, and the password
185         final String key = String.format("%s:%s", url, username);
186 
187         // avoid if possible to create the pool multiple times, and store the data source pool for later use
188         if (!CONNECTION_POOLS.containsKey(key)) {
189             synchronized (ConnectionFactory.class) {
190                 if (!CONNECTION_POOLS.containsKey(key)) {
191 
192                     // this call is thread safe even without the double if check and extra synchronization. However, it
193                     // might happen that the pool is created multiple times. While additional copies would be simply
194                     // thrown away, we might run into problems in case that, for instance, the number of connections
195                     // from the same user / machine are restricted on the DB server.
196                     // While this does not happen a lot (it only happens if there is not already an entry and multiple
197                     // threads race this block and lose), it could still lead to a failure, and we must take this double
198                     // sync workaround. There is a solution for Java 8 - see below.
199                     CONNECTION_POOLS.putIfAbsent(key, getPoolingDataSource(url, properties, poolSpec));
200                 }
201             }
202         }
203         // This would solve the problem of multiple pools being created and all but one being throws away, but it
204         // does not work before Java 8 because the "computeIfAbsent()" method with the lambda function is not
205         // available before Java 8:
206         // TODO: add the pooled data source with the "computeIfAbsent()" method to improve performance in Java 8
207         //CONNECTION_POOLS.computeIfAbsent(key, k -> getPoolingDataSource(url, properties, poolSpec));
208 
209         try {
210             return CONNECTION_POOLS.get(key).getConnection();
211         } catch (SQLException e) {
212             final String error = "Error retrieving JDBC connection from pool: " + key;
213             LOG.warn(error);
214             throw new FactoryException(error, e);
215         }
216     }
217 
218     /**
219      * Resets the internal state of the factory.
220      * <p/>
221      * <strong>This method does not release any resources that have been borrowed from the connection pools managed
222      * by this factory.</strong> To avoid resource leaks, you <strong>must</strong> close / return all connections to
223      * their pools before calling this method.
224      */
225     public static void reset() {
226 
227         // Unset the cached connections
228         CONNECTION_POOLS.clear();
229     }
230 
231     /**
232      * Make sure that the database driver exists
233      *
234      * @param driver The JDBC driver class to load
235      * @throws FactoryException When the driver cannot be loaded
236      */
237     private static void loadDriver(final String driver) throws FactoryException {
238 
239         // assert in private method
240         assert driver != null : "The driver cannot be null";
241 
242         LOG.debug("Loading the database driver '" + driver + "'");
243 
244         // make sure the driver is available
245         try {
246             Class.forName(driver);
247         } catch (ClassNotFoundException e) {
248             final String error = "Error loading JDBC driver class: " + driver;
249             LOG.warn(error, e);
250             throw new FactoryException(error, e);
251         }
252     }
253 
254     /**
255      * Get a pooled data source for the provided connection parameters.
256      *
257      * @param url        The JDBC database URL of the form <code>jdbc:subprotocol:subname</code>
258      * @param properties A list of key/value configuration parameters to pass as connection arguments. Normally at
259      *                   least a "user" and "password" property should be included
260      * @param poolSpec   A connection pool spec
261      * @return A pooled database connection
262      */
263     private static PoolingDataSource<PoolableConnection> getPoolingDataSource(final String url,
264                                                                               final Map<String, String> properties,
265                                                                               final ConnectionPoolSpec poolSpec) {
266 
267         // assert in private method
268         assert url != null : "The url cannot be null";
269         assert properties != null : "The properties cannot be null";
270         assert poolSpec != null : "The pol spec cannot be null";
271 
272         LOG.debug("Creating new pooled data source for '" + url + "'");
273 
274         // convert the properties hashmap to java properties
275         final Properties props = new Properties();
276         props.putAll(properties);
277 
278         // create a Apache DBCP pool configuration from the pool spec
279         final GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
280         poolConfig.setMaxTotal(poolSpec.getMaxTotal());
281         poolConfig.setMaxIdle(poolSpec.getMaxIdle());
282         poolConfig.setMinIdle(poolSpec.getMinIdle());
283         poolConfig.setMaxWaitMillis(poolSpec.getMaxWaitMillis());
284         poolConfig.setTestOnCreate(poolSpec.isTestOnCreate());
285         poolConfig.setTestOnBorrow(poolSpec.isTestOnBorrow());
286         poolConfig.setTestOnReturn(poolSpec.isTestOnReturn());
287         poolConfig.setTestWhileIdle(poolSpec.isTestWhileIdle());
288         poolConfig.setTimeBetweenEvictionRunsMillis(poolSpec.getTimeBetweenEvictionRunsMillis());
289         poolConfig.setNumTestsPerEvictionRun(poolSpec.getNumTestsPerEvictionRun());
290         poolConfig.setMinEvictableIdleTimeMillis(poolSpec.getMinEvictableIdleTimeMillis());
291         poolConfig.setSoftMinEvictableIdleTimeMillis(poolSpec.getSoftMinEvictableIdleTimeMillis());
292         poolConfig.setLifo(poolSpec.isLifo());
293 
294 
295         // create the pool and assign the factory to the pool
296         final org.apache.commons.dbcp2.ConnectionFactory connFactory = new DriverManagerConnectionFactory(url, props);
297         final PoolableConnectionFactory poolConnFactory = new PoolableConnectionFactory(connFactory, null);
298         poolConnFactory.setDefaultAutoCommit(poolSpec.isDefaultAutoCommit());
299         poolConnFactory.setDefaultReadOnly(poolSpec.isDefaultReadOnly());
300         poolConnFactory.setDefaultTransactionIsolation(poolSpec.getDefaultTransactionIsolation());
301         poolConnFactory.setCacheState(poolSpec.isCacheState());
302         poolConnFactory.setValidationQuery(poolSpec.getValidationQuery());
303         poolConnFactory.setMaxConnLifetimeMillis(poolSpec.getMaxConnLifetimeMillis());
304         final GenericObjectPool<PoolableConnection> connPool = new GenericObjectPool<>(poolConnFactory, poolConfig);
305         poolConnFactory.setPool(connPool);
306 
307         // create a new pooled data source
308         return new PoolingDataSource<>(connPool);
309     }
310 }