001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.db;
029
030import org.opencms.configuration.CmsParameterConfiguration;
031import org.opencms.main.CmsLog;
032import org.opencms.main.OpenCms;
033import org.opencms.security.I_CmsCredentialsResolver;
034import org.opencms.util.CmsStringUtil;
035
036import java.sql.Connection;
037import java.util.ArrayList;
038import java.util.List;
039import java.util.Properties;
040
041import org.apache.commons.dbcp.ConnectionFactory;
042import org.apache.commons.dbcp.DriverManagerConnectionFactory;
043import org.apache.commons.dbcp.PoolableConnectionFactory;
044import org.apache.commons.dbcp.PoolingDriver;
045import org.apache.commons.pool.impl.GenericKeyedObjectPool;
046import org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory;
047import org.apache.commons.pool.impl.GenericObjectPool;
048
049/**
050 * Various methods to create DBCP pools.<p>
051 *
052 * Only JDBC Driver based pools are supported currently. JNDI DataSource
053 * based pools might be added probably later.<p>
054 *
055 * <b>Please note:</b> This class is subject to change in later versions.
056 * To obtain information about the connections, please use the
057 * {@link org.opencms.db.CmsSqlManager}.<p>
058 *
059 * @since 6.0.0
060 */
061public final class CmsDbPool {
062
063    /** This prefix is required to make the JDBC DriverManager return pooled DBCP connections. */
064    public static final String DBCP_JDBC_URL_PREFIX = "jdbc:apache:commons:dbcp:";
065
066    /** Key for number of connection attempts. */
067    public static final String KEY_CONNECT_ATTEMTS = "connects";
068
069    /** Key for connection waiting. */
070    public static final String KEY_CONNECT_WAITS = "wait";
071
072    /** Prefix for database keys. */
073    public static final String KEY_DATABASE = "db.";
074
075    /** Key for the database name. */
076    public static final String KEY_DATABASE_NAME = KEY_DATABASE + "name";
077
078    /** Key for the pool id. */
079    public static final String KEY_DATABASE_POOL = KEY_DATABASE + "pool";
080
081    /** Key for statement pooling. */
082    public static final String KEY_DATABASE_STATEMENTS = KEY_DATABASE + "statements";
083
084    /** Key for the entity manager pool size. */
085    public static final String KEY_ENTITY_MANAGER_POOL_SIZE = "entityMangerPoolSize";
086
087    /**
088     * Key for the connection properties (optional). Alternative to {@link #KEY_JDBC_URL_PARAMS jdbc url params} to
089     * configure the jdbc driver. (Notice that not all drivers support both configuration methods.)
090     * <p>
091     * From the Apache's Commons DBCP 1.4 API: 'list of arbitrary string tag/value pairs as connection arguments;
092     * normally at least a "user" and "password" property should be included.'
093     */
094    public static final String KEY_CONNECTION_PROPERTIES = "connectionProperties";
095
096    /** Key for jdbc driver. */
097    public static final String KEY_JDBC_DRIVER = "jdbcDriver";
098
099    /** Key for jdbc url. */
100    public static final String KEY_JDBC_URL = "jdbcUrl";
101
102    /** Key for jdbc url params. */
103    public static final String KEY_JDBC_URL_PARAMS = KEY_JDBC_URL + ".params";
104
105    /** Key for maximum active connections. */
106    public static final String KEY_MAX_ACTIVE = "maxActive";
107
108    /** Key for maximum idle connections. */
109    public static final String KEY_MAX_IDLE = "maxIdle";
110
111    /** Key for maximum wait time. */
112    public static final String KEY_MAX_WAIT = "maxWait";
113
114    /** Key for minimum idle time before a connection is subject to an eviction test. */
115    public static final String KEY_MIN_EVICTABLE_IDLE_TIME = "minEvictableIdleTime";
116
117    /** Key for minimum number of connections kept open. */
118    public static final String KEY_MIN_IDLE = "minIdle";
119
120    /** Key for number of tested connections per run. */
121    public static final String KEY_NUM_TESTS_PER_EVICTION_RUN = "numTestsPerEvictionRun";
122
123    /** Key for database password. */
124    public static final String KEY_PASSWORD = "password";
125
126    /** Key for default. */
127    public static final String KEY_POOL_DEFAULT = "default";
128
129    /** Key for pool url. */
130    public static final String KEY_POOL_URL = "poolUrl";
131
132    /** Key for pool user. */
133    public static final String KEY_POOL_USER = "user";
134
135    /** Key for vfs pool. */
136    public static final String KEY_POOL_VFS = "vfs";
137
138    /** Key for pooling flag. */
139    public static final String KEY_POOLING = "pooling";
140
141    /** Key for test on borrow flag. */
142    public static final String KEY_TEST_ON_BORROW = "testOnBorrow";
143
144    /** Key for test query. */
145    public static final String KEY_TEST_QUERY = "testQuery";
146
147    /** Key for test while idle flag. */
148    public static final String KEY_TEST_WHILE_IDLE = "testWhileIdle";
149
150    /** Key for time between two eviction runs. */
151    public static final String KEY_TIME_BETWEEN_EVICTION_RUNS = "timeBetweenEvictionRuns";
152
153    /** Key for user name. */
154    public static final String KEY_USERNAME = "user";
155
156    /** Key for "when pool exhausted" action. */
157    public static final String KEY_WHEN_EXHAUSTED_ACTION = "whenExhaustedAction";
158
159    /** The name of the opencms default pool. */
160    public static final String OPENCMS_DEFAULT_POOL_NAME = "default";
161
162    /** The default OpenCms JDBC pool URL. */
163    public static final String OPENCMS_DEFAULT_POOL_URL = "opencms:default";
164
165    /** The prefix used for opencms JDBC pools. */
166    public static final String OPENCMS_URL_PREFIX = "opencms:";
167
168    /**
169     * Default constructor.<p>
170     *
171     * Nobody is allowed to create an instance of this class!
172     */
173    private CmsDbPool() {
174
175        super();
176    }
177
178    /**
179     * Creates a JDBC DriverManager based DBCP connection pool.<p>
180     *
181     * @param config the configuration (opencms.properties)
182     * @param key the key of the database pool in the configuration
183     * @return String the URL to access the created DBCP pool
184     * @throws Exception if the pool could not be initialized
185     */
186    public static PoolingDriver createDriverManagerConnectionPool(CmsParameterConfiguration config, String key)
187    throws Exception {
188
189        // read the values of the pool configuration specified by the given key
190        String jdbcDriver = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_JDBC_DRIVER);
191        String jdbcUrl = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_JDBC_URL);
192        String jdbcUrlParams = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_JDBC_URL_PARAMS);
193        int maxActive = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_MAX_ACTIVE, 10);
194        int maxWait = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_MAX_WAIT, 2000);
195        int maxIdle = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_MAX_IDLE, 5);
196        int minEvictableIdleTime = config.getInteger(
197            KEY_DATABASE_POOL + '.' + key + '.' + KEY_MIN_EVICTABLE_IDLE_TIME,
198            1800000);
199        int minIdle = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_MIN_IDLE, 0);
200        int numTestsPerEvictionRun = config.getInteger(
201            KEY_DATABASE_POOL + '.' + key + '.' + KEY_NUM_TESTS_PER_EVICTION_RUN,
202            3);
203        int timeBetweenEvictionRuns = config.getInteger(
204            KEY_DATABASE_POOL + '.' + key + '.' + KEY_TIME_BETWEEN_EVICTION_RUNS,
205            3600000);
206        String testQuery = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_TEST_QUERY);
207        String username = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_USERNAME);
208        username = OpenCms.getCredentialsResolver().resolveCredential(I_CmsCredentialsResolver.DB_USER, username);
209        String password = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_PASSWORD);
210        password = OpenCms.getCredentialsResolver().resolveCredential(I_CmsCredentialsResolver.DB_PASSWORD, password);
211        String poolUrl = config.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_POOL_URL);
212        String whenExhaustedActionValue = config.get(
213            KEY_DATABASE_POOL + '.' + key + '.' + KEY_WHEN_EXHAUSTED_ACTION).trim();
214        byte whenExhaustedAction = 0;
215        boolean testOnBorrow = Boolean.valueOf(
216            config.getString(KEY_DATABASE_POOL + '.' + key + '.' + KEY_TEST_ON_BORROW, "false").trim()).booleanValue();
217        boolean testWhileIdle = Boolean.valueOf(
218            config.getString(KEY_DATABASE_POOL + '.' + key + '.' + KEY_TEST_WHILE_IDLE, "false").trim()).booleanValue();
219
220        if ("block".equalsIgnoreCase(whenExhaustedActionValue)) {
221            whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_BLOCK;
222        } else if ("fail".equalsIgnoreCase(whenExhaustedActionValue)) {
223            whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_FAIL;
224        } else if ("grow".equalsIgnoreCase(whenExhaustedActionValue)) {
225            whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_GROW;
226        } else {
227            whenExhaustedAction = GenericObjectPool.DEFAULT_WHEN_EXHAUSTED_ACTION;
228        }
229
230        if ("".equals(testQuery)) {
231            testQuery = null;
232        }
233
234        if (username == null) {
235            username = "";
236        }
237
238        if (password == null) {
239            password = "";
240        }
241
242        // read the values of the statement pool configuration specified by the given key
243        boolean poolingStmts = Boolean.valueOf(
244            config.getString(
245                KEY_DATABASE_STATEMENTS + '.' + key + '.' + KEY_POOLING,
246                CmsStringUtil.TRUE).trim()).booleanValue();
247        int maxActiveStmts = config.getInteger(KEY_DATABASE_STATEMENTS + '.' + key + '.' + KEY_MAX_ACTIVE, 25);
248        int maxWaitStmts = config.getInteger(KEY_DATABASE_STATEMENTS + '.' + key + '.' + KEY_MAX_WAIT, 250);
249        int maxIdleStmts = config.getInteger(KEY_DATABASE_STATEMENTS + '.' + key + '.' + KEY_MAX_IDLE, 15);
250        String whenStmtsExhaustedActionValue = config.get(
251            KEY_DATABASE_STATEMENTS + '.' + key + '.' + KEY_WHEN_EXHAUSTED_ACTION);
252        byte whenStmtsExhaustedAction = GenericKeyedObjectPool.WHEN_EXHAUSTED_GROW;
253        if (whenStmtsExhaustedActionValue != null) {
254            whenStmtsExhaustedActionValue = whenStmtsExhaustedActionValue.trim();
255            whenStmtsExhaustedAction = ("block".equalsIgnoreCase(whenStmtsExhaustedActionValue))
256            ? GenericKeyedObjectPool.WHEN_EXHAUSTED_BLOCK
257            : ("fail".equalsIgnoreCase(whenStmtsExhaustedActionValue))
258            ? GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL
259            : GenericKeyedObjectPool.WHEN_EXHAUSTED_GROW;
260        }
261
262        int connectionAttempts = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_CONNECT_ATTEMTS, 10);
263        int connetionsWait = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_CONNECT_WAITS, 5000);
264
265        // create an instance of the JDBC driver
266        Class.forName(jdbcDriver).newInstance();
267
268        // initialize a keyed object pool to store connections
269        GenericObjectPool connectionPool = new GenericObjectPool(null);
270
271        /* Abandoned pool configuration:
272         *
273         * In case the systems encounters "pool exhaustion" (runs out of connections),
274         * comment the above line with "new GenericObjectPool(null)" and uncomment the
275         * 5 lines below. This will generate an "abandoned pool" configuration that logs
276         * abandoned connections to the System.out. Unfortunatly this code is deprecated,
277         * so to avoid code warnings it's also disabled here.
278         * Tested with commons-pool v 1.2.
279         */
280
281        //        AbandonedConfig abandonedConfig = new AbandonedConfig();
282        //        abandonedConfig.setLogAbandoned(true);
283        //        abandonedConfig.setRemoveAbandoned(true);
284        //        abandonedConfig.setRemoveAbandonedTimeout(5);
285        //        GenericObjectPool connectionPool = new AbandonedObjectPool(null, abandonedConfig);
286        // initialize an object pool to store connections
287        connectionPool.setMaxActive(maxActive);
288        connectionPool.setMaxIdle(maxIdle);
289        connectionPool.setMinIdle(minIdle);
290        connectionPool.setMaxWait(maxWait);
291        connectionPool.setWhenExhaustedAction(whenExhaustedAction);
292
293        if (testQuery != null) {
294            connectionPool.setTestOnBorrow(testOnBorrow);
295            connectionPool.setTestWhileIdle(testWhileIdle);
296            connectionPool.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRuns);
297            connectionPool.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
298            connectionPool.setMinEvictableIdleTimeMillis(minEvictableIdleTime);
299        }
300
301        // initialize a connection factory to make the DriverManager taking connections from the pool
302        if (jdbcUrlParams != null) {
303            jdbcUrl += jdbcUrlParams;
304        }
305
306        Properties connectionProperties = config.getPrefixedProperties(
307            KEY_DATABASE_POOL + '.' + key + '.' + KEY_CONNECTION_PROPERTIES);
308        connectionProperties.put(KEY_USERNAME, username);
309        connectionProperties.put(KEY_PASSWORD, password);
310        ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(jdbcUrl, connectionProperties);
311
312        // Set up statement pool, if desired
313        GenericKeyedObjectPoolFactory statementFactory = null;
314        if (poolingStmts) {
315            statementFactory = new GenericKeyedObjectPoolFactory(
316                null,
317                maxActiveStmts,
318                whenStmtsExhaustedAction,
319                maxWaitStmts,
320                maxIdleStmts);
321        }
322
323        // initialize a factory to obtain pooled connections and prepared statements
324        new PoolableConnectionFactory(connectionFactory, connectionPool, statementFactory, testQuery, false, true);
325
326        // initialize a new pooling driver using the pool
327        PoolingDriver driver = new PoolingDriver();
328        driver.registerPool(poolUrl, connectionPool);
329
330        Connection con = null;
331        boolean connect = false;
332        int connectionTests = 0;
333
334        // try to connect once to the database to ensure it can be connected to at all
335        // if the conection cannot be established, multiple attempts will be done to connect
336        // just in cast the database was not fast enough to start before OpenCms was started
337
338        do {
339            try {
340                // try to connect
341                con = connectionFactory.createConnection();
342                connect = true;
343            } catch (Exception e) {
344                // connection failed, increase attempts, sleept for some seconds and log a message
345                connectionTests++;
346                if (CmsLog.INIT.isInfoEnabled()) {
347                    CmsLog.INIT.info(
348                        Messages.get().getBundle().key(
349                            Messages.INIT_WAIT_FOR_DB_4,
350                            new Object[] {
351                                poolUrl,
352                                jdbcUrl,
353                                new Integer(connectionTests),
354                                new Integer(connetionsWait)}));
355                }
356                Thread.sleep(connetionsWait);
357            } finally {
358                if (con != null) {
359                    con.close();
360                }
361            }
362        } while (!connect && (connectionTests < connectionAttempts));
363
364        if (CmsLog.INIT.isInfoEnabled()) {
365            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_JDBC_POOL_2, poolUrl, jdbcUrl));
366        }
367        return driver;
368    }
369
370    /**
371     * Returns the database pool name for a given configuration key.<p>
372     *
373     * @param configuration the configuration
374     * @param key a db pool configuration key
375     * @return the database pool name
376     */
377    public static String getDbPoolName(CmsParameterConfiguration configuration, String key) {
378
379        return configuration.get(KEY_DATABASE_POOL + '.' + key + '.' + KEY_POOL_URL);
380    }
381
382    /**
383     * Returns a list of available database pool names.<p>
384     *
385     * @param configuration the configuration to read the pool names from
386     *
387     * @return a list of database pool names
388     */
389    public static List<String> getDbPoolUrls(CmsParameterConfiguration configuration) {
390
391        List<String> dbPoolNames = new ArrayList<String>();
392        List<String> driverPoolNames = configuration.getList(CmsDriverManager.CONFIGURATION_DB + ".pools");
393
394        for (String driverPoolName : driverPoolNames) {
395            dbPoolNames.add(getDbPoolName(configuration, driverPoolName));
396        }
397        return dbPoolNames;
398    }
399
400    /**
401     * Returns the name of the default database connection pool.<p>
402     *
403     * @return the name of the default database connection pool
404     */
405    public static String getDefaultDbPoolName() {
406
407        return OPENCMS_DEFAULT_POOL_NAME;
408    }
409}