diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index f0b65c5a84..2d6467076b 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -41,6 +41,8 @@
+
+
diff --git a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
index 2df896cbbe..ea9557d7c9 100644
--- a/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
+++ b/core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
@@ -17,6 +17,7 @@ import java.net.URL
* @property contractClassNames List of contracts
* @property initiatedFlows List of initiatable flow classes
* @property rpcFlows List of RPC initiable flows classes
+ * @property schedulableFlows List of flows startable by the scheduler
* @property servies List of RPC services
* @property plugins List of Corda plugin registries
* @property customSchemas List of custom schemas
@@ -27,6 +28,7 @@ interface Cordapp {
val contractClassNames: List
val initiatedFlows: List>>
val rpcFlows: List>>
+ val schedulableFlows: List>>
val services: List>
val plugins: List
val customSchemas: Set
diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt
index 38a5a0ecf4..8cfda77d42 100644
--- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt
+++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt
@@ -12,6 +12,7 @@ data class CordappImpl(
override val contractClassNames: List,
override val initiatedFlows: List>>,
override val rpcFlows: List>>,
+ override val schedulableFlows: List>>,
override val services: List>,
override val plugins: List,
override val customSchemas: Set,
diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt
index 39de4816a9..3bba0b8ae6 100644
--- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt
+++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt
@@ -114,7 +114,8 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr
}
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val contractAttachments = findAttachmentContracts(resolvedInputs, resolveContractAttachment, resolveAttachment)
- val attachments = contractAttachments + (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) }).distinct()
+ // Order of attachments is important since contracts may refer to indexes so only append automatic attachments
+ val attachments = (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + contractAttachments).distinct()
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt)
}
diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt
index 03b9a9435e..4a344aec4e 100644
--- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt
+++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt
@@ -1,5 +1,6 @@
package net.corda.node.services
+import net.corda.client.rpc.RPCException
import net.corda.core.contracts.Contract
import net.corda.core.contracts.PartyAndReference
import net.corda.core.cordapp.CordappProvider
@@ -13,6 +14,7 @@ import net.corda.core.serialization.SerializationFactory
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
+import net.corda.core.utilities.loggerFor
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.nodeapi.User
@@ -21,6 +23,7 @@ import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.driver.driver
import net.corda.testing.node.MockServices
+import net.corda.testing.resetTestSerialization
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@@ -37,9 +40,10 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
override val cordappProvider: CordappProvider = provider
}
- companion object {
- private val isolatedJAR = this::class.java.getResource("isolated.jar")!!
- private val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
+ private companion object {
+ val logger = loggerFor()
+ val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
+ val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
}
private lateinit var services: Services
@@ -67,19 +71,20 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
assertEquals(expected, actual)
}
- // TODO - activate this test
- // @Test
+ @Test
fun `test that attachments retrieved over the network are not used for code`() {
driver(initialiseSerialization = false) {
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
// Copy the app jar to the first node. The second won't have it.
val path = (baseDirectory(bankAName.toString()) / "plugins").createDirectories() / "isolated.jar"
+ logger.info("Installing isolated jar to $path")
isolatedJAR.openStream().buffered().use { input ->
Files.newOutputStream(path).buffered().use { output ->
input.copyTo(output)
}
}
+
val admin = User("admin", "admin", permissions = setOf("ALL"))
val (bankA, bankB) = listOf(
startNode(providedName = bankAName, rpcUsers = listOf(admin)),
@@ -95,7 +100,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
val proxy = rpc.proxy
val party = proxy.wellKnownPartyFromX500Name(bankBName)!!
- assertFailsWith("xxx") {
+ assertFailsWith("net.corda.client.rpc.RPCException: net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
proxy.startFlowDynamic(clazz, party).returnValue.getOrThrow()
}
}
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 6ba72a75d6..ec5034ccdb 100644
--- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
@@ -147,6 +147,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
protected lateinit var database: CordaPersistence
protected var dbCloser: (() -> Any?)? = null
lateinit var cordappProvider: CordappProviderImpl
+ protected val cordappLoader by lazy { makeCordappLoader() }
protected val _nodeReadyFuture = openFuture()
/** Completes once the node has successfully registered with the network map service
@@ -378,7 +379,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
*/
private fun makeServices(): MutableList {
checkpointStorage = DBCheckpointStorage()
- cordappProvider = CordappProviderImpl(makeCordappLoader())
+ cordappProvider = CordappProviderImpl(cordappLoader)
_services = ServiceHubInternalImpl()
attachments = NodeAttachmentService(services.monitoringService.metrics)
cordappProvider.start(attachments)
@@ -399,10 +400,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages")
return if (CordappLoader.testPackages.isNotEmpty()) {
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
- CordappLoader.createWithTestPackages(CordappLoader.testPackages)
+ CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, CordappLoader.testPackages)
} else if (scanPackages != null) {
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
- CordappLoader.createWithTestPackages(scanPackages.split(","))
+ CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, scanPackages.split(","))
} else {
CordappLoader.createDefault(configuration.baseDirectory)
}
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 bc4dcb386f..17660cfe37 100644
--- a/node/src/main/kotlin/net/corda/node/internal/Node.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt
@@ -14,6 +14,7 @@ import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.utilities.*
import net.corda.node.VersionInfo
+import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.serialization.NodeClock
import net.corda.node.services.RPCUserService
@@ -340,14 +341,15 @@ open class Node(override val configuration: FullNodeConfiguration,
}
private fun initialiseSerialization() {
+ val classloader = cordappLoader.appClassLoader
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme())
}
- SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
- SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT
- SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT
- SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT
+ SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT.withClassLoader(classloader)
+ SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader)
+ SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT.withClassLoader(classloader)
+ SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader)
}
/** Starts a blocking event loop for message dispatch. */
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 f29fe98ed4..bb98afbe43 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
@@ -5,10 +5,7 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.contracts.Contract
import net.corda.core.contracts.UpgradedContract
import net.corda.core.cordapp.Cordapp
-import net.corda.core.flows.ContractUpgradeFlow
-import net.corda.core.flows.FlowLogic
-import net.corda.core.flows.InitiatedBy
-import net.corda.core.flows.StartableByRPC
+import net.corda.core.flows.*
import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.CordaPluginRegistry
@@ -40,10 +37,17 @@ import kotlin.streams.toList
* @property cordappJarPaths The classpath of cordapp JARs
*/
class CordappLoader private constructor(private val cordappJarPaths: List) {
- val cordapps: List by lazy { loadCordapps() }
+ val cordapps: List by lazy { loadCordapps() + coreCordapp }
- @VisibleForTesting
- internal val appClassLoader: ClassLoader = javaClass.classLoader
+ internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader)
+
+ init {
+ if (cordappJarPaths.isEmpty()) {
+ logger.info("No CorDapp paths provided")
+ } else {
+ logger.info("Loading CorDapps from ${cordappJarPaths.joinToString()}")
+ }
+ }
companion object {
private val logger = loggerFor()
@@ -54,19 +58,31 @@ class CordappLoader private constructor(private val cordappJarPaths: List)
* @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory
* for classpath scanning.
*/
- fun createDefault(baseDir: Path): CordappLoader {
- val pluginsDir = getPluginsPath(baseDir)
- return CordappLoader(if (!pluginsDir.exists()) emptyList() else pluginsDir.list {
- it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
- })
- }
-
- fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins"
+ fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)))
/**
- * Create a dev mode CordappLoader for test environments
+ * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath
+ * and plugins directory. This is intended mostly for use by the driver.
+ *
+ * @param baseDir See [createDefault.baseDir]
+ * @param testPackages See [createWithTestPackages.testPackages]
*/
- fun createWithTestPackages(testPackages: List = CordappLoader.testPackages) = CordappLoader(testPackages.flatMap(this::createScanPackage))
+ @VisibleForTesting
+ @JvmOverloads
+ fun createDefaultWithTestPackages(baseDir: Path, testPackages: List = CordappLoader.testPackages)
+ = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)) + testPackages.flatMap(this::createScanPackage))
+
+ /**
+ * Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath.
+ * This is intended for use in unit and integration tests.
+ *
+ * @param testPackages List of package names that contain CorDapp classes that can be automatically turned into
+ * CorDapps.
+ */
+ @VisibleForTesting
+ @JvmOverloads
+ fun createWithTestPackages(testPackages: List = CordappLoader.testPackages)
+ = CordappLoader(testPackages.flatMap(this::createScanPackage))
/**
* Creates a dev mode CordappLoader intended only to be used in test environments
@@ -76,6 +92,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List)
@VisibleForTesting
fun createDevMode(scanJars: List) = CordappLoader(scanJars)
+ private fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins"
+
private fun createScanPackage(scanPackage: String): List {
val resource = scanPackage.replace('.', '/')
return this::class.java.classLoader.getResources(resource)
@@ -90,7 +108,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List)
.toList()
}
- /** Takes a package of classes and creates a JAR from them - only use in tests */
+ /** 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")
@@ -118,12 +136,41 @@ class CordappLoader private constructor(private val cordappJarPaths: List)
return generatedCordapps[path]!!
}
+ private fun getCordappsInDirectory(pluginsDir: Path): List {
+ return if (!pluginsDir.exists()) {
+ emptyList()
+ } else {
+ pluginsDir.list {
+ it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
+ }
+ }
+ }
+
/**
- * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only
+ * A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only.
*/
@VisibleForTesting
var testPackages: List = emptyList()
private val generatedCordapps = mutableMapOf()
+
+ /** A list of the core RPC flows present in Corda */
+ private val coreRPCFlows = listOf(
+ ContractUpgradeFlow.Initiate::class.java,
+ ContractUpgradeFlow.Authorise::class.java,
+ ContractUpgradeFlow.Deauthorise::class.java)
+
+ /** A Cordapp representing the core package which is not scanned automatically. */
+ @VisibleForTesting
+ internal val coreCordapp = CordappImpl(
+ listOf(),
+ listOf(),
+ coreRPCFlows,
+ listOf(),
+ listOf(),
+ listOf(),
+ setOf(),
+ ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location
+ )
}
private fun loadCordapps(): List {
@@ -132,6 +179,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List)
CordappImpl(findContractClassNames(scanResult),
findInitiatedFlows(scanResult),
findRPCFlows(scanResult),
+ findSchedulableFlows(scanResult),
findServices(scanResult),
findPlugins(it),
findCustomSchemas(scanResult),
@@ -163,13 +211,11 @@ class CordappLoader private constructor(private val cordappJarPaths: List)
return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers))
}
- val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
- val coreFlows = listOf(
- ContractUpgradeFlow.Initiate::class.java,
- ContractUpgradeFlow.Authorise::class.java,
- ContractUpgradeFlow.Deauthorise::class.java
- )
- return found + coreFlows
+ return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
+ }
+
+ private fun findSchedulableFlows(scanResult: ScanResult): List>> {
+ return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class)
}
private fun findContractClassNames(scanResult: ScanResult): List {
diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt
index 3eb485787f..9c3f58293f 100644
--- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt
@@ -66,7 +66,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl
* @return A cordapp context for the given CorDapp
*/
fun getAppContext(cordapp: Cordapp): CordappContext {
- return CordappContext(cordapp, getCordappAttachmentId(cordapp), URLClassLoader(arrayOf(cordapp.jarPath), cordappLoader.appClassLoader))
+ return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader)
}
/**
diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt
index 70761cfc4e..ad2eb239fa 100644
--- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt
+++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt
@@ -459,4 +459,4 @@ object RpcServerObservableSerializer : Serializer>() {
observableContext.clientAddressToObservables.put(observableContext.clientAddress, observableId)
observableContext.observableMap.put(observableId, observableWithSubscription)
}
-}
\ No newline at end of file
+}
diff --git a/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt
deleted file mode 100644
index aadf3ee066..0000000000
--- a/node/src/test/kotlin/net/corda/node/cordapp/CordappLoaderTest.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package net.corda.node.cordapp
-
-import net.corda.core.flows.FlowLogic
-import net.corda.core.flows.InitiatedBy
-import net.corda.core.flows.InitiatingFlow
-import net.corda.node.internal.cordapp.CordappLoader
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Test
-import java.nio.file.Paths
-
-@InitiatingFlow
-class DummyFlow : FlowLogic() {
- override fun call() { }
-}
-
-@InitiatedBy(DummyFlow::class)
-class LoaderTestFlow : FlowLogic() {
- override fun call() { }
-}
-
-class CordappLoaderTest {
- @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).isEmpty()
- }
-
- @Test
- fun `test that classes that are in a cordapp are loaded`() {
- val loader = CordappLoader.createWithTestPackages(listOf("net.corda.node.cordapp"))
- val initiatedFlows = loader.cordapps.first().initiatedFlows
- val expectedClass = loader.appClassLoader.loadClass("net.corda.node.cordapp.LoaderTestFlow").asSubclass(FlowLogic::class.java)
- assertThat(initiatedFlows).contains(expectedClass)
- }
-
- @Test
- fun `isolated JAR contains a CorDapp with a contract and plugin`() {
- val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
- val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
-
- val actual = loader.cordapps.toTypedArray()
- assertThat(actual).hasSize(1)
-
- val actualCordapp = actual.first()
- assertThat(actualCordapp.contractClassNames).isEqualTo(listOf("net.corda.finance.contracts.isolated.AnotherDummyContract"))
- assertThat(actualCordapp.initiatedFlows).isEmpty()
- assertThat(actualCordapp.rpcFlows).contains(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiate").asSubclass(FlowLogic::class.java))
- assertThat(actualCordapp.services).isEmpty()
- assertThat(actualCordapp.plugins).hasSize(1)
- assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin")
- assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR)
- }
-}
diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt
new file mode 100644
index 0000000000..806b7ac929
--- /dev/null
+++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappLoaderTest.kt
@@ -0,0 +1,89 @@
+package net.corda.node.internal.cordapp
+
+import net.corda.core.flows.*
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import java.nio.file.Paths
+
+@InitiatingFlow
+class DummyFlow : FlowLogic() {
+ override fun call() { }
+}
+
+@InitiatedBy(DummyFlow::class)
+class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic() {
+ override fun call() { }
+}
+
+@SchedulableFlow
+class DummySchedulableFlow : FlowLogic() {
+ override fun call() { }
+}
+
+@StartableByRPC
+class DummyRPCFlow : FlowLogic() {
+ override fun call() { }
+}
+
+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"
+ }
+
+ @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)
+ }
+
+ @Test
+ fun `isolated JAR contains a CorDapp with a contract and plugin`() {
+ val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
+ val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
+
+ val actual = loader.cordapps.toTypedArray()
+ assertThat(actual).hasSize(2)
+
+ val actualCordapp = actual.single { it != CordappLoader.coreCordapp }
+ assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
+ assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
+ assertThat(actualCordapp.rpcFlows).isEmpty()
+ assertThat(actualCordapp.schedulableFlows).isEmpty()
+ assertThat(actualCordapp.services).isEmpty()
+ assertThat(actualCordapp.plugins).hasSize(1)
+ assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin")
+ assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR)
+ }
+
+ @Test
+ fun `flows are loaded by loader`() {
+ val loader = CordappLoader.createWithTestPackages(testScanPackages)
+
+ val actual = loader.cordapps.toTypedArray()
+ // One core cordapp, one cordapp from this source tree, and two others due to identically named locations
+ // in resources and the non-test part of node. This is okay due to this being test code. In production this
+ // cannot happen. In gradle it will also pick up the node jar.
+ assertThat(actual.size == 4 || actual.size == 5).isTrue()
+
+ val actualCordapp = actual.single { !it.initiatedFlows.isEmpty() }
+ assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java)
+ assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java)
+ assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java)
+ }
+
+ // 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
+ fun `cordapp classloader can load cordapp classes`() {
+ val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
+ val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
+
+ loader.appClassLoader.loadClass(isolatedContractId)
+ loader.appClassLoader.loadClass(isolatedFlowName)
+ }
+}
diff --git a/node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt
similarity index 94%
rename from node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt
rename to node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt
index 762761d99e..10dcd4a3ad 100644
--- a/node/src/test/kotlin/net/corda/node/cordapp/CordappProviderImplTests.kt
+++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt
@@ -1,8 +1,6 @@
-package net.corda.node.cordapp
+package net.corda.node.internal.cordapp
import net.corda.core.node.services.AttachmentStorage
-import net.corda.node.internal.cordapp.CordappLoader
-import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.node.MockAttachmentStorage
import org.junit.Assert
import org.junit.Before
diff --git a/node/src/test/resources/net/corda/node/cordapp/empty.jar b/node/src/test/resources/net/corda/node/internal/cordapp/empty.jar
similarity index 100%
rename from node/src/test/resources/net/corda/node/cordapp/empty.jar
rename to node/src/test/resources/net/corda/node/internal/cordapp/empty.jar
diff --git a/node/src/test/resources/net/corda/node/cordapp/isolated.jar b/node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar
similarity index 100%
rename from node/src/test/resources/net/corda/node/cordapp/isolated.jar
rename to node/src/test/resources/net/corda/node/internal/cordapp/isolated.jar
diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt
index a0ca53fe0e..f51d59d20d 100644
--- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt
+++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappProvider.kt
@@ -14,7 +14,7 @@ class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(co
val cordappRegistry = mutableListOf>()
fun addMockCordapp(contractClassName: ContractClassName, services: ServiceHub) {
- val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL())
+ val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL())
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), services)))
}