/* * Copyright (c) 2014 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.werval.modules.jdbc; import java.net.URI; import java.net.URISyntaxException; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.naming.InitialContext; import javax.naming.NamingException; import io.werval.api.Application; import io.werval.api.Config; import io.werval.api.Plugin; import io.werval.api.exceptions.ActivationException; import io.werval.modules.jndi.JNDI; import io.werval.modules.metrics.Metrics; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import static io.werval.util.Strings.EMPTY; import static io.werval.util.Strings.isEmpty; import static io.werval.util.Strings.join; /** * JDBC Plugin that manage DataSources using HikariCP pools. */ public class JDBCPlugin implements Plugin<JDBC> { private static final String DEFAULT_DATASOURCE = "jdbc.default_datasource"; private static final String DATASOURCES = "jdbc.datasources"; private static final String METRICS = "jdbc.metrics"; private static final String LOG4JDBC_DRIVER = "net.sf.log4jdbc.sql.jdbcapi.DriverSpy"; private JDBC jdbc; @Override public Class<JDBC> apiType() { return JDBC.class; } @Override public List<Class<?>> dependencies( Config config ) { List<Class<?>> deps = new ArrayList<>(); if( config.has( DATASOURCES ) ) { Config allDsConfig = config.atPath( DATASOURCES ); for( String dsName : allDsConfig.subKeys() ) { if( allDsConfig.atKey( dsName ).has( "jndiName" ) ) { // At least one DataSource has to be registered in JNDI, depend on the JNDI plugin deps.add( JNDI.class ); break; } } } if( config.bool( METRICS ) ) { deps.add( Metrics.class ); } return deps; } @Override public JDBC api() { return jdbc; } @Override public void onActivate( Application application ) throws ActivationException { Config config = application.config(); Map<String, HikariDataSource> dataSources = new HashMap<>(); if( config.has( DATASOURCES ) ) { Config allDsConfig = config.atPath( DATASOURCES ); setupLog4Jdbc( application, allDsConfig ); for( String dsName : allDsConfig.subKeys() ) { Config dsConfig = allDsConfig.atKey( dsName ); HikariDataSource ds = createDataSource( dsName, dsConfig, application, config.bool( METRICS ) ); dataSources.put( dsName, ds ); } } jdbc = new JDBC( dataSources, config.string( DEFAULT_DATASOURCE ) ); } @Override public void onPassivate( Application application ) { if( jdbc != null ) { jdbc.passivate(); jdbc = null; } } private void setupLog4Jdbc( Application application, Config allDsConfig ) { // Load log4jdbc properties from application configuration Map<String, String> globalProperties = new HashMap<>(); if( application.config().has( "jdbc.log4jdbc" ) ) { application.config().stringMap( "jdbc.log4jdbc" ).forEach( (key, value) -> { globalProperties.put( "log4jdbc." + key, value ); } ); } // Load used drivers from datasources configuration Set<String> log4jdbcEnabledDrivers = new LinkedHashSet<>(); for( String dsName : allDsConfig.subKeys() ) { Config dsConfig = allDsConfig.atKey( dsName ); if( dsConfig.has( "log4jdbc" ) && dsConfig.bool( "log4jdbc" ) ) { log4jdbcEnabledDrivers.add( dsConfig.string( "driver" ) ); } } // Apply if appropriate if( !log4jdbcEnabledDrivers.isEmpty() ) { System.setProperty( "log4jdbc.drivers", join( log4jdbcEnabledDrivers, "," ) ); globalProperties.forEach( (key, value) -> { System.setProperty( key, value ); } ); } } private HikariDataSource createDataSource( String dsName, Config dsConfig, Application app, boolean metrics ) { // JDBC configuration boolean log4jdbc = dsConfig.has( "log4jdbc" ) && dsConfig.bool( "log4jdbc" ); String driver = dsConfig.string( "driver" ); String url = dsConfig.string( "url" ); String user = null; String password = null; try { // Extract username/password from the URL if available // This provide Heroku DATABASE_URL syntax support URI uri = new URI( url ); if( !isEmpty( uri.getUserInfo() ) ) { String[] userInfo = uri.getUserInfo().split( ":" ); if( userInfo.length > 0 ) { user = userInfo[0]; if( userInfo.length > 1 ) { password = userInfo[1]; } // Resolve Scheme if needed String scheme; switch( uri.getScheme() ) { case "postgres": scheme = "postgresql"; break; default: scheme = uri.getScheme(); break; } // Remove UserInfo url = scheme + "://" + ( uri.getHost() != null ? uri.getHost() : EMPTY ) + ( uri.getPort() != -1 ? ":" + uri.getPort() : EMPTY ) + ( uri.getPath() != null ? uri.getPath() : EMPTY ) + ( uri.getQuery() != null ? uri.getQuery() : EMPTY ) + ( uri.getFragment() != null ? uri.getFragment() : EMPTY ); } } } catch( URISyntaxException ex ) { throw new ActivationException( "Invalid JDBC URI: " + url, ex ); } if( !url.startsWith( "jdbc:" ) ) { url = "jdbc:" + url; } if( log4jdbc ) { url = url.substring( 0, 5 ) + "log4jdbc:" + url.substring( 5 ); } if( dsConfig.has( "user" ) ) { user = dsConfig.string( "user" ); } if( dsConfig.has( "password" ) ) { password = dsConfig.string( "password" ); } // Setup DataSource try { // Load Database Driver Explicitely DriverManager.registerDriver( (Driver) Class.forName( driver, true, app.classLoader() ).newInstance() ); if( log4jdbc ) { // Load log4jdbc Driver Explicitely DriverManager.registerDriver( (Driver) Class.forName( LOG4JDBC_DRIVER, true, app.classLoader() ).newInstance() ); } HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setPoolName( dsName ); hikariConfig.setDriverClassName( log4jdbc ? LOG4JDBC_DRIVER : driver ); hikariConfig.setJdbcUrl( url ); hikariConfig.setUsername( user ); hikariConfig.setPassword( password ); if( dsConfig.has( "autocommit" ) ) { hikariConfig.setAutoCommit( dsConfig.bool( "autocommit" ) ); } if( dsConfig.has( "isolation" ) ) { hikariConfig.setTransactionIsolation( dsConfig.string( "isolation" ) ); } if( dsConfig.has( "readOnly" ) ) { hikariConfig.setReadOnly( dsConfig.bool( "readOnly" ) ); } if( dsConfig.has( "catalog" ) ) { hikariConfig.setCatalog( dsConfig.string( "catalog" ) ); } // Pool configuration if( dsConfig.has( "minimumIdle" ) ) { hikariConfig.setMinimumIdle( dsConfig.intNumber( "minimumIdle" ) ); } if( dsConfig.has( "maximumPoolSize" ) ) { hikariConfig.setMaximumPoolSize( dsConfig.intNumber( "maximumPoolSize" ) ); } if( dsConfig.has( "connectionTimeout" ) ) { hikariConfig.setConnectionTimeout( dsConfig.milliseconds( "connectionTimeout" ) ); } if( dsConfig.has( "idleTimeout" ) ) { hikariConfig.setIdleTimeout( dsConfig.milliseconds( "idleTimeout" ) ); } if( dsConfig.has( "maxLifetime" ) ) { hikariConfig.setMaxLifetime( dsConfig.milliseconds( "maxLifetime" ) ); } if( dsConfig.has( "initializationFailFast" ) ) { hikariConfig.setInitializationFailFast( dsConfig.bool( "initializationFailFast" ) ); } if( dsConfig.has( "leakDetectionThreshold" ) ) { hikariConfig.setLeakDetectionThreshold( dsConfig.milliseconds( "leakDetectionThreshold" ) ); } if( dsConfig.has( "connectionInitSql" ) ) { hikariConfig.setConnectionInitSql( dsConfig.string( "connectionInitSql" ) ); } if( dsConfig.has( "connectionTestQuery" ) ) { hikariConfig.setConnectionTestQuery( dsConfig.string( "connectionTestQuery" ) ); } if( dsConfig.has( "registerMbeans" ) ) { hikariConfig.setRegisterMbeans( dsConfig.bool( "registerMbeans" ) ); } if( dsConfig.has( "isolateInternalQueries" ) ) { hikariConfig.setIsolateInternalQueries( dsConfig.bool( "isolateInternalQueries" ) ); } // Metrics if( metrics ) { hikariConfig.setMetricRegistry( app.plugin( Metrics.class ).metrics() ); } HikariDataSource hds = new HikariDataSource( hikariConfig ); // JNDI if( dsConfig.has( "jndiName" ) ) { String jndiName = dsConfig.string( "jndiName" ); new InitialContext().rebind( jndiName, hds ); } return hds; } catch( ClassNotFoundException | IllegalAccessException | InstantiationException | SQLException | NamingException ex ) { throw new ActivationException( "JDBC Plugin unable to create '" + dsName + "' DataSource", ex ); } } }