mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +00:00
Add Cordapp class to define CorDapps inside Corda and encapsulate Cordapp loading (#1494)
Introduced a Cordapp class which contains all relevant information about a Cordapp. The Cordapp loader now produces Cordapps instead of lists of classes and services without any relation to the original Cordapp. Added new static constructor to CordappLoader to be able to take arbitrary paths to load cordapps from in dev mode. Moved the CordappLoader into the cordapp package.
This commit is contained in:
parent
5504493c8d
commit
1ef4cec0cd
7
.idea/compiler.xml
generated
7
.idea/compiler.xml
generated
@ -19,6 +19,9 @@
|
||||
<module name="corda-webserver_test" target="1.8" />
|
||||
<module name="cordform-common_main" target="1.8" />
|
||||
<module name="cordform-common_test" target="1.8" />
|
||||
<module name="cordformation_main" target="1.8" />
|
||||
<module name="cordformation_runnodes" target="1.8" />
|
||||
<module name="cordformation_test" target="1.8" />
|
||||
<module name="core_main" target="1.8" />
|
||||
<module name="core_test" target="1.8" />
|
||||
<module name="demobench_main" target="1.8" />
|
||||
@ -67,8 +70,12 @@
|
||||
<module name="node_test" target="1.8" />
|
||||
<module name="notary-demo_main" target="1.8" />
|
||||
<module name="notary-demo_test" target="1.8" />
|
||||
<module name="publish-utils_main" target="1.8" />
|
||||
<module name="publish-utils_test" target="1.8" />
|
||||
<module name="quasar-hook_main" target="1.8" />
|
||||
<module name="quasar-hook_test" target="1.8" />
|
||||
<module name="quasar-utils_main" target="1.8" />
|
||||
<module name="quasar-utils_test" target="1.8" />
|
||||
<module name="rpc_integrationTest" target="1.8" />
|
||||
<module name="rpc_main" target="1.8" />
|
||||
<module name="rpc_smokeTest" target="1.8" />
|
||||
|
@ -31,7 +31,7 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.cert
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.classloading.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.services.NotaryChangeHandler
|
||||
import net.corda.node.services.NotifyTransactionHandler
|
||||
@ -68,6 +68,7 @@ import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.services.vault.VaultSoftLockManager
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.node.utilities.AddOrRemove.ADD
|
||||
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
@ -148,20 +149,20 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
CordaX500Name.build(cert.subject).copy(commonName = null)
|
||||
}
|
||||
|
||||
/** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */
|
||||
open val pluginRegistries: List<CordaPluginRegistry> by lazy {
|
||||
ServiceLoader.load(CordaPluginRegistry::class.java).toList()
|
||||
}
|
||||
|
||||
val cordappLoader: CordappLoader by lazy {
|
||||
if (System.getProperty("net.corda.node.cordapp.scan.package") != null) {
|
||||
val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package")
|
||||
if (scanPackage != null) {
|
||||
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
|
||||
CordappLoader.createDevMode(System.getProperty("net.corda.node.cordapp.scan.package"))
|
||||
CordappLoader.createDevMode(scanPackage)
|
||||
} else {
|
||||
CordappLoader.createDefault(configuration.baseDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
open val pluginRegistries: List<CordaPluginRegistry> by lazy {
|
||||
cordappLoader.cordapps.flatMap { it.plugins } + DefaultWhitelist()
|
||||
}
|
||||
|
||||
/** Set to true once [start] has been successfully called. */
|
||||
@Volatile
|
||||
var started = false
|
||||
@ -213,8 +214,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
|
||||
installCordaServices()
|
||||
registerCordappFlows()
|
||||
_services.rpcFlows += cordappLoader.findRPCFlows()
|
||||
registerCustomSchemas(cordappLoader.findCustomSchemas())
|
||||
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
|
||||
registerCustomSchemas(cordappLoader.cordapps.flatMap { it.customSchemas }.toSet())
|
||||
|
||||
runOnStop += network::stop
|
||||
}
|
||||
@ -232,7 +233,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
private class ServiceInstantiationException(cause: Throwable?) : Exception(cause)
|
||||
|
||||
private fun installCordaServices() {
|
||||
cordappLoader.findServices(info).forEach {
|
||||
cordappLoader.cordapps.flatMap { it.filterEnabledServices(info) }.map {
|
||||
try {
|
||||
installCordaService(it)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
@ -274,7 +275,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun registerCordappFlows() {
|
||||
cordappLoader.findInitiatedFlows()
|
||||
cordappLoader.cordapps.flatMap { it.initiatedFlows }
|
||||
.forEach {
|
||||
try {
|
||||
registerInitiatedFlowInternal(it, track = false)
|
||||
|
@ -0,0 +1,58 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Defines a CorDapp
|
||||
*
|
||||
* @property contractClassNames List of contracts
|
||||
* @property initiatedFlows List of initiatable flow classes
|
||||
* @property rpcFlows List of RPC initiable flows classes
|
||||
* @property servies List of RPC services
|
||||
* @property plugins List of Corda plugin registries
|
||||
* @property jarPath The path to the JAR for this CorDapp
|
||||
*/
|
||||
data class Cordapp(
|
||||
val contractClassNames: List<String>,
|
||||
val initiatedFlows: List<Class<out FlowLogic<*>>>,
|
||||
val rpcFlows: List<Class<out FlowLogic<*>>>,
|
||||
val services: List<Class<out SerializeAsToken>>,
|
||||
val plugins: List<CordaPluginRegistry>,
|
||||
val customSchemas: Set<MappedSchema>,
|
||||
val jarPath: URL) {
|
||||
companion object {
|
||||
private val logger = loggerFor<Cordapp>()
|
||||
}
|
||||
|
||||
fun filterEnabledServices(info: NodeInfo): List<Class<out SerializeAsToken>> {
|
||||
return services.filter {
|
||||
val serviceType = getServiceType(it)
|
||||
if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) {
|
||||
logger.debug {
|
||||
"Ignoring ${it.name} as a Corda service since $serviceType is not one of our " +
|
||||
"advertised services"
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getServiceType(clazz: Class<*>): ServiceType? {
|
||||
return try {
|
||||
clazz.getField("type").get(null) as ServiceType
|
||||
} catch (e: NoSuchFieldException) {
|
||||
logger.warn("${clazz.name} does not have a type field, optimistically proceeding with install.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,60 +1,70 @@
|
||||
package net.corda.node.internal.classloading
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
|
||||
import net.corda.core.contracts.Contract
|
||||
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.internal.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
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.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.stream.Collectors
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.streams.toList
|
||||
|
||||
/**
|
||||
* Handles CorDapp loading and classpath scanning
|
||||
* Handles CorDapp loading and classpath scanning of CorDapp JARs
|
||||
*
|
||||
* @property cordappJarPaths The classpath of cordapp JARs
|
||||
*/
|
||||
class CordappLoader private constructor (val cordappClassPath: List<Path>) {
|
||||
val appClassLoader: ClassLoader = javaClass.classLoader
|
||||
val scanResult = scanCordapps()
|
||||
class CordappLoader private constructor(private val cordappJarPaths: List<URL>) {
|
||||
val cordapps: List<Cordapp> by lazy { loadCordapps() }
|
||||
|
||||
@VisibleForTesting
|
||||
internal val appClassLoader: ClassLoader = javaClass.classLoader
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<CordappLoader>()
|
||||
|
||||
/**
|
||||
* Creates the default CordappLoader intended to be used in non-dev or non-test environments.
|
||||
* Creates a default CordappLoader intended to be used in non-dev or non-test environments.
|
||||
*
|
||||
* @param basedir The directory that this node is running in. Will use this to resolve the plugins directory
|
||||
* @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 = baseDir / "plugins"
|
||||
return CordappLoader(if (!pluginsDir.exists()) emptyList<Path>() else pluginsDir.list {
|
||||
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.collect(Collectors.toList())
|
||||
return CordappLoader(if (!pluginsDir.exists()) emptyList<URL>() else pluginsDir.list {
|
||||
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the dev mode CordappLoader intended to only be used in dev or test environments.
|
||||
* Creates a dev mode CordappLoader intended to only be used in test environments.
|
||||
*
|
||||
* @param scanPackage Resolves the JARs that contain scanPackage and use them as the source for
|
||||
* the classpath scanning.
|
||||
*/
|
||||
fun createDevMode(scanPackage: String): CordappLoader {
|
||||
val resource = scanPackage.replace('.', '/')
|
||||
val paths = javaClass.classLoader.getResources(resource)
|
||||
val paths = this::class.java.classLoader.getResources(resource)
|
||||
.asSequence()
|
||||
.map {
|
||||
val uri = if (it.protocol == "jar") {
|
||||
@ -62,43 +72,43 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
|
||||
} else {
|
||||
URI(it.toExternalForm().removeSuffix(resource))
|
||||
}
|
||||
Paths.get(uri)
|
||||
uri.toURL()
|
||||
}
|
||||
.toList()
|
||||
return CordappLoader(paths)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dev mode CordappLoader intended only to be used in test environments
|
||||
*
|
||||
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun createDevMode(scanJars: List<URL>) = CordappLoader(scanJars)
|
||||
}
|
||||
|
||||
fun findServices(info: NodeInfo): List<Class<out SerializeAsToken>> {
|
||||
fun getServiceType(clazz: Class<*>): ServiceType? {
|
||||
return try {
|
||||
clazz.getField("type").get(null) as ServiceType
|
||||
} catch (e: NoSuchFieldException) {
|
||||
logger.warn("${clazz.name} does not have a type field, optimistically proceeding with install.")
|
||||
null
|
||||
private fun loadCordapps(): List<Cordapp> {
|
||||
return cordappJarPaths.map {
|
||||
val scanResult = scanCordapp(it)
|
||||
Cordapp(findContractClassNames(scanResult),
|
||||
findInitiatedFlows(scanResult),
|
||||
findRPCFlows(scanResult),
|
||||
findServices(scanResult),
|
||||
findPlugins(it),
|
||||
findCustomSchemas(scanResult),
|
||||
it)
|
||||
}
|
||||
}
|
||||
|
||||
return scanResult?.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
|
||||
?.filter {
|
||||
val serviceType = getServiceType(it)
|
||||
if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) {
|
||||
logger.debug {
|
||||
"Ignoring ${it.name} as a Corda service since $serviceType is not one of our " +
|
||||
"advertised services"
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} ?: emptyList<Class<SerializeAsToken>>()
|
||||
private fun findServices(scanResult: ScanResult): List<Class<out SerializeAsToken>> {
|
||||
return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
|
||||
}
|
||||
|
||||
fun findInitiatedFlows(): List<Class<out FlowLogic<*>>> {
|
||||
return scanResult?.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
|
||||
private fun findInitiatedFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
|
||||
return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
|
||||
// First group by the initiating flow class in case there are multiple mappings
|
||||
?.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
|
||||
?.map { (initiatingFlow, initiatedFlows) ->
|
||||
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
|
||||
.map { (initiatingFlow, initiatedFlows) ->
|
||||
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
|
||||
if (sorted.size > 1) {
|
||||
logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
|
||||
@ -106,41 +116,43 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
|
||||
"specific sub-type for registration: ${sorted[0].name}.")
|
||||
}
|
||||
sorted[0]
|
||||
} ?: emptyList<Class<out FlowLogic<*>>>()
|
||||
}
|
||||
}
|
||||
|
||||
fun findRPCFlows(): List<Class<out FlowLogic<*>>> {
|
||||
private fun findRPCFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
|
||||
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
|
||||
return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers))
|
||||
}
|
||||
|
||||
val found = scanResult?.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class)?.filter { it.isUserInvokable() } ?: emptyList<Class<out FlowLogic<*>>>()
|
||||
val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
|
||||
val coreFlows = listOf(ContractUpgradeFlow.Initiator::class.java)
|
||||
return found + coreFlows
|
||||
}
|
||||
|
||||
fun findCustomSchemas(): Set<MappedSchema> {
|
||||
return scanResult?.getClassesWithSuperclass(MappedSchema::class)?.toSet() ?: emptySet()
|
||||
private fun findContractClassNames(scanResult: ScanResult): List<String> {
|
||||
return scanResult.getNamesOfClassesImplementing(Contract::class.java)
|
||||
}
|
||||
|
||||
private fun scanCordapps(): ScanResult? {
|
||||
logger.info("Scanning CorDapps in $cordappClassPath")
|
||||
return if (cordappClassPath.isNotEmpty())
|
||||
FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappClassPath).scan()
|
||||
else
|
||||
null
|
||||
private fun findPlugins(cordappJarPath: URL): List<CordaPluginRegistry> {
|
||||
return ServiceLoader.load(CordaPluginRegistry::class.java, URLClassLoader(arrayOf(cordappJarPath), null)).toList()
|
||||
}
|
||||
|
||||
private fun findCustomSchemas(scanResult: ScanResult): Set<MappedSchema> {
|
||||
return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet()
|
||||
}
|
||||
|
||||
private fun scanCordapp(cordappJarPath: URL): ScanResult {
|
||||
logger.info("Scanning CorDapp in $cordappJarPath")
|
||||
return FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath).scan()
|
||||
}
|
||||
|
||||
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
|
||||
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
|
||||
return if (o1 == o2) {
|
||||
0
|
||||
} else if (o1.isAssignableFrom(o2)) {
|
||||
1
|
||||
} else if (o2.isAssignableFrom(o1)) {
|
||||
-1
|
||||
} else {
|
||||
throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
|
||||
return when {
|
||||
o1 == o2 -> 0
|
||||
o1.isAssignableFrom(o2) -> 1
|
||||
o2.isAssignableFrom(o1) -> -1
|
||||
else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
|
||||
"both ${o1.name} and ${o2.name}")
|
||||
}
|
||||
}
|
||||
@ -148,7 +160,7 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
|
||||
|
||||
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
|
||||
return try {
|
||||
appClassLoader.loadClass(className) as Class<T>
|
||||
appClassLoader.loadClass(className).asSubclass(type.java)
|
||||
} catch (e: ClassCastException) {
|
||||
logger.warn("As $className must be a sub-type of ${type.java.name}")
|
||||
null
|
@ -1,38 +0,0 @@
|
||||
package net.corda.node.classloading
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.node.internal.classloading.CordappLoader
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class DummyFlow : FlowLogic<Unit>() {
|
||||
override fun call() { }
|
||||
}
|
||||
|
||||
@InitiatedBy(DummyFlow::class)
|
||||
class LoaderTestFlow : FlowLogic<Unit>() {
|
||||
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("."))
|
||||
Assert.assertNull(loader.findInitiatedFlows().find { it == LoaderTestFlow::class })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that classes that are in a cordapp are loaded`() {
|
||||
val loader = CordappLoader.createDevMode("net.corda.node.classloading")
|
||||
val initiatedFlows = loader.findInitiatedFlows()
|
||||
val expectedClass = loader.appClassLoader.loadClass("net.corda.node.classloading.LoaderTestFlow")
|
||||
Assert.assertNotNull(initiatedFlows.find { it == expectedClass })
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package net.corda.node.cordapp
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.node.internal.cordapp.Cordapp
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
||||
class DummyFlow : FlowLogic<Unit>() {
|
||||
override fun call() { }
|
||||
}
|
||||
|
||||
@InitiatedBy(DummyFlow::class)
|
||||
class LoaderTestFlow : FlowLogic<Unit>() {
|
||||
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.createDevMode("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`() {
|
||||
val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
|
||||
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
|
||||
val expectedCordapp = Cordapp(
|
||||
listOf("net.corda.finance.contracts.isolated.AnotherDummyContract"),
|
||||
emptyList(),
|
||||
listOf(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiator").asSubclass(FlowLogic::class.java)),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
isolatedJAR)
|
||||
val expected = arrayOf(expectedCordapp)
|
||||
Assert.assertArrayEquals(expected, loader.cordapps.toTypedArray())
|
||||
}
|
||||
}
|
BIN
node/src/test/resources/net/corda/node/cordapp/isolated.jar
Normal file
BIN
node/src/test/resources/net/corda/node/cordapp/isolated.jar
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user