From cea5b223198330fa8ee49139d12e068e60eef616 Mon Sep 17 00:00:00 2001 From: simonbrandhof Date: Thu, 14 Apr 2011 16:42:01 +0200 Subject: [PATCH] SONAR-2359 Most of properties sonar.jdbc.* should not be required when JDBC datasource is loaded from JNDI --- .../src/main/assembly/conf/sonar.properties | 34 +++--- .../session/AbstractDatabaseConnector.java | 2 + ...=> CustomHibernateConnectionProvider.java} | 23 ++-- .../database/JndiDatabaseConnector.java | 108 ++++++------------ .../database/UniqueDatasourceFactory.java | 48 -------- .../database/JndiDatabaseConnectorTest.java | 20 ++-- .../database/UniqueDatasourceFactoryTest.java | 62 ---------- 7 files changed, 81 insertions(+), 216 deletions(-) rename sonar-server/src/main/java/org/sonar/server/database/{JndiException.java => CustomHibernateConnectionProvider.java} (66%) delete mode 100644 sonar-server/src/main/java/org/sonar/server/database/UniqueDatasourceFactory.java delete mode 100644 sonar-server/src/test/java/org/sonar/server/database/UniqueDatasourceFactoryTest.java diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index aca69ce9dbe..23823ae59ec 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -35,14 +35,21 @@ # #--------------------------------------------------------- +#----- Credentials +# Permissions to create tables and indexes must be granted to JDBC user. +# The schema must be created first. +sonar.jdbc.username: sonar +sonar.jdbc.password: sonar + #----- Embedded database Derby # Note : it does accept connections from remote hosts, so the # sonar server and the maven plugin must be executed on the same host. -# Comment the 3 following lines to deactivate the default embedded database +# Comment the following lines to deactivate the default embedded database. sonar.jdbc.url: jdbc:derby://localhost:1527/sonar;create=true sonar.jdbc.driverClassName: org.apache.derby.jdbc.ClientDriver -sonar.jdbc.validationQuery: values(1) +#sonar.jdbc.validationQuery: values(1) + # directory containing Derby database files. By default it's the /data directory in the sonar installation. #sonar.embeddedDatabase.dataDir: # derby embedded database server listening port, defaults to 1527 @@ -52,14 +59,14 @@ sonar.jdbc.validationQuery: values(1) #sonar.derby.drda.host: 0.0.0.0 #----- MySQL 5.x/6.x -# Comment the embedded database and uncomment the following lines to use MySQL +# Comment the embedded database and uncomment the following properties to use MySQL. The validation query is optional. #sonar.jdbc.url: jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8 #sonar.jdbc.driverClassName: com.mysql.jdbc.Driver #sonar.jdbc.validationQuery: select 1 #----- Oracle 10g/11g -# Comment the embedded database and uncomment the following lines to use Oracle +# Comment the embedded database and uncomment the following properties to use Oracle. The validation query is optional. #sonar.jdbc.url: jdbc:oracle:thin:@localhost/XE #sonar.jdbc.driverClassName: oracle.jdbc.driver.OracleDriver #sonar.jdbc.validationQuery: select 1 from dual @@ -69,7 +76,7 @@ sonar.jdbc.validationQuery: values(1) #sonar.hibernate.default_schema: sonar #----- PostgreSQL 8.x, 9.x -# uncomment the 3 following lines to use PostgreSQL +# Uncomment the following properties to use PostgreSQL. The validation query is optional. #sonar.jdbc.url: jdbc:postgresql://localhost/sonar #sonar.jdbc.driverClassName: org.postgresql.Driver #sonar.jdbc.validationQuery: select 1 @@ -77,14 +84,13 @@ sonar.jdbc.validationQuery: values(1) #----- Microsoft SQLServer # The Jtds open source driver is available in extensions/jdbc-driver/mssql. More details on http://jtds.sourceforge.net +# The validation query is optional. #sonar.jdbc.url: jdbc:jtds:sqlserver://localhost;databaseName=SONAR;SelectMethod=Cursor #sonar.jdbc.driverClassName: net.sourceforge.jtds.jdbc.Driver #sonar.jdbc.validationQuery: select 1 -#----- Global database settings -sonar.jdbc.username: sonar -sonar.jdbc.password: sonar +#----- Connection pool settings sonar.jdbc.maxActive: 10 sonar.jdbc.maxIdle: 5 sonar.jdbc.minIdle: 2 @@ -92,14 +98,12 @@ sonar.jdbc.maxWait: 5000 sonar.jdbc.minEvictableIdleTimeMillis: 600000 sonar.jdbc.timeBetweenEvictionRunsMillis: 30000 -# Transaction isolation level. Default driver setting is used by default. -# Values : 1 (TRANSACTION_READ_UNCOMMITED), 2 (TRANSACTION_READ_COMMITTED), 4 (TRANSACTION_REPEATABLE_READ), 8 (TRANSACTION_SERIALIZABLE) -#sonar.jdbc.defaultTransactionIsolation: 2 - -# When packaged in a WAR, JDBC datasource can be configured into the application server then registered to JNDI. -# In such a case Sonar uses this datasource to connect to database, else if binds itself its own datasource. -# Note : Jonas does not accept to bind subcontexts, so name should be something like 'jdbc-sonar', without slashes. +#----- JDBC Datasource bounded to JNDI +# When sonar webapp is deployed into a JEE server, the JDBC datasource can be configured into the JEE server and registered into JNDI. +# In such a case Sonar uses this datasource to connect to database. +# If you activate this feature, then the properties starting with "sonar.jdbc." can be commented, EXCEPT "sonar.jdbc.driverClassName". +# The JDBC driver must still be deployed into the directory /extensions/jdbc-driver. #sonar.jdbc.jndiName: jdbc/sonar # If you don't use the default JDBC drivers, as listed above, then you have to explicitly set the dialect to use. diff --git a/sonar-core/src/main/java/org/sonar/jpa/session/AbstractDatabaseConnector.java b/sonar-core/src/main/java/org/sonar/jpa/session/AbstractDatabaseConnector.java index 3dcc9908278..3ba4cbc5d47 100644 --- a/sonar-core/src/main/java/org/sonar/jpa/session/AbstractDatabaseConnector.java +++ b/sonar-core/src/main/java/org/sonar/jpa/session/AbstractDatabaseConnector.java @@ -24,6 +24,7 @@ import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.database.DatabaseProperties; +import org.sonar.api.utils.Logs; import org.sonar.jpa.dialect.Dialect; import org.sonar.jpa.dialect.DialectRepository; import org.sonar.jpa.entity.SchemaMigration; @@ -106,6 +107,7 @@ public abstract class AbstractDatabaseConnector implements DatabaseConnector { throw new DatabaseException(databaseVersion, SchemaMigration.LAST_VERSION); } if (upToDate) { + Logs.INFO.info("Initializing Hibernate"); factory = createEntityManagerFactory(); operational = true; } diff --git a/sonar-server/src/main/java/org/sonar/server/database/JndiException.java b/sonar-server/src/main/java/org/sonar/server/database/CustomHibernateConnectionProvider.java similarity index 66% rename from sonar-server/src/main/java/org/sonar/server/database/JndiException.java rename to sonar-server/src/main/java/org/sonar/server/database/CustomHibernateConnectionProvider.java index 3f7a0887b04..35d83ecce78 100644 --- a/sonar-server/src/main/java/org/sonar/server/database/JndiException.java +++ b/sonar-server/src/main/java/org/sonar/server/database/CustomHibernateConnectionProvider.java @@ -17,21 +17,22 @@ * License along with Sonar; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ + package org.sonar.server.database; -public class JndiException extends RuntimeException { - public JndiException() { - } +import org.hibernate.HibernateException; +import org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider; - public JndiException(String s) { - super(s); - } +import javax.sql.DataSource; +import java.util.Properties; - public JndiException(String s, Throwable throwable) { - super(s, throwable); - } +public class CustomHibernateConnectionProvider extends InjectedDataSourceConnectionProvider { + + static DataSource datasource; - public JndiException(Throwable throwable) { - super(throwable); + @Override + public void configure(Properties props) throws HibernateException { + setDataSource(datasource); + super.configure(props); } } diff --git a/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java b/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java index 1d235ef0cf1..e78ba0bacc4 100644 --- a/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java +++ b/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java @@ -20,12 +20,19 @@ package org.sonar.server.database; import org.apache.commons.configuration.Configuration; +import org.apache.commons.dbcp.BasicDataSourceFactory; +import org.apache.commons.lang.StringUtils; +import org.hibernate.cfg.Environment; +import org.sonar.api.CoreProperties; import org.sonar.api.database.DatabaseProperties; import org.sonar.api.utils.Logs; +import org.sonar.api.utils.SonarException; import org.sonar.jpa.entity.SchemaMigration; import org.sonar.jpa.session.AbstractDatabaseConnector; -import javax.naming.*; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @@ -34,10 +41,13 @@ import java.util.Properties; public class JndiDatabaseConnector extends AbstractDatabaseConnector { + static final String JNDI_ENV_CONTEXT = "java:comp/env"; private DataSource datasource = null; + private String jndiKey; public JndiDatabaseConnector(Configuration configuration) { super(configuration, false); + jndiKey = getConfiguration().getString(DatabaseProperties.PROP_JNDI_NAME); } @Override @@ -53,11 +63,10 @@ public class JndiDatabaseConnector extends AbstractDatabaseConnector { @Override public void start() { if (!isStarted()) { - // get the datasource from JNDI - datasource = loadDatasourceFromJndi(); - // bind the datasource to JNDI if it is not already done - if (datasource == null) { - createAndBindDatasource(); + if (StringUtils.isNotBlank(jndiKey)) { + loadJndiDatasource(); + } else { + createDatasource(); } } if (!super.isOperational()) { @@ -71,81 +80,47 @@ public class JndiDatabaseConnector extends AbstractDatabaseConnector { super.stop(); } - private String getJndiName() { - return getConfiguration().getString(DatabaseProperties.PROP_JNDI_NAME, "jdbc/sonar"); - } - private DataSource loadDatasourceFromJndi() { + private void loadJndiDatasource() { Context ctx; try { ctx = new InitialContext(); - } catch (NamingException e) { - throw new JndiException("can not instantiate a JNDI context", e); - } - try { - String jndiName = getJndiName(); - DataSource source = (DataSource) ctx.lookup(jndiName); - Logs.INFO.info("Use JDBC datasource from JNDI, name=" + jndiName); - return source; } catch (NamingException e) { - // datasource not found - } finally { - try { - ctx.close(); - } catch (NamingException e) { - } + throw new SonarException("Can not instantiate JNDI context", e); } - return null; - } - private void createAndBindDatasource() { - Reference ref = createDatasourceReference(); - - // bind the datasource to JNDI - Context ctx = null; try { - ctx = new InitialContext(); - createJNDISubContexts(ctx, getJndiName()); - ctx.rebind(getJndiName(), ref); - datasource = (DataSource) ctx.lookup(getJndiName()); - Logs.INFO.info("JDBC datasource bound to JNDI, name=" + getJndiName()); + Context envCtx = (Context) ctx.lookup(JNDI_ENV_CONTEXT); + datasource = (DataSource)envCtx.lookup(jndiKey); + Logs.INFO.info("JDBC datasource loaded from JNDI: " + jndiKey); } catch (NamingException e) { - throw new JndiException("Can not bind JDBC datasource to JNDI", e); + throw new SonarException("JNDI context of JDBC datasource not found: " + jndiKey, e); } finally { - if (ctx != null) { - try { - ctx.close(); - } catch (NamingException e) { - } + try { + ctx.close(); + } catch (NamingException e) { } } } - private Reference createDatasourceReference() { + private void createDatasource() { try { - Reference ref = new Reference(DataSource.class.getName(), UniqueDatasourceFactory.class.getName(), null); + Logs.INFO.info("Creating JDBC datasource"); + Properties properties = new Properties(); Configuration dsConfig = getConfiguration().subset("sonar.jdbc"); for (Iterator it = dsConfig.getKeys(); it.hasNext();) { String key = it.next(); - String value = dsConfig.getString(key); - ref.add(new StringRefAddr(key, value)); - - // backward compatibility - if (value != null && key.equals("user")) { - ref.add(new StringRefAddr("username", value)); - } - if (value != null && key.equals("driver")) { - ref.add(new StringRefAddr("driverClassName", value)); - } + properties.setProperty(key, dsConfig.getString(key)); } - return ref; + + datasource = BasicDataSourceFactory.createDataSource(properties); + CustomHibernateConnectionProvider.datasource=datasource; } catch (Exception e) { - throw new RuntimeException("Cannot create the JDBC datasource", e); + throw new SonarException("Fail to connect to database", e); } - } public Connection getConnection() throws SQLException { @@ -161,21 +136,10 @@ public class JndiDatabaseConnector extends AbstractDatabaseConnector { @Override public void setupEntityManagerFactory(Properties factoryProps) { - factoryProps.put("hibernate.connection.datasource", getJndiName()); - } - - private void createJNDISubContexts(Context ctx, String jndiBinding) throws NamingException { - Name name = new CompositeName(jndiBinding); - for (int i = 0; i < name.size() - 1; i++) { - String namingContext = name.get(i); - try { - Object obj = ctx.lookup(namingContext); - if (!(obj instanceof Context)) { - throw new NamingException(namingContext + " is not a JNDI Context"); - } - } catch (NameNotFoundException ex) { - ctx = ctx.createSubcontext(namingContext); - } + if (StringUtils.isNotBlank(jndiKey)) { + factoryProps.put(Environment.DATASOURCE, JNDI_ENV_CONTEXT + "/" + jndiKey); + } else { + factoryProps.put(Environment.CONNECTION_PROVIDER, CustomHibernateConnectionProvider.class.getName()); } } diff --git a/sonar-server/src/main/java/org/sonar/server/database/UniqueDatasourceFactory.java b/sonar-server/src/main/java/org/sonar/server/database/UniqueDatasourceFactory.java deleted file mode 100644 index 58f8e515f7b..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/database/UniqueDatasourceFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * Sonar is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.server.database; - -import org.apache.commons.dbcp.BasicDataSourceFactory; - -import java.util.Hashtable; -import javax.naming.Context; -import javax.naming.Name; -import javax.sql.DataSource; - -public class UniqueDatasourceFactory extends BasicDataSourceFactory { - - /** - * getObjectInstance can be called multiple times (at each jndi.lookup calls), BasicDataSourceFactory getObjectInstance - * method impl return each times a new datasource, so cache it and make sure that this factory - * always return the same datasource object instance - */ - private static DataSource ds = null; - - @Override - public Object getObjectInstance(Object arg0, Name arg1, Context arg2, Hashtable arg3) throws Exception { - synchronized (UniqueDatasourceFactory.class) { - if (ds == null) { - ds = (DataSource)super.getObjectInstance(arg0, arg1, arg2, arg3); - } - } - return ds; - } - -} diff --git a/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java b/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java index 39a266ec0a4..27e55256fc7 100644 --- a/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java +++ b/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java @@ -20,6 +20,7 @@ package org.sonar.server.database; import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.PropertiesConfiguration; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -49,9 +50,9 @@ public class JndiDatabaseConnectorTest { @Before public void setup() { - conf = mock(Configuration.class); - when(conf.getString(eq(DatabaseProperties.PROP_JNDI_NAME), anyString())).thenReturn("test"); - when(conf.getString(eq(DatabaseProperties.PROP_DIALECT))).thenReturn(DatabaseProperties.DIALECT_HSQLDB); + conf = new PropertiesConfiguration(); + conf.setProperty(DatabaseProperties.PROP_JNDI_NAME, "jdbc/sonar"); + conf.setProperty(DatabaseProperties.PROP_DIALECT, DatabaseProperties.DIALECT_HSQLDB); currentInitialContextFacto = System.getProperty(Context.INITIAL_CONTEXT_FACTORY); connector = getTestJndiConnector(conf); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, TestInitialContextFactory.class.getName()); @@ -80,7 +81,7 @@ public class JndiDatabaseConnectorTest { @Test public void transactionIsolationCorrectlySet() throws Exception { int fakeTransactionIsolation = 9; - when(conf.getInteger(eq(DatabaseProperties.PROP_ISOLATION), (Integer) anyObject())).thenReturn(fakeTransactionIsolation); + conf.setProperty(DatabaseProperties.PROP_ISOLATION, fakeTransactionIsolation); connector.start(); Connection c = connector.getConnection(); // start method call get a connection to test it, so total number is 2 @@ -109,19 +110,22 @@ public class JndiDatabaseConnectorTest { private Connection c; public Context getInitialContext(Hashtable env) { - Context ctx = mock(Context.class); + Context envContext = mock(Context.class); + Context context = mock(Context.class); DataSource ds = mock(DataSource.class); DatabaseMetaData m = mock(DatabaseMetaData.class); c = mock(Connection.class); try { - when(ctx.lookup(anyString())).thenReturn(ds); + when(envContext.lookup(JndiDatabaseConnector.JNDI_ENV_CONTEXT)).thenReturn(context); + when(context.lookup("jdbc/sonar")).thenReturn(ds); when(ds.getConnection()).thenReturn(c); - when(m.getURL()).thenReturn("testdbcUrl"); + when(m.getURL()).thenReturn("jdbc:test"); when(c.getMetaData()).thenReturn(m); } catch (Exception e) { throw new RuntimeException(e); } - return ctx; + + return envContext; } } diff --git a/sonar-server/src/test/java/org/sonar/server/database/UniqueDatasourceFactoryTest.java b/sonar-server/src/test/java/org/sonar/server/database/UniqueDatasourceFactoryTest.java deleted file mode 100644 index 34d54f642a1..00000000000 --- a/sonar-server/src/test/java/org/sonar/server/database/UniqueDatasourceFactoryTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * Sonar is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.server.database; - -import org.junit.Test; - -import java.util.Hashtable; -import javax.naming.Context; -import javax.naming.Name; -import javax.naming.Reference; -import javax.sql.DataSource; - -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class UniqueDatasourceFactoryTest { - - @Test - public void testGetObjectInstance() throws Exception { - Reference ref = mock(Reference.class); - Name name = mock(Name.class); - Context ctx = mock(Context.class); - - when(ref.getClassName()).thenReturn(DataSource.class.getName()); - - UniqueDatasourceFactory factory = new UniqueDatasourceFactory(); - DataSource ds = (DataSource) factory.getObjectInstance(ref, name, ctx, new Hashtable()); - DataSource ds2 = (DataSource) factory.getObjectInstance(ref, name, ctx, new Hashtable()); - assertNotNull(ds); - assertNotNull(ds2); - // must be the same memory reference - assertTrue(ds == ds2); - - // new factory instance - factory = new UniqueDatasourceFactory(); - DataSource ds3 = (DataSource) factory.getObjectInstance(ref, name, ctx, new Hashtable()); - assertNotNull(ds3); - assertTrue(ds == ds3); - assertTrue(ds == ds2); - - } - -} -- 2.39.5