View Javadoc
1   /*
2    * #%L
3    * This file is part of a universal JDBC Connection factory.
4    * %%
5    * Copyright (C) 2014 - 2016 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.util.Properties;
50  import java.util.concurrent.ConcurrentHashMap;
51  import java.util.concurrent.ConcurrentMap;
52  
53  /**
54   * This class creates and manages JDBC Data Source instances either from:
55   * <ul>
56   * <li>a named JNDI managed data source or</li>
57   * <li>a data source pool that is maintained by this factory</li>
58   * </ul>
59   */
60  public final class DataSourceFactory {
61  
62      /**
63       * The logger object for this class
64       */
65      private static final Logger LOG = LoggerFactory.getLogger(DataSourceFactory.class);
66  
67  
68      /**
69       * This hash map stores the generated data source pools per connection parameter set
70       */
71      private static final ConcurrentHashMap<String, PoolingDataSource<PoolableConnection>> DS_POOLS =
72              new ConcurrentHashMap<>();
73  
74      /**
75       * A private constructor to prevent instantiation of this class
76       */
77      private DataSourceFactory() {
78      }
79  
80      /**
81       * Return a DataSource instance for a JNDI managed JDBC data source.
82       *
83       * @param jndiName The JNDI connection name
84       * @return a JDBC data source
85       * @throws FactoryException         When no date source can be retrieved from JNDI
86       * @throws NullPointerException     When {@code jndiName} is null
87       * @throws IllegalArgumentException When {@code jndiName} is empty
88       */
89      public static DataSource getDataSource(final String jndiName)
90              throws FactoryException {
91  
92          Validate.notBlank(jndiName, "The validated character sequence 'jndiName' is null or empty");
93  
94          // no need for defensive copies of Strings
95  
96          try {
97              // the initial context is created from the provided JNDI settings
98              final Context context = new InitialContext();
99  
100             // retrieve a data source object, close the context as it is no longer needed, and return the data source
101             final Object namedObject = context.lookup(jndiName);
102             if (DataSource.class.isInstance(namedObject)) {
103                 final DataSource dataSource = (DataSource) context.lookup(jndiName);
104                 context.close();
105 
106                 return dataSource;
107             } else {
108                 final String error = "The JNDI name '" + jndiName + "' does not reference a SQL DataSource."
109                         + " This is a configuration issue.";
110                 LOG.warn(error);
111                 throw new FactoryException(error);
112             }
113         } catch (NamingException e) {
114             final String error = "Error retrieving JDBC date source from JNDI: " + jndiName;
115             LOG.warn(error);
116             throw new FactoryException(error, e);
117         }
118     }
119 
120     /**
121      * Return a DataSource instance from a pool that manages JDBC driver based connections.
122      * <p>
123      * The driver-based data sources are managed in a data source pool. The pool is created using the provided
124      * properties for both the connection / data source and the pool spec. Once the pool has been created, it is cached
125      * (based on the connection parameters, i.e. the URL and username), and can no longer be changed. Subsequent calls
126      * to this method will return a data source from the cached pool, and changes in the pool spec (e.g. changes to the
127      * size of the pool) will be ignored.
128      *
129      * @param poolSpec A connection pool spec that has the driver and url configured as non-empty strings
130      * @return a JDBC connection
131      * @throws FactoryException         When the data source  cannot be retrieved from the pool, or the pool cannot be
132      *                                  created
133      * @throws NullPointerException     When the {@code poolSpec}, {@code poolSpec.getDriver()}, or
134      *                                  {@code poolSpec.getUrl()} are {@code null}
135      * @throws IllegalArgumentException When {@code poolSpec.getDriver()} or {@code poolSpec.getUrl()} are empty
136      */
137     public static DataSource getDataSource(final ConnectionProperties poolSpec)
138             throws FactoryException {
139 
140         Validate.notNull(poolSpec, "The validated object 'poolSpec' is null");
141         Validate.notBlank(poolSpec.getDriver(),
142                 "The validated character sequence 'poolSpec.getDriver()' is null or empty");
143         Validate.notBlank(poolSpec.getUrl(), "The validated character sequence 'poolSpec.getUrl()' is null or empty");
144 
145         // no need for defensive copies of Strings
146 
147         final String driver = poolSpec.getDriver();
148         final String url = poolSpec.getUrl();
149         // CHECKSTYLE:OFF
150         // this particular set of inline conditions is easy to read :-)
151         final String username = poolSpec.getUsername() == null ? "" : poolSpec.getUsername();
152         final String password = poolSpec.getPassword() == null ? "" : poolSpec.getPassword();
153         // CHECKSTYLE:OFF
154 
155         // Load the database driver (if not already done)
156         loadDriver(driver);
157 
158         // create the hash map required for the connection pool username + password
159         final ConcurrentMap<String, String> properties = new ConcurrentHashMap<>();
160         properties.put("user", username);
161         properties.put("password", password);
162 
163         // we keep a separate pool per connection
164         // a connection is identified by the URL, the username, and the password
165         final String key = String.format("%s:%s", url, username);
166 
167         // avoid if possible to create the pool multiple times, and store the data source pool for later use
168         if (!DS_POOLS.containsKey(key)) {
169             synchronized (DataSourceFactory.class) {
170                 if (!DS_POOLS.containsKey(key)) {
171 
172                     // this call is thread safe even without the double if check and extra synchronization. However, it
173                     // might happen that the pool is created multiple times. While additional copies would be simply
174                     // thrown away, we might run into problems in case that, for instance, the number of connections
175                     // from the same user / machine are restricted on the DB server.
176                     // While this does not happen a lot (it only happens if there is not already an entry and multiple
177                     // threads race this block and lose), it could still lead to a failure, and we must take this double
178                     // sync workaround. There is a solution for Java 8 - see below.
179                     DS_POOLS.putIfAbsent(key, getPoolingDataSource(url, properties, poolSpec));
180                 }
181             }
182         }
183         // This would solve the problem of multiple pools being created and all but one being throws away, but it
184         // does not work before Java 8 because the "computeIfAbsent()" method with the lambda function is not
185         // available before Java 8:
186         // TODO: add the pooled data source with the "computeIfAbsent()" method to improve performance in Java 8
187         //DS_POOLS.computeIfAbsent(key, k -> getPoolingDataSource(url, properties, poolSpec));
188 
189         return DS_POOLS.get(key);
190     }
191 
192     /**
193      * Resets the internal state of the factory.
194      * <p>
195      * <strong>This method does not release any resources that have been borrowed from the connection pools managed
196      * by this factory.</strong> To avoid resource leaks, you <strong>must</strong> close / return all connections to
197      * their pools before calling this method.
198      */
199     public static void reset() {
200 
201         // Unset the cached connections
202         DS_POOLS.clear();
203     }
204 
205     /**
206      * Make sure that the database driver exists
207      *
208      * @param driver The JDBC driver class to load
209      * @throws FactoryException When the driver cannot be loaded
210      */
211     private static void loadDriver(final String driver) throws FactoryException {
212 
213         // assert in private method
214         assert driver != null : "The driver cannot be null";
215 
216         LOG.debug("Loading the database driver '" + driver + "'");
217 
218         // make sure the driver is available
219         try {
220             Class.forName(driver);
221         } catch (ClassNotFoundException e) {
222             final String error = "Error loading JDBC driver class: " + driver;
223             LOG.warn(error, e);
224             throw new FactoryException(error, e);
225         }
226     }
227 
228     /**
229      * Get a pooled data source for the provided connection parameters.
230      *
231      * @param url        The JDBC database URL of the form <code>jdbc:subprotocol:subname</code>
232      * @param properties A list of key/value configuration parameters to pass as connection arguments. Normally at
233      *                   least a "user" and "password" property should be included
234      * @param poolSpec   A connection pool spec
235      * @return A pooled database connection
236      */
237     private static PoolingDataSource<PoolableConnection> getPoolingDataSource(final String url,
238                                                                               final ConcurrentMap<String, String> properties,
239                                                                               final ConnectionProperties poolSpec) {
240 
241         // assert in private method
242         assert url != null : "The url cannot be null";
243         assert properties != null : "The properties cannot be null";
244         assert poolSpec != null : "The pol spec cannot be null";
245 
246         LOG.debug("Creating new pooled data source for '" + url + "'");
247 
248         // convert the properties hashmap to java properties
249         final Properties props = new Properties();
250         props.putAll(properties);
251 
252         // create a Apache DBCP pool configuration from the pool spec
253         final GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
254         poolConfig.setMaxTotal(poolSpec.getMaxTotal());
255         poolConfig.setMaxIdle(poolSpec.getMaxIdle());
256         poolConfig.setMinIdle(poolSpec.getMinIdle());
257         poolConfig.setMaxWaitMillis(poolSpec.getMaxWaitMillis());
258         poolConfig.setTestOnCreate(poolSpec.isTestOnCreate());
259         poolConfig.setTestOnBorrow(poolSpec.isTestOnBorrow());
260         poolConfig.setTestOnReturn(poolSpec.isTestOnReturn());
261         poolConfig.setTestWhileIdle(poolSpec.isTestWhileIdle());
262         poolConfig.setTimeBetweenEvictionRunsMillis(poolSpec.getTimeBetweenEvictionRunsMillis());
263         poolConfig.setNumTestsPerEvictionRun(poolSpec.getNumTestsPerEvictionRun());
264         poolConfig.setMinEvictableIdleTimeMillis(poolSpec.getMinEvictableIdleTimeMillis());
265         poolConfig.setSoftMinEvictableIdleTimeMillis(poolSpec.getSoftMinEvictableIdleTimeMillis());
266         poolConfig.setLifo(poolSpec.isLifo());
267 
268 
269         // create the pool and assign the factory to the pool
270         final org.apache.commons.dbcp2.ConnectionFactory connFactory = new DriverManagerConnectionFactory(url, props);
271         final PoolableConnectionFactory poolConnFactory = new PoolableConnectionFactory(connFactory, null);
272         poolConnFactory.setDefaultAutoCommit(poolSpec.isDefaultAutoCommit());
273         poolConnFactory.setDefaultReadOnly(poolSpec.isDefaultReadOnly());
274         poolConnFactory.setDefaultTransactionIsolation(poolSpec.getDefaultTransactionIsolation());
275         poolConnFactory.setCacheState(poolSpec.isCacheState());
276         poolConnFactory.setValidationQuery(poolSpec.getValidationQuery());
277         poolConnFactory.setMaxConnLifetimeMillis(poolSpec.getMaxConnLifetimeMillis());
278         final GenericObjectPool<PoolableConnection> connPool = new GenericObjectPool<>(poolConnFactory, poolConfig);
279         poolConnFactory.setPool(connPool);
280 
281         // create a new pooled data source
282         return new PoolingDataSource<>(connPool);
283     }
284 }