From 1be4f0950d987eaac2a9ca3907b86c05879effab Mon Sep 17 00:00:00 2001 From: igor nitto Date: Thu, 25 Jan 2018 13:38:51 +0000 Subject: [PATCH] Remove compile time dependency on JDBC drivers [ENT-1363] (#387) * Change JDBC drivers dependencies to integrationTestRuntimeOnly * Added smoke test validating JDBC driver registration located in "./plugins" directory * Some docsite tweaks --- .idea/compiler.xml | 2 + .../rpc/StandaloneCordaRPCJavaClientTest.java | 3 +- core/build.gradle | 3 + .../net/corda/core/PluginRegistrationTest.kt | 63 ++++++++++++++++++ core/src/smoke-test/resources/dummydriver.jar | Bin 0 -> 2195 bytes docs/source/changelog.rst | 3 + docs/source/node-database.rst | 26 ++++---- node/build.gradle | 24 +++++-- .../net/corda/node/internal/AbstractNode.kt | 6 ++ .../net/corda/smoketesting/NodeConfig.kt | 4 +- .../net/corda/smoketesting/NodeProcess.kt | 25 +++++-- 11 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 core/src/smoke-test/kotlin/net/corda/core/PluginRegistrationTest.kt create mode 100644 core/src/smoke-test/resources/dummydriver.jar diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 687b740285..2f959f2b02 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -41,6 +41,7 @@ + @@ -148,6 +149,7 @@ + diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 32e2747072..438af3fc91 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -49,7 +49,8 @@ public class StandaloneCordaRPCJavaClientTest { port.getAndIncrement(), true, Collections.singletonList(rpcUser), - true + true, + Collections.emptyList() ); @Before diff --git a/core/build.gradle b/core/build.gradle index bd443598d7..778c169d64 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -43,6 +43,9 @@ sourceSets { runtimeClasspath += main.output srcDir file('src/smoke-test/java') } + resources { + srcDir file('src/smoke-test/resources') + } } } diff --git a/core/src/smoke-test/kotlin/net/corda/core/PluginRegistrationTest.kt b/core/src/smoke-test/kotlin/net/corda/core/PluginRegistrationTest.kt new file mode 100644 index 0000000000..ca1556f2a4 --- /dev/null +++ b/core/src/smoke-test/kotlin/net/corda/core/PluginRegistrationTest.kt @@ -0,0 +1,63 @@ +package net.corda.core + +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.div +import net.corda.nodeapi.internal.config.User +import net.corda.smoketesting.NodeConfig +import net.corda.smoketesting.NodeProcess +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.FileOutputStream +import java.util.jar.JarOutputStream + + +class PluginRegistrationTest { + private companion object { + val pluginJarFile = PluginRegistrationTest::class.java.getResource("/dummydriver.jar")!!.path + } + + @Rule + @JvmField + val temporaryFolder = TemporaryFolder() + + @Test + fun `test plugin registration` () { + // Create node jarDir with an empty jar file in it + val jarDir = temporaryFolder.newFolder("jardir").path!! + JarOutputStream(FileOutputStream((jarDir / "empty.jar").toFile())).close () + val config = NodeConfig( + legalName = CordaX500Name(organisation = "org", locality = "Madrid", country = "ES"), + p2pPort = 30100, + rpcPort = 30101, + webPort = 30102, + isNotary = false, + users = listOf(User("_", "_", setOf("ALL"))), + jarDirs = listOf(jarDir)) + + // Check we do not have plugin on classpath + assertThatThrownBy({ Class.forName("net.corda.smoketesting.plugins.DummyJDBCDriver") }) + + // Install plugin Jars in node directory, then start the node and close it + val consoleOutput = temporaryFolder.newFile("node-stdout.txt") + val nodeJvmArgs = arrayOf("-logging-level", "DEBUG", "-no-local-shell", "-log-to-console") + NodeProcess.Factory(extraJvmArgs = nodeJvmArgs, redirectConsoleTo = consoleOutput) + .setupPlugins(config, listOf(pluginJarFile)) + .create(config) + .close() + val outputLines = consoleOutput.readLines() + val classpath = outputLines.filter { it.contains(" classpath:") }.last() + + // If DummyJdbcDriver has been registered it should have printed a message + assert(outputLines.count { it.contains("[DummyJDBCDriver] hello") } > 0) { + "Cannot find registration message from installed jdbc driver" + } + + // Check the printed classpath contains 'nodeJarDir' + assert(classpath.contains(jarDir)) { + "Expected to find $jarDir in printed classpath" + } + } +} + diff --git a/core/src/smoke-test/resources/dummydriver.jar b/core/src/smoke-test/resources/dummydriver.jar new file mode 100644 index 0000000000000000000000000000000000000000..0cb272f7c42a241f7fc75248ec078a7cb6d2de0e GIT binary patch literal 2195 zcmWIWW@h1HVBp|jSkPwf!vF+KAOZ+Df!NnI#8KDN&rP41ApowBtJ2(O)$AIVr9cJG zfLH`sp|7K#r<-eVh@P*TZ=WMqvw?`q_ong_tV*7fgC4IFneL@|^soOXZ{}GZLe>AS ziEW(Sm{?(axv`wzXC|M-WbS>6dy-NMIWIEp?VBL4=vSW@IcK_z+KF=)^lqfO+&bLC z`*DW$=AhM&t`!`f{(ItrCC_F_sx5U~Do~M|eelAI$uiRFPkT&J{LcdPR$gieir3NA z07DL}CON+-B@wSmaTJxsx%t_tC8@E2^?I$XBrQi_DWB26ua2PPseRV&>Bu`%j3=A7OtzvG~hf`%{nS-rT}_`^CY> zr~mBuUU&cRvwOQw&aeOb&AMUrjSqnbH?^17ifnlE*fwUS(&w#4VKp(wf6Li@_>d^+ zD<2&3^~1#Weo3|PR3%>7uCK?$oL4U0)s(nQSGdr;cYEuSkqjp3`q#W+;U0I^FEJMvhyV%}s6Z0{Nx3GkKb}n)H3Qi{5=T>hh<~3%+dnA)ZQ3 zhYJ?pyZ6+;LZFbTq(?2rc=HlBw}ZB8e>q8OvYmBHldBPDUCW~z>J)P{#69eRp_Guu zmKpDSK}dGTWR)T5HAS!qvZ*Mz^+6>_nPU2N7?HnZo3|GTp7ho^P@p7^A*u#o-H z4zbS6)p7nAZN*=YbxiA6vn_Py_LmRYoGVRx+JX<}8PBntbmQly+6Y6T@=eJVKPo1F zziV@$A@Hb`*42d()649h74^n%auM}OZcBW*|7=hDY|)ygXt^lSH3v^sP2v8Tmgw#> z@zaL5gUfd~^GdIto%nBs<@d`+PDJ!rWb4k0G2xE-@}!|HVd3i?;%l=O&9QB&+qm~N zTZP(nhqhlC_eu}?t~wfsQK@Bed$YM1M{VwRdqqLzBS7VUb(sW z#|)|UL6b_YxTkj=^HpnG7Vu$4RasWg%zelA?R7nA@AFek?4|osrYy_9*L*Je*)A!a zmdE(E=Ub=u?*#vs0SCOd_*p&X49Q%Z!f4TIc}tT$OT+4e?=Df@w!c%H&sjWidM2y% z_9ahT**~UpN%O9Szlc%tmVCa(M4F-Krpfw8e+Ke5kg-s&q4vafW%Zhx8mWN*v%xe*7PA2zn1wzHfz zXVZgxM5%+G;e~)X7GCNWrxulECZ}TNaT}oo1p}Z;Js?)Zsv|40EK#qxFh>uPtxtRF zUi3bv@8x+#``p>{e!8BzdMD3#U)MgN>3jZ+_O)WovuD?SDJm=~(>$f)t;+-~?HQRw zm~mGOKs$jzfZ?qph=w~DSsS)$0;CuOmNaStNw_woN(0#h^wJhoB13=x!(T@RMo8I< zPY0-cM(9w1>VTBi`1FBFYlJ>O6n!XVH?kq!s>fa%7Xw<=$5e}#Sc6kqU%RbhcFYiG_Js^A1ze|c(byBv~U7p8qgi$ GU>*QHZ}lty literal 0 HcmV?d00001 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 29970a8b30..19f11448e5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -15,6 +15,9 @@ UNRELEASED the table name for the `participants` collection. For an example on how the mapping can be done, see: DummyDealStateSchemaV1.PersistentDummyDealState +* JDBC drivers for SQL server and PostgresSQL are no longer bundled as part of Corda releases. If you are running a node + on such databases you need to provide the associated driver as described in :doc:`node-database`. + R3 Corda 3.0 Developer Preview ------------------------------ diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index 1ec3d35ad2..a6633842cd 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -36,18 +36,19 @@ Standalone database To run a node against a remote database modify node JDBC connection properties in `dataSourceProperties` entry and Hibernate properties in `database` entry - see :ref:`Node configuration `. -.. _sql_server_ref: +Corda will search for valid JDBC drivers either under the ``./plugins`` subdirectory of the node base directory or in one +of the paths specified by the ``jarDirs`` field of the node configuration. Please make sure a ``jar`` file containing drivers +supporting the database in use is present in one of these locations. SQL Azure and SQL Server ```````````````````````` -Corda supports SQL Server 2017 (14.0.3006.16) and Azure SQL (12.0.2000.8). +Corda has been tested with SQL Server 2017 (14.0.3006.16) and Azure SQL (12.0.2000.8), using Microsoft JDBC Driver 6.2. The minimum transaction isolation level ``database.transactionIsolationLevel`` is 'READ_COMMITTED'. The property ``database.schema`` is optional. -Corda ships with Microsoft JDBC Driver 6.2 for SQLServer out-of-the-box. Example node configuration for SQL Azure: -.. sourcecode:: none +.. sourcecode:: groovy dataSourceProperties { dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource" @@ -60,21 +61,17 @@ Example node configuration for SQL Azure: transactionIsolationLevel = READ_COMMITTED schema = [SCHEMA] } - -.. _postgres_ref: + jarDirs = [PATH_TO_JDBC_DRIVER_DIR] PostgreSQL ```````````````````````` - -Corda supports PostgreSQL 9.6 database preliminarily. -The property ``database.schema`` is optional. Currently only lowercase value is supported, -please ensure your database setup uses lowercase schema names (Corda sends an unquoted schema name -and PostgresSQL interprets the value as a lowercase one e.g. `AliceCorp` becomes `alicecorp`). -Corda ships with PostgreSQL JDBC Driver 42.1.4 out-of-the-box. +Corda has been tested on PostgreSQL 9.6 database, using PostgreSQL JDBC Driver 42.1.4. +The property ``database.schema`` is optional. The value of ``database.schema`` is automatically wrapped in double quotes +to preserve case-sensitivity (e.g. `AliceCorp` becomes `"AliceCorp"`, without quotes PostgresSQL would treat the value as `alicecorp`). Example node configuration for PostgreSQL: -.. sourcecode:: none +.. sourcecode:: groovy dataSourceProperties = { dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" @@ -85,4 +82,5 @@ Example node configuration for PostgreSQL: database = { transactionIsolationLevel = READ_COMMITTED schema = [SCHEMA] - } \ No newline at end of file + } + jarDirs = [PATH_TO_JDBC_DRIVER_DIR] diff --git a/node/build.gradle b/node/build.gradle index af2a905aae..349cba1a03 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -51,6 +51,13 @@ processTestResources { from file("$rootDir/config/test/jolokia-access.xml") } +// Map DB provider to driver artifact imported as runtime dependency in integration tests. +def jdbcRuntimeDependency = [ + 'integration-sql-server': "com.microsoft.sqlserver:mssql-jdbc:6.2.1.jre8", + 'integration-azure-sql' : "com.microsoft.sqlserver:mssql-jdbc:6.2.1.jre8", + 'integration-postgres' : "org.postgresql:postgresql:${postgresql_version}" +] + // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // build/reports/project/dependencies/index.html for green highlighted parts of the tree. @@ -122,12 +129,6 @@ dependencies { // For H2 database support in persistence compile "com.h2database:h2:$h2_version" - //TODO remove once we can put driver jar into a predefined directory - //JDBC driver can be passed to the Node at startup using setting the jarDirs property in the Node configuration file. - //For Postgres database support in persistence - compile "org.postgresql:postgresql:$postgresql_version" - //For Azure SQL and SQL Server support in persistence - compile 'com.microsoft.sqlserver:mssql-jdbc:6.2.1.jre8' // For the MySQLUniquenessProvider compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6' @@ -195,6 +196,17 @@ dependencies { testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" + // Add runtime-only dependency on the JDBC driver for the specified DB provider + def DB_PROVIDER = System.getProperty("databaseProvider") + if (DB_PROVIDER != null) { + final driverDependency = jdbcRuntimeDependency[DB_PROVIDER] + if (driverDependency != null) { + integrationTestRuntimeOnly driverDependency + } else { + throw new GradleException('Unsupported DB provider: ' + DB_PROVIDER) + } + } + // Jolokia JVM monitoring agent runtime "org.jolokia:jolokia-jvm:${jolokia_version}:agent" } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 5dd3d7cb2d..56cd445cb3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -84,6 +84,7 @@ import java.security.KeyStoreException import java.security.PublicKey import java.security.cert.X509Certificate import java.sql.Connection +import java.sql.DriverManager import java.time.Clock import java.time.Duration import java.util.* @@ -616,6 +617,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, class DatabaseConfigurationException(msg: String) : CordaException(msg) protected open fun initialiseDatabasePersistence(schemaService: SchemaService, identityService: IdentityService, insideTransaction: (CordaPersistence) -> T): T { + log.debug { + val driverClasses = DriverManager.getDrivers().asSequence().map { it.javaClass.name } + "Available JDBC drivers: $driverClasses" + } + val props = configuration.dataSourceProperties if (props.isNotEmpty()) { val database = configureDatabase(props, configuration.database, identityService, schemaService) diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 649b2ed63f..54cf6c940e 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -15,7 +15,8 @@ class NodeConfig( val webPort: Int, val isNotary: Boolean, val users: List, - val runMigration: Boolean = true + val runMigration: Boolean = true, + val jarDirs: List = emptyList() ) { companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) @@ -37,6 +38,7 @@ class NodeConfig( .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) .withValue("database", valueFor(mapOf("runMigration" to runMigration))) .withValue("useTestClock", valueFor(true)) + .withValue("jarDirs", valueFor(jarDirs)) return if (isNotary) { config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true))) } else { diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index ffe478998d..ac4d2f48b4 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -4,6 +4,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.internal.KryoClientSerializationScheme import net.corda.core.internal.copyTo +import net.corda.core.internal.copyToDirectory import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort @@ -11,6 +12,8 @@ import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.asContextEnv +import java.io.File +import java.nio.file.Files.createDirectories import java.nio.file.Path import java.nio.file.Paths import java.time.Instant @@ -51,7 +54,9 @@ class NodeProcess( // as a CorDapp for the nodes. class Factory( val buildDirectory: Path = Paths.get("build"), - val cordaJar: Path = Paths.get(this::class.java.getResource("/corda.jar").toURI()) + val cordaJar: Path = Paths.get(this::class.java.getResource("/corda.jar").toURI()), + val extraJvmArgs: Array = emptyArray(), + val redirectConsoleTo: File? = null ) { private companion object { val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") @@ -91,6 +96,13 @@ class NodeProcess( return NodeProcess(config, nodeDir, process, client) } + fun setupPlugins(config: NodeConfig, jarPaths: List): Factory { + (baseDirectory(config) / "plugins").createDirectories().also { + jarPaths.forEach { jar -> Paths.get(jar).copyToDirectory(it) } + } + return this + } + private fun waitForNode(process: Process, config: NodeConfig, client: CordaRPCClient) { val executor = Executors.newSingleThreadScheduledExecutor() try { @@ -121,11 +133,16 @@ class NodeProcess( } private fun startNode(nodeDir: Path): Process { + val redirectTo = redirectConsoleTo?.let { + ProcessBuilder.Redirect.appendTo(it) + } ?: ProcessBuilder.Redirect.INHERIT + val builder = ProcessBuilder() - .command(javaPath.toString(), "-Dcapsule.log=verbose", "-jar", cordaJar.toString()) + .command(javaPath.toString(), "-Dcapsule.log=verbose", + "-jar", cordaJar.toString(), *extraJvmArgs) .directory(nodeDir.toFile()) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(redirectTo) + .redirectOutput(redirectTo) builder.environment().putAll(mapOf( "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString()