001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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 & Co. KG, 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.sql.SQLException;
038import java.sql.SQLTimeoutException;
039import java.sql.SQLTransientConnectionException;
040import java.util.Map;
041import java.util.Properties;
042
043import org.apache.commons.logging.Log;
044
045import com.google.common.collect.Maps;
046import com.zaxxer.hikari.HikariConfig;
047import com.zaxxer.hikari.HikariDataSource;
048
049/**
050 * Database connection pool class using HikariCP.
051 */
052public final class CmsDbPoolV11 {
053
054    /** Prefix for database keys. */
055    public static final String KEY_DATABASE = "db.";
056
057    /** Key for the database name. */
058    public static final String KEY_DATABASE_NAME = KEY_DATABASE + "name";
059
060    /** Key for the pool id. */
061    public static final String KEY_DATABASE_POOL = KEY_DATABASE + "pool";
062
063    /** Key for number of connection attempts. */
064    public static final String KEY_CONNECT_ATTEMTS = "connects";
065
066    /** Key for connection waiting. */
067    public static final String KEY_CONNECT_WAITS = "wait";
068
069    /** Key for the entity manager pool size. */
070    public static final String KEY_ENTITY_MANAGER_POOL_SIZE = "entityMangerPoolSize";
071
072    /** Key for jdbc driver. */
073    public static final String KEY_JDBC_DRIVER = "jdbcDriver";
074
075    /** Key for jdbc url. */
076    public static final String KEY_JDBC_URL = "jdbcUrl";
077
078    /** Key for jdbc url params. */
079    public static final String KEY_JDBC_URL_PARAMS = KEY_JDBC_URL + ".params";
080
081    /** Key for database password. */
082    public static final String KEY_PASSWORD = "password";
083
084    /** Key for default. */
085    public static final String KEY_POOL_DEFAULT = "default";
086
087    /** Key for pool url. */
088    public static final String KEY_POOL_URL = "poolUrl";
089
090    /** Key for pool user. */
091    public static final String KEY_POOL_USER = "user";
092
093    /** Key for vfs pool. */
094    public static final String KEY_POOL_VFS = "vfs";
095
096    /** Key for user name. */
097    public static final String KEY_USERNAME = "user";
098
099    /** The name of the opencms default pool. */
100    public static final String OPENCMS_DEFAULT_POOL_NAME = "default";
101
102    /** The default OpenCms JDBC pool URL. */
103    public static final String OPENCMS_DEFAULT_POOL_URL = "opencms:default";
104
105    /** The prefix used for opencms JDBC pools. */
106    public static final String OPENCMS_URL_PREFIX = "opencms:";
107
108    /** Logger instance for this class. */
109    private static final Log LOG = CmsLog.getLog(CmsDbPoolV11.class);
110
111    /** Map of default test queries. */
112    private static Map<String, String> testQueries = Maps.newHashMap();
113
114    static {
115        testQueries.put("com.ibm.db2.jcc.DB2Driver", "SELECT 1 FROM SYSIBM.SYSDUMMY1");
116        testQueries.put("net.sourceforge.jtds.jdbc.Driver", "SELECT 1");
117        testQueries.put("oracle.jdbc.driver.OracleDriver", "SELECT 1 FROM DUAL");
118        testQueries.put("com.ibm.as400.access.AS400JDBCDriver", "SELECT NOW()");
119    }
120
121    /** The opencms pool url. */
122    private String m_poolUrl;
123
124    /** The HikariCP data source. */
125    private HikariDataSource m_dataSource;
126
127    /**
128     * Default constructor.<p>
129     *
130     * @param config the OpenCms configuration (opencms.properties)
131     * @param key the name of the pool (without the opencms: prefix)
132     *
133     * @throws Exception if something goes wrong
134     */
135    public CmsDbPoolV11(CmsParameterConfiguration config, String key)
136    throws Exception {
137
138        HikariConfig hikariConf = createHikariConfig(config, key);
139        m_poolUrl = OPENCMS_URL_PREFIX + key;
140        m_dataSource = new HikariDataSource(hikariConf);
141        Connection con = null;
142        boolean connect = false;
143        int connectionTests = 0;
144        int connectionAttempts = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_CONNECT_ATTEMTS, 10);
145        int connectionsWait = config.getInteger(KEY_DATABASE_POOL + '.' + key + '.' + KEY_CONNECT_WAITS, 5000);
146
147        // try to connect once to the database to ensure it can be connected to at all
148        // if the conection cannot be established, multiple attempts will be done to connect
149        // just in cast the database was not fast enough to start before OpenCms was started
150
151        do {
152            try {
153                // try to connect
154                con = m_dataSource.getConnection();
155                connect = true;
156            } catch (Exception e) {
157                // connection failed, increase attempts, sleept for some seconds and log a message
158                connectionTests++;
159                if (CmsLog.INIT.isInfoEnabled()) {
160                    CmsLog.INIT.info(
161                        Messages.get().getBundle().key(
162                            Messages.INIT_WAIT_FOR_DB_4,
163                            new Object[] {
164                                getPoolUrl(),
165                                m_dataSource.getJdbcUrl(),
166                                Integer.valueOf(connectionTests),
167                                Integer.valueOf(connectionsWait)}));
168                }
169                Thread.sleep(connectionsWait);
170            } finally {
171                if (con != null) {
172                    con.close();
173                }
174            }
175        } while (!connect && (connectionTests < connectionAttempts));
176
177        if (CmsLog.INIT.isInfoEnabled()) {
178            CmsLog.INIT.info(
179                Messages.get().getBundle().key(Messages.INIT_JDBC_POOL_2, getPoolUrl(), m_dataSource.getJdbcUrl()));
180        }
181    }
182
183    /**
184     * Creates the HikariCP configuration based on the configuration of a pool defined in opencms.properties.
185     *
186     * @param config the configuration object with the properties
187     * @param key the pool name (without the opencms prefix)
188     *
189     * @return the HikariCP configuration for the pool
190     */
191    public static HikariConfig createHikariConfig(CmsParameterConfiguration config, String key) {
192
193        Map<String, String> poolMap = Maps.newHashMap();
194        for (Map.Entry<String, String> entry : config.entrySet()) {
195            String suffix = getPropertyRelativeSuffix(KEY_DATABASE_POOL + "." + key, entry.getKey());
196            if ((suffix != null) && !CmsStringUtil.isEmptyOrWhitespaceOnly(entry.getValue())) {
197                String value = entry.getValue().trim();
198                poolMap.put(suffix, value);
199            }
200        }
201
202        // these are for backwards compatibility , all other properties not of the form db.pool.poolname.v11..... are ignored
203        String jdbcUrl = poolMap.get(KEY_JDBC_URL);
204        String params = poolMap.get(KEY_JDBC_URL_PARAMS);
205        String driver = poolMap.get(KEY_JDBC_DRIVER);
206        String user = poolMap.get(KEY_USERNAME);
207        String password = poolMap.get(KEY_PASSWORD);
208        String poolName = OPENCMS_URL_PREFIX + key;
209
210        if ((params != null) && (jdbcUrl != null)) {
211            jdbcUrl += params;
212        }
213
214        Properties hikariProps = new Properties();
215
216        if (jdbcUrl != null) {
217            hikariProps.put("jdbcUrl", jdbcUrl);
218        }
219        if (driver != null) {
220            hikariProps.put("driverClassName", driver);
221        }
222        if (user != null) {
223            user = OpenCms.getCredentialsResolver().resolveCredential(I_CmsCredentialsResolver.DB_USER, user);
224            hikariProps.put("username", user);
225        }
226        if (password != null) {
227            password = OpenCms.getCredentialsResolver().resolveCredential(
228                I_CmsCredentialsResolver.DB_PASSWORD,
229                password);
230            hikariProps.put("password", password);
231        }
232
233        hikariProps.put("maximumPoolSize", "30");
234
235        // Properties of the form db.pool.poolname.v11.<foo> are directly passed to HikariCP as <foo>
236        for (Map.Entry<String, String> entry : poolMap.entrySet()) {
237            String suffix = getPropertyRelativeSuffix("v11", entry.getKey());
238            if (suffix != null) {
239                hikariProps.put(suffix, entry.getValue());
240            }
241        }
242
243        String configuredTestQuery = (String)(hikariProps.get("connectionTestQuery"));
244        String testQueryForDriver = testQueries.get(driver);
245        if ((testQueryForDriver != null) && CmsStringUtil.isEmptyOrWhitespaceOnly(configuredTestQuery)) {
246            hikariProps.put("connectionTestQuery", testQueryForDriver);
247        }
248        hikariProps.put("registerMbeans", "true");
249        HikariConfig result = new HikariConfig(hikariProps);
250
251        result.setPoolName(poolName.replace(":", "_"));
252        return result;
253    }
254
255    /**
256     * Returns the name of the default database connection pool.<p>
257     *
258     * @return the name of the default database connection pool
259     */
260    public static String getDefaultDbPoolName() {
261
262        return OPENCMS_DEFAULT_POOL_NAME;
263    }
264
265    /**
266     * If str starts with prefix + '.', return the remaining part, otherwise return null.<p>
267     *
268     * @param prefix the prefix
269     * @param str the string to remove the prefix from
270     * @return str with the prefix removed, or null if it didn't start with prefix + '.'
271     */
272    public static String getPropertyRelativeSuffix(String prefix, String str) {
273
274        String realPrefix = prefix + ".";
275        if (str.startsWith(realPrefix)) {
276            return str.substring(realPrefix.length());
277        }
278        return null;
279    }
280
281    /**
282     * Closes the pool.<p>
283     *
284     * @throws Exception if something goes wrong
285     */
286    public void close() throws Exception {
287
288        m_dataSource.close();
289    }
290
291    /**
292     * Returns the number of active connections.<p>
293     *
294     * @return the number of active connections
295     */
296    public int getActiveConnections() {
297
298        return m_dataSource.getHikariPoolMXBean().getActiveConnections();
299    }
300
301    /**
302     * Gets a database connection from the pool.<p>
303     *
304     * @return the database connection
305     * @throws SQLException if something goes wrong
306     */
307    public Connection getConnection() throws SQLException {
308
309        try {
310            return m_dataSource.getConnection();
311        } catch (SQLTransientConnectionException | SQLTimeoutException e) {
312            LOG.error(e.getLocalizedMessage(), e);
313            throw e;
314        }
315    }
316
317    /**
318     * Gets the number of idle connections.<p>
319     *
320     * @return the number of idle connections
321     */
322    public int getIdleConnections() {
323
324        return m_dataSource.getHikariPoolMXBean().getIdleConnections();
325    }
326
327    /**
328     * Gets the pool url.<p>
329     *
330     * @return the pool url
331     */
332    public String getPoolUrl() {
333
334        return m_poolUrl;
335    }
336}