diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index 0d5c9deb52..327cbd06eb 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -37,13 +37,13 @@ class CordappScanningDriverTest { @StartableByRPC @InitiatingFlow - class ReceiveFlow(val otherParty: Party) : FlowLogic() { + class ReceiveFlow(private val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): String = initiateFlow(otherParty).receive().unwrap { it } } @InitiatedBy(ReceiveFlow::class) - open class SendClassFlow(val otherPartySession: FlowSession) : FlowLogic() { + open class SendClassFlow(private val otherPartySession: FlowSession) : FlowLogic() { @Suspendable override fun call() = otherPartySession.send(javaClass.name) } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 673d7bf73a..41076753a3 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -85,8 +85,10 @@ open class Node(configuration: NodeConfiguration, } private val sameVmNodeCounter = AtomicInteger() - val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" - val scanPackagesSeparator = "," + + const val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages" + const val scanPackagesSeparator = "," + private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader { return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages -> CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator)) diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt index 98947dbe78..9c3b2d60b4 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappLoader.kt @@ -1,5 +1,6 @@ package net.corda.node.internal.cordapp +import com.google.common.cache.CacheBuilder import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.contracts.Contract @@ -19,18 +20,17 @@ import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.serialization.DefaultWhitelist import org.apache.commons.collections4.map.LRUMap -import java.io.File -import java.io.FileOutputStream import java.lang.reflect.Modifier import java.net.JarURLConnection -import java.net.URI import java.net.URL import java.net.URLClassLoader import java.nio.file.Files import java.nio.file.Path +import java.nio.file.Paths import java.nio.file.attribute.FileTime import java.time.Instant import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.reflect.KClass @@ -69,10 +69,21 @@ class CordappLoader private constructor(private val cordappJarPaths: List, CordappLoader>(1000) + private val cordappLoadersCache = CacheBuilder.newBuilder().softValues().build, CordappLoader>() + private val generatedCordapps = ConcurrentHashMap() + + private fun simplifyScanPackages(scanPackages: List): List { + return scanPackages.sorted().fold(emptyList()) { listSoFar, packageName -> + when { + listSoFar.isEmpty() -> listOf(packageName) + packageName.startsWith(listSoFar.last()) -> listSoFar // Squash ["com.foo", "com.foo.bar"] into just ["com.foo"] + else -> listSoFar + packageName + } + } + } /** * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath @@ -86,8 +97,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List): CordappLoader { - return cordappLoadersCache.computeIfAbsent(testPackages, { CordappLoader(testPackages.flatMap(this::createScanPackage)) }) + val urls = simplifyScanPackages(testPackages).flatMap(this::getPackageURLs) + return cordappLoadersCache.asMap().computeIfAbsent(urls, ::CordappLoader) } /** @@ -110,64 +122,57 @@ class CordappLoader private constructor(private val cordappJarPaths: List) = CordappLoader(scanJars.map { RestrictedURL(it, null) }) - private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME - - private fun createScanPackage(scanPackage: String): List { + private fun getPackageURLs(scanPackage: String): List { val resource = scanPackage.replace('.', '/') return this::class.java.classLoader.getResources(resource) .asSequence() - .map { path -> - if (path.protocol == "jar") { + .map { url -> + if (url.protocol == "jar") { // When running tests from gradle this may be a corda module jar, so restrict to scanPackage: - RestrictedURL((path.openConnection() as JarURLConnection).jarFileURL, scanPackage) + RestrictedURL((url.openConnection() as JarURLConnection).jarFileURL, scanPackage) } else { // No need to restrict as createDevCordappJar has already done that: - RestrictedURL(createDevCordappJar(scanPackage, path, resource).toURL(), null) + RestrictedURL(createDevCordappJar(scanPackage, url, resource).toUri().toURL(), null) } } .toList() } /** Takes a package of classes and creates a JAR from them - only use in tests. */ - private fun createDevCordappJar(scanPackage: String, path: URL, jarPackageName: String): URI { - if (!generatedCordapps.contains(path)) { - val cordappDir = File("build/tmp/generated-test-cordapps") - cordappDir.mkdirs() - val cordappJAR = File(cordappDir, "$scanPackage-${UUID.randomUUID()}.jar") - logger.info("Generating a test-only cordapp of classes discovered in $scanPackage at $cordappJAR") - FileOutputStream(cordappJAR).use { - JarOutputStream(it).use { jos -> - val scanDir = File(path.toURI()) - scanDir.walkTopDown().forEach { - val entryPath = jarPackageName + "/" + scanDir.toPath().relativize(it.toPath()).toString().replace('\\', '/') - val time = FileTime.from(Instant.EPOCH) - val entry = ZipEntry(entryPath).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time) - jos.putNextEntry(entry) - if (it.isFile) { - Files.copy(it.toPath(), jos) - } - jos.closeEntry() + private fun createDevCordappJar(scanPackage: String, url: URL, resource: String): Path { + return generatedCordapps.computeIfAbsent(url) { + // TODO Using the driver in out-of-process mode causes each node to have their own copy of the same dev CorDapps + val cordappDir = (Paths.get("build") / "tmp" / "generated-test-cordapps").createDirectories() + val cordappJar = cordappDir / "$scanPackage-${UUID.randomUUID()}.jar" + logger.info("Generating a test-only CorDapp of classes discovered for package $scanPackage in $url: $cordappJar") + JarOutputStream(Files.newOutputStream(cordappJar)).use { jos -> + val scanDir = Paths.get(url.toURI()) + Files.walk(scanDir).use { it.forEach { + val entryPath = "$resource/${scanDir.relativize(it).toString().replace('\\', '/')}" + val time = FileTime.from(Instant.EPOCH) + val entry = ZipEntry(entryPath).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time) + jos.putNextEntry(entry) + if (it.isRegularFile()) { + Files.copy(it, jos) } - } + jos.closeEntry() + } } } - generatedCordapps[path] = cordappJAR.toURI() + cordappJar } - - return generatedCordapps[path]!! } - private fun getCordappsInDirectory(cordappsDir: Path): List { + private fun getNodeCordappURLs(baseDir: Path): List { + val cordappsDir = baseDir / CORDAPPS_DIR_NAME return if (!cordappsDir.exists()) { emptyList() } else { cordappsDir.list { - it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() + it.filter { it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList() } } } - private val generatedCordapps = mutableMapOf() - /** A list of the core RPC flows present in Corda */ private val coreRPCFlows = listOf( ContractUpgradeFlow.Initiate::class.java, @@ -267,7 +272,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List(1000) private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { - logger.info("Scanning CorDapp in $cordappJarPath") + logger.info("Scanning CorDapp in ${cordappJarPath.url}") return cachedScanResult.computeIfAbsent(cordappJarPath, { RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix) }) @@ -297,10 +302,9 @@ class CordappLoader private constructor(private val cordappJarPaths: List() { @Suspendable - override fun call() { - } + override fun call() = Unit } @InitiatedBy(DummyFlow::class) -class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() { +class LoaderTestFlow(@Suppress("UNUSED_PARAMETER") unusedSession: FlowSession) : FlowLogic() { @Suspendable - override fun call() { - } + override fun call() = Unit } @SchedulableFlow class DummySchedulableFlow : FlowLogic() { @Suspendable - override fun call() { - } + override fun call() = Unit } @StartableByRPC class DummyRPCFlow : FlowLogic() { @Suspendable - override fun call() { - } + override fun call() = Unit } class CordappLoaderTest { private companion object { - val testScanPackages = listOf("net.corda.node.internal.cordapp") - val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" - val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" + const val testScanPackage = "net.corda.node.internal.cordapp" + const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract" + const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator" } @Test fun `test that classes that aren't in cordapps aren't loaded`() { // Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp val loader = CordappLoader.createDefault(Paths.get(".")) - assertThat(loader.cordapps) - .hasSize(1) - .contains(CordappLoader.coreCordapp) + assertThat(loader.cordapps).containsOnly(CordappLoader.coreCordapp) } @Test @@ -71,7 +65,7 @@ class CordappLoaderTest { @Test fun `flows are loaded by loader`() { - val loader = CordappLoader.createWithTestPackages(testScanPackages) + val loader = CordappLoader.createWithTestPackages(listOf(testScanPackage)) val actual = loader.cordapps.toTypedArray() // One core cordapp, one cordapp from this source tree, and two others due to identically named locations @@ -85,6 +79,20 @@ class CordappLoaderTest { assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) } + @Test + fun `duplicate packages are ignored`() { + val loader = CordappLoader.createWithTestPackages(listOf(testScanPackage, testScanPackage)) + val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } + assertThat(cordapps).hasSize(1) + } + + @Test + fun `sub-packages are ignored`() { + val loader = CordappLoader.createWithTestPackages(listOf("net.corda", testScanPackage)) + val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows } + assertThat(cordapps).hasSize(1) + } + // This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader // being used internally. Later iterations will use a classloader per cordapp and this test can be retired. @Test