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 }