001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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
223        // If username/password are not empty, we process them with the credentials resolver.
224        // Otherwise, try to get them from the secret store.
225        // At this point, the secret store has not been initialized properly with a CmsObject,
226        // so VFS based secret stores won't work for that.
227
228        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(user)) {
229            user = OpenCms.getCredentialsResolver().resolveCredential(I_CmsCredentialsResolver.DB_USER, user);
230            hikariProps.put("username", user);
231        } else {
232            user = OpenCms.getSecretStore().getSecret(KEY_DATABASE_POOL + "." + key + "." + KEY_USERNAME);
233            if (user != null) {
234                hikariProps.put("username", user);
235            }
236        }
237        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(password)) {
238            password = OpenCms.getCredentialsResolver().resolveCredential(
239                I_CmsCredentialsResolver.DB_PASSWORD,
240                password);
241            hikariProps.put("password", password);
242        } else {
243            password = OpenCms.getSecretStore().getSecret(KEY_DATABASE_POOL + "." + key + "." + KEY_PASSWORD);
244            if (password != null) {
245                hikariProps.put("password", password);
246            }
247        }
248
249        hikariProps.put("maximumPoolSize", "30");
250
251        // Properties of the form db.pool.poolname.v11.<foo> are directly passed to HikariCP as <foo>
252        for (Map.Entry<String, String> entry : poolMap.entrySet()) {
253            String suffix = getPropertyRelativeSuffix("v11", entry.getKey());
254            if (suffix != null) {
255                hikariProps.put(suffix, entry.getValue());
256            }
257        }
258
259        String configuredTestQuery = (String)(hikariProps.get("connectionTestQuery"));
260        String testQueryForDriver = testQueries.get(driver);
261        if ((testQueryForDriver != null) && CmsStringUtil.isEmptyOrWhitespaceOnly(configuredTestQuery)) {
262            hikariProps.put("connectionTestQuery", testQueryForDriver);
263        }
264        hikariProps.put("registerMbeans", "true");
265        HikariConfig result = new HikariConfig(hikariProps);
266
267        result.setPoolName(poolName.replace(":", "_"));
268        return result;
269    }
270
271    /**
272     * Returns the name of the default database connection pool.<p>
273     *
274     * @return the name of the default database connection pool
275     */
276    public static String getDefaultDbPoolName() {
277
278        return OPENCMS_DEFAULT_POOL_NAME;
279    }
280
281    /**
282     * If str starts with prefix + '.', return the remaining part, otherwise return null.<p>
283     *
284     * @param prefix the prefix
285     * @param str the string to remove the prefix from
286     * @return str with the prefix removed, or null if it didn't start with prefix + '.'
287     */
288    public static String getPropertyRelativeSuffix(String prefix, String str) {
289
290        String realPrefix = prefix + ".";
291        if (str.startsWith(realPrefix)) {
292            return str.substring(realPrefix.length());
293        }
294        return null;
295    }
296
297    /**
298     * Closes the pool.<p>
299     *
300     * @throws Exception if something goes wrong
301     */
302    public void close() throws Exception {
303
304        m_dataSource.close();
305    }
306
307    /**
308     * Returns the number of active connections.<p>
309     *
310     * @return the number of active connections
311     */
312    public int getActiveConnections() {
313
314        return m_dataSource.getHikariPoolMXBean().getActiveConnections();
315    }
316
317    /**
318     * Gets a database connection from the pool.<p>
319     *
320     * @return the database connection
321     * @throws SQLException if something goes wrong
322     */
323    public Connection getConnection() throws SQLException {
324
325        try {
326            return m_dataSource.getConnection();
327        } catch (SQLTransientConnectionException | SQLTimeoutException e) {
328            LOG.error(e.getLocalizedMessage(), e);
329            throw e;
330        }
331    }
332
333    /**
334     * Gets the number of idle connections.<p>
335     *
336     * @return the number of idle connections
337     */
338    public int getIdleConnections() {
339
340        return m_dataSource.getHikariPoolMXBean().getIdleConnections();
341    }
342
343    /**
344     * Gets the pool url.<p>
345     *
346     * @return the pool url
347     */
348    public String getPoolUrl() {
349
350        return m_poolUrl;
351    }
352}