001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.sql.hikari;
018
019import com.zaxxer.hikari.HikariConfig;
020import com.zaxxer.hikari.HikariDataSource;
021import org.checkerframework.checker.nullness.qual.Nullable;
022import org.h2.engine.ConnectionInfo;
023import org.pcollections.HashTreePMap;
024import org.pcollections.PMap;
025
026import java.nio.file.Path;
027import java.nio.file.Paths;
028import java.sql.DriverManager;
029import java.sql.SQLException;
030import java.util.Properties;
031import java.util.function.BiFunction;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034
035import static java.util.Objects.requireNonNull;
036
037public class Hikari {
038    private static final Pattern JDBC_URL_REGEX = Pattern.compile("(?:jdbc:)?([^:]+):(//)?(?:([^:]+)(?::([^@]+))?@)?(.*)");
039
040    /**
041     * Map from protocol names to a function that transforms the given JDBC url
042     */
043    private static final PMap<String, BiFunction<Path, String, String>> PATH_CANONICALIZERS = HashTreePMap.singleton("h2", (baseDir, orig) ->{
044        // Bleh if only h2 had a better way of supplying a base directory... oh well...
045        final ConnectionInfo h2Info = new ConnectionInfo(orig);
046        if (!h2Info.isPersistent() || h2Info.isRemote()) {
047            return orig;
048        }
049
050        String url = orig;
051        if (url.startsWith("file:")) {
052            url = orig.substring("file:".length());
053        }
054
055        final Path origPath = Paths.get(url);
056        if (origPath.isAbsolute()) {
057            return origPath.toString();
058        } else {
059            return baseDir.toAbsolutePath().resolve(origPath).toString().replace('\\', '/');
060        }
061    });
062
063    /**
064     * Properties specific to a certain JDBC protocol, immutable.
065     *
066     * Protocols are identified by their jdbc driver names.
067     */
068    private static final PMap<String, Properties> PROTOCOL_SPECIFIC_PROPS;
069
070    static {
071        final Properties mysqlProps = new Properties();
072        // Config options based on http://assets.en.oreilly.com/1/event/21/Connector_J%20Performance%20Gems%20Presentation.pdf
073        mysqlProps.setProperty("useConfigs", "maxPerformance");
074
075        PROTOCOL_SPECIFIC_PROPS = HashTreePMap.<String, Properties>empty()
076                .plus("com.mysql.jdbc.Driver", mysqlProps)
077                .plus("org.maridadb.jdbc.Driver", mysqlProps);
078    }
079
080    /**
081     * Create a data source for the provided URL, relative to the current working directory.
082     *
083     * @param jdbcUrl URL to connect to
084     * @return a new data source
085     * @throws SQLException if unable to resolve a driver for the URL
086     * @since 2.0.0
087     */
088    public static HikariDataSource createDataSource(final String jdbcUrl) throws SQLException {
089        return createDataSource(jdbcUrl, Paths.get("."));
090    }
091
092    /**
093     * Create a data source for the provided {@code jdbcUrl}, with any filesystem
094     * paths made relative to {@code baseDir}.
095     *
096     * @param jdbcUrl URL to connect to
097     * @param baseDir base directory
098     * @return a new data source
099     * @throws SQLException if unable to resolve a driver for the URL
100     * @since 2.0.0
101     */
102    public static HikariDataSource createDataSource(final String jdbcUrl, final Path baseDir) throws SQLException {
103        // Based on Sponge`s code, but without alias handling and caching
104        final Matcher match = JDBC_URL_REGEX.matcher(requireNonNull(jdbcUrl, "jdbcUrl"));
105        if (!match.matches()) {
106            throw new IllegalArgumentException("URL " + jdbcUrl + " is not a valid JDBC URL");
107        }
108
109        final String protocol = match.group(1);
110        final boolean hasSlashes = match.group(2) != null;
111        final @Nullable String user = match.group(3);
112        final @Nullable String pass = match.group(4);
113        String serverDatabaseSpecifier = match.group(5);
114        final BiFunction<Path, String, String> derelativizer = PATH_CANONICALIZERS.get(protocol);
115        if (derelativizer != null) {
116            serverDatabaseSpecifier = derelativizer.apply(baseDir, serverDatabaseSpecifier);
117        }
118
119        final String unauthedUrl = new StringBuilder("jdbc:")
120                .append(protocol)
121                .append(hasSlashes ? "://" : ":")
122                .append(serverDatabaseSpecifier)
123                .toString();
124        final String driverClass = DriverManager.getDriver(unauthedUrl).getClass().getCanonicalName();
125
126        final HikariConfig config = new HikariConfig();
127        config.setUsername(user);
128        config.setPassword(pass);
129        config.setDriverClassName(driverClass);
130
131        // https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing for info on pool sizing
132        config.setMaximumPoolSize((Runtime.getRuntime().availableProcessors() * 2) + 1);
133        final @Nullable Properties driverSpecificProperties = PROTOCOL_SPECIFIC_PROPS.get(driverClass);
134        final Properties dsProps;
135        if (driverSpecificProperties == null) {
136            dsProps = new Properties();
137        } else {
138            dsProps = new Properties(driverSpecificProperties);
139        }
140        dsProps.setProperty("baseDir", baseDir.toAbsolutePath().toString());
141        config.setDataSourceProperties(dsProps);
142        config.setJdbcUrl(unauthedUrl);
143        return new HikariDataSource(config);
144    }
145
146    private Hikari() {}
147}