diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 9b0c4dcb3c..52e9092d84 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -427,14 +427,14 @@ public static final class net.corda.core.contracts.AmountTransfer$Companion exte ## @CordaSerializable public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash - public abstract void extractFile(String, java.io.OutputStream) + public void extractFile(String, java.io.OutputStream) @NotNull public abstract java.util.List getSigners() public abstract int getSize() @NotNull public abstract java.io.InputStream open() @NotNull - public abstract java.util.jar.JarInputStream openAsJAR() + public java.util.jar.JarInputStream openAsJAR() ## @DoNotImplement @CordaSerializable @@ -526,7 +526,6 @@ public final class net.corda.core.contracts.ContractAttachment extends java.lang public (net.corda.core.contracts.Attachment, String) public (net.corda.core.contracts.Attachment, String, java.util.Set) public (net.corda.core.contracts.Attachment, String, java.util.Set, String) - public void extractFile(String, java.io.OutputStream) @NotNull public final java.util.Set getAdditionalContracts() @NotNull @@ -545,8 +544,6 @@ public final class net.corda.core.contracts.ContractAttachment extends java.lang @NotNull public java.io.InputStream open() @NotNull - public java.util.jar.JarInputStream openAsJAR() - @NotNull public String toString() ## @CordaSerializable @@ -4613,7 +4610,6 @@ public static final class net.corda.core.transactions.ContractUpgradeFilteredTra @DoNotImplement public final class net.corda.core.transactions.ContractUpgradeLedgerTransaction extends net.corda.core.transactions.FullTransaction implements net.corda.core.transactions.TransactionWithSignatures public (java.util.List>, net.corda.core.identity.Party, net.corda.core.contracts.Attachment, String, net.corda.core.contracts.Attachment, net.corda.core.crypto.SecureHash, net.corda.core.contracts.PrivacySalt, java.util.List, net.corda.core.node.NetworkParameters) - public void checkSignaturesAreValid() @NotNull public final java.util.List> component1() @NotNull @@ -4642,8 +4638,6 @@ public final class net.corda.core.transactions.ContractUpgradeLedgerTransaction @NotNull public final net.corda.core.contracts.Attachment getLegacyContractAttachment() @NotNull - public java.util.Set getMissingSigners() - @NotNull public net.corda.core.identity.Party getNotary() @NotNull public java.util.List> getOutputs() @@ -4659,9 +4653,6 @@ public final class net.corda.core.transactions.ContractUpgradeLedgerTransaction public final String getUpgradedContractClassName() public int hashCode() public String toString() - public void verifyRequiredSignatures() - public void verifySignaturesExcept(java.util.Collection) - public void verifySignaturesExcept(java.security.PublicKey...) ## @DoNotImplement @CordaSerializable @@ -4879,7 +4870,6 @@ public final class net.corda.core.transactions.MissingContractAttachments extend @DoNotImplement public final class net.corda.core.transactions.NotaryChangeLedgerTransaction extends net.corda.core.transactions.FullTransaction implements net.corda.core.transactions.TransactionWithSignatures public (java.util.List>, net.corda.core.identity.Party, net.corda.core.identity.Party, net.corda.core.crypto.SecureHash, java.util.List) - public void checkSignaturesAreValid() @NotNull public final java.util.List> component1() @NotNull @@ -4900,8 +4890,6 @@ public final class net.corda.core.transactions.NotaryChangeLedgerTransaction ext @NotNull public java.util.List getKeyDescriptions(java.util.Set) @NotNull - public java.util.Set getMissingSigners() - @NotNull public final net.corda.core.identity.Party getNewNotary() @NotNull public net.corda.core.identity.Party getNotary() @@ -4913,9 +4901,6 @@ public final class net.corda.core.transactions.NotaryChangeLedgerTransaction ext public java.util.List getSigs() public int hashCode() public String toString() - public void verifyRequiredSignatures() - public void verifySignaturesExcept(java.util.Collection) - public void verifySignaturesExcept(java.security.PublicKey...) ## @DoNotImplement @CordaSerializable @@ -4958,7 +4943,6 @@ public final class net.corda.core.transactions.SignedTransaction extends java.la public (net.corda.core.transactions.CoreTransaction, java.util.List) @NotNull public final net.corda.core.transactions.FilteredTransaction buildFilteredTransaction(java.util.function.Predicate) - public void checkSignaturesAreValid() @NotNull public final net.corda.core.serialization.SerializedBytes component1() @NotNull @@ -4974,8 +4958,6 @@ public final class net.corda.core.transactions.SignedTransaction extends java.la public final java.util.List getInputs() @NotNull public java.util.ArrayList getKeyDescriptions(java.util.Set) - @NotNull - public java.util.Set getMissingSigners() @Nullable public final net.corda.core.identity.Party getNotary() @NotNull @@ -5012,9 +4994,6 @@ public final class net.corda.core.transactions.SignedTransaction extends java.la public String toString() public final void verify(net.corda.core.node.ServiceHub) public final void verify(net.corda.core.node.ServiceHub, boolean) - public void verifyRequiredSignatures() - public void verifySignaturesExcept(java.util.Collection) - public void verifySignaturesExcept(java.security.PublicKey...) @NotNull public final net.corda.core.transactions.SignedTransaction withAdditionalSignature(java.security.KeyPair, net.corda.core.crypto.SignatureMetadata) @NotNull @@ -5115,18 +5094,18 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob ## @DoNotImplement public interface net.corda.core.transactions.TransactionWithSignatures extends net.corda.core.contracts.NamedByHash - public abstract void checkSignaturesAreValid() + public void checkSignaturesAreValid() @NotNull public abstract java.util.List getKeyDescriptions(java.util.Set) @NotNull - public abstract java.util.Set getMissingSigners() + public java.util.Set getMissingSigners() @NotNull public abstract java.util.Set getRequiredSigningKeys() @NotNull public abstract java.util.List getSigs() - public abstract void verifyRequiredSignatures() - public abstract void verifySignaturesExcept(java.util.Collection) - public abstract void verifySignaturesExcept(java.security.PublicKey...) + public void verifyRequiredSignatures() + public void verifySignaturesExcept(java.util.Collection) + public void verifySignaturesExcept(java.security.PublicKey...) ## @DoNotImplement @CordaSerializable diff --git a/build.gradle b/build.gradle index d27318f913..22d775bb57 100644 --- a/build.gradle +++ b/build.gradle @@ -100,6 +100,8 @@ buildscript { ext.jcabi_manifests_version = '1.1' ext.deterministic_rt_version = '1.0-SNAPSHOT' + // Name of the IntelliJ SDK created for the deterministic Java rt.jar. + // ext.deterministic_idea_sdk = '1.8 (Deterministic)' // Update 121 is required for ObjectInputFilter. // Updates [131, 161] also have zip compression bugs on MacOS (High Sierra). @@ -195,7 +197,7 @@ allprojects { apiVersion = "1.2" jvmTarget = "1.8" javaParameters = true // Useful for reflection. - freeCompilerArgs = ['-Xenable-jvm-default'] + freeCompilerArgs = ['-Xjvm-default=compatibility'] } } diff --git a/constants.properties b/constants.properties index caa3effd56..ced3c2015d 100644 --- a/constants.properties +++ b/constants.properties @@ -9,7 +9,7 @@ # gradlePluginsVersion=4.0.23 -kotlinVersion=1.2.41 +kotlinVersion=1.2.50 platformVersion=4 guavaVersion=21.0 proguardVersion=6.0.3 diff --git a/core-deterministic/build.gradle b/core-deterministic/build.gradle index 42d3a9f32a..8f84a5d5ad 100644 --- a/core-deterministic/build.gradle +++ b/core-deterministic/build.gradle @@ -9,17 +9,15 @@ */ description 'Corda core (deterministic)' -apply plugin: 'kotlin' +apply from: '../deterministic.gradle' apply plugin: 'com.jfrog.artifactory' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'idea' -evaluationDependsOn(':jdk8u-deterministic') evaluationDependsOn(":core") def javaHome = System.getProperty('java.home') def jarBaseName = "corda-${project.name}".toString() -def jdkTask = project(':jdk8u-deterministic').assemble -def deterministic_jdk_home = project(':jdk8u-deterministic').jdk_home configurations { runtimeLibraries @@ -44,18 +42,6 @@ dependencies { runtimeLibraries "org.slf4j:slf4j-api:$slf4j_version" } -tasks.withType(AbstractCompile) { - dependsOn jdkTask -} - -tasks.withType(JavaCompile) { - options.compilerArgs << '-bootclasspath' << "$deterministic_jdk_home/jre/lib/rt.jar".toString() -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { - kotlinOptions.jdkHome = deterministic_jdk_home -} - jar { baseName 'DOES-NOT-EXIST' // Don't build a jar here because it would be the wrong one. @@ -173,7 +159,7 @@ task metafix(type: MetaFixerTask) { task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { injars metafix - libraryjars "$deterministic_jdk_home/jre/lib/rt.jar" + libraryjars deterministic_rt_jar configurations.runtimeLibraries.forEach { libraryjars it.path, filter: '!META-INF/versions/**' @@ -209,3 +195,11 @@ publish { // Must be after publish {} so that the previous install task exists for overwriting. task install(overwrite: true, dependsOn: 'publishToMavenLocal') + +idea { + module { + if (project.hasProperty("deterministic_idea_sdk")) { + jdkName project.property("deterministic_idea_sdk") as String + } + } +} diff --git a/core-deterministic/testing/common/build.gradle b/core-deterministic/testing/common/build.gradle index 0d8ee674c5..28d35e1b0c 100644 --- a/core-deterministic/testing/common/build.gradle +++ b/core-deterministic/testing/common/build.gradle @@ -7,12 +7,8 @@ * * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -apply plugin: 'kotlin' - -evaluationDependsOn(':jdk8u-deterministic') - -def jdkTask = project(':jdk8u-deterministic').assemble -def deterministic_jdk_home = project(':jdk8u-deterministic').jdk_home +apply from: '../../../deterministic.gradle' +apply plugin: 'idea' dependencies { compileOnly project(path: ':core-deterministic', configuration: 'runtimeArtifacts') @@ -20,14 +16,10 @@ dependencies { compileOnly "junit:junit:$junit_version" } -tasks.withType(AbstractCompile) { - dependsOn jdkTask -} - -tasks.withType(JavaCompile) { - options.compilerArgs << '-bootclasspath' << "$deterministic_jdk_home/jre/lib/rt.jar".toString() -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { - kotlinOptions.jdkHome = deterministic_jdk_home +idea { + module { + if (project.hasProperty("deterministic_idea_sdk")) { + jdkName project.property("deterministic_idea_sdk") as String + } + } } diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index fee5bc6723..403a0960c9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -42,6 +42,8 @@ import java.util.jar.JarInputStream @CordaSerializable interface Attachment : NamedByHash { fun open(): InputStream + + @JvmDefault fun openAsJAR(): JarInputStream { val stream = open() try { @@ -55,6 +57,7 @@ interface Attachment : NamedByHash { * Finds the named file case insensitively and copies it to the output stream. * @throws FileNotFoundException if the given path doesn't exist in the attachment. */ + @JvmDefault fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) } /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt index 621df96f91..493cf09307 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionWithSignatures.kt @@ -43,6 +43,7 @@ interface TransactionWithSignatures : NamedByHash { * @throws SignatureException if any signatures are invalid or unrecognised. * @throws SignaturesMissingException if any signatures should have been present but were not. */ + @JvmDefault @Throws(SignatureException::class) fun verifyRequiredSignatures() = verifySignaturesExcept(emptySet()) @@ -58,6 +59,7 @@ interface TransactionWithSignatures : NamedByHash { * @throws SignatureException if any signatures are invalid or unrecognised. * @throws SignaturesMissingException if any signatures should have been present but were not. */ + @JvmDefault @Throws(SignatureException::class) fun verifySignaturesExcept(vararg allowedToBeMissing: PublicKey) { verifySignaturesExcept(Arrays.asList(*allowedToBeMissing)) @@ -75,6 +77,7 @@ interface TransactionWithSignatures : NamedByHash { * @throws SignatureException if any signatures are invalid or unrecognised. * @throws SignaturesMissingException if any signatures should have been present but were not. */ + @JvmDefault @Throws(SignatureException::class) fun verifySignaturesExcept(allowedToBeMissing: Collection) { val needed = getMissingSigners() - allowedToBeMissing @@ -92,6 +95,7 @@ interface TransactionWithSignatures : NamedByHash { * @throws InvalidKeyException if the key on a signature is invalid. * @throws SignatureException if a signature fails to verify. */ + @JvmDefault @Throws(InvalidKeyException::class, SignatureException::class) fun checkSignaturesAreValid() { for (sig in sigs) { @@ -110,6 +114,7 @@ interface TransactionWithSignatures : NamedByHash { /** * Return the [PublicKey]s for which we still need signatures. */ + @JvmDefault fun getMissingSigners(): Set { val sigKeys = sigs.map { it.by }.toSet() // TODO Problem is that we can get single PublicKey wrapped as CompositeKey in allowedToBeMissing/mustSign diff --git a/deterministic.gradle b/deterministic.gradle new file mode 100644 index 0000000000..fefca67c76 --- /dev/null +++ b/deterministic.gradle @@ -0,0 +1,29 @@ +/* + * Gradle script plugin: Configure a module such that the Java and Kotlin + * compilers use the deterministic rt.jar instead of the full JDK rt.jar. + */ +apply plugin: 'kotlin' + +evaluationDependsOn(':jdk8u-deterministic') + +def jdk8uDeterministic = project(':jdk8u-deterministic') + +ext { + jdkTask = jdk8uDeterministic.assemble + deterministic_jdk_home = jdk8uDeterministic.jdk_home + deterministic_rt_jar = jdk8uDeterministic.rt_jar +} + +tasks.withType(AbstractCompile) { + dependsOn jdkTask + + // This is a bit ugly, but Gradle isn't recognising the KotlinCompile task + // as it does the built-in JavaCompile task. + if (it.class.name.startsWith("org.jetbrains.kotlin.gradle.tasks.KotlinCompile")) { + kotlinOptions.jdkHome = deterministic_jdk_home + } +} + +tasks.withType(JavaCompile) { + options.compilerArgs << '-bootclasspath' << deterministic_rt_jar +} diff --git a/docs/source/cordapp-custom-serializers.rst b/docs/source/cordapp-custom-serializers.rst index 0e2554ebc7..30be31039a 100644 --- a/docs/source/cordapp-custom-serializers.rst +++ b/docs/source/cordapp-custom-serializers.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + Pluggable Serializers for CorDapps ================================== @@ -21,16 +27,16 @@ Serializers must * Inherit from ``net.corda.core.serialization.SerializationCustomSerializer`` * Provide a proxy class to transform the object to and from * Implement the ``toProxy`` and ``fromProxy`` methods - * Be either included into CorDapp Jar or made known to the running process via ``amqp.custom.serialization.scanSpec`` - system property. This system property may be necessary to be able to discover custom serializer in the classpath. At a minimum the value - of the property should include comma separated set of packages where custom serializers located. Full syntax includes - scanning specification as defined by: `` + * Be either included into the CorDapp Jar or made known to the running process via the ``amqp.custom.serialization.scanSpec`` + system property. This system property may be necessary to be able to discover custom serializer in the classpath. + At a minimum the value of the property should include comma separated set of packages where custom serializers located. + Full syntax includes scanning specification as defined by: `` Serializers inheriting from ``SerializationCustomSerializer`` have to implement two methods and two types. Example ------- -Consider this example class: +Consider the following class: .. sourcecode:: java @@ -38,6 +44,9 @@ Consider this example class: private final Int a private final Int b + // Because this is marked private the serialization framework will not + // consider it when looking to see which constructor should be used + // when serializing instances of this class. private Example(Int a, Int b) { this.a = a; this.b = b; @@ -52,23 +61,166 @@ Consider this example class: Without a custom serializer we cannot serialize this class as there is no public constructor that facilitates the initialisation of all of its properties. -To be serializable by Corda this would require a custom serializer as follows: +.. note:: This is clearly a contrived example, simply making the constructor public would alleviate the issues. + However, for the purposes of this example we are assuming that for external reasons this cannot be done. + +To be serializable by Corda this would require a custom serializer to be written that can transform the unserializable +class into a form we can serialize. Continuing the above example, this could be written as follows: + +.. container:: codeset + + .. sourcecode:: java + + /** + * The class lacks a public constructor that takes parameters it can associate + * with its properties and is thus not serializable by the CORDA serialization + * framework. + */ + class Example { + private int a; + private int b; + + public int getA() { return a; } + public int getB() { return b; } + + public Example(List l) { + this.a = l.get(0); + this.b = l.get(1); + } + } + + /** + * This is the class that will Proxy instances of Example within the serializer + */ + public class ExampleProxy { + /** + * These properties will be serialized into the byte stream, this is where we choose how to + * represent instances of the object we're proxying. In this example, which is somewhat + * contrived, this choice is obvious. In your own classes / 3rd party libraries, however, this + * may require more thought. + */ + private int proxiedA; + private int proxiedB; + + /** + * The proxy class itself must be serializable by the framework, it must thus have a constructor that + * can be mapped to the properties of the class via getter methods. + */ + public int getProxiedA() { return proxiedA; } + public int getProxiedB() { return proxiedB; } + + public ExampleProxy(int proxiedA, int proxiedB) { + this.proxiedA = proxiedA; + this.proxiedB = proxiedB; + } + } + + /** + * Finally this is the custom serializer that will automatically loaded into the serialization + * framework when the CorDapp Jar is scanned at runtime. + */ + public class ExampleSerializer implements SerializationCustomSerializer { + + /** + * Given an instance of the Example class, create an instance of the proxying object ExampleProxy. + * + * Essentially convert Example -> ExampleProxy + */ + public ExampleProxy toProxy(Example obj) { + return new ExampleProxy(obj.getA(), obj.getB()); + } + + /** + * Conversely, given an instance of the proxy object, revert that back to an instance of the + * type being proxied. + * + * Essentially convert ExampleProxy -> Example + */ + public Example fromProxy(ExampleProxy proxy) { + List l = new ArrayList(2); + l.add(proxy.getProxiedA()); + l.add(proxy.getProxiedB()); + return new Example(l); + } + } + + .. sourcecode:: kotlin + + class ExampleSerializer : SerializationCustomSerializer { + /** + * This is the actual proxy class that is used as an intermediate representation + * of the Example class + */ + data class Proxy(val a: Int, val b: Int) + + /** + * This method should be able to take an instance of the type being proxied and + * transpose it into that form, instantiating an instance of the Proxy object (it + * is this class instance that will be serialized into the byte stream. + */ + override fun toProxy(obj: Example) = Proxy(obj.a, obj.b) + + /** + * This method is used during deserialization. The bytes will have been read + * from the serialized blob and an instance of the Proxy class returned, we must + * now be able to transform that back into an instance of our original class. + * + * In our example this requires us to evoke the static "of" method on the + * Example class, transforming the serialized properties of the Proxy instance + * into a form expected by the construction method of Example. + */ + override fun fromProxy(proxy: Proxy) : Example { + val constructorArg = IntArray(2); + constructorArg[0] = proxy.a + constructorArg[1] = proxy.b + return Example.of(constructorArg) + } + } + +In the above examples + +- ``ExampleSerializer`` is the actual serializer that will be loaded by the framework to serialize instances of the ``Example`` type. +- ``ExampleSerializer.Proxy``, in the Kotlin example, and ``ExampleProxy`` in the Java example, is the intermediate representation used by the framework to represent instances of ``Example`` within the wire format. + +The Proxy Object +---------------- + +The proxy object should be thought of as an intermediate representation that the serialization framework +can reason about. One is being written for a class because, for some reason, that class cannot be +introspected successfully but that framework. It is therefore important to note that the proxy class must +only contain elements that the framework can reason about. + +The proxy class itself is distinct from the proxy serializer. The serializer must refer to the unserializable +type in the ``toProxy`` and ``fromProxy`` methods. + +For example, the first thought a developer may have when implementing a proxy class is to simply *wrap* an +instance of the object being proxied. This is shown below .. sourcecode:: kotlin class ExampleSerializer : SerializationCustomSerializer { - data class Proxy(val a: Int, val b: Int) + /** + * In this example, we are trying to wrap the Example type to make it serializable + */ + data class Proxy(val e: Example) - override fun toProxy(obj: Example) = Proxy(obj.a, obj.b) + override fun toProxy(obj: Example) = Proxy(obj) override fun fromProxy(proxy: Proxy) : Example { - val constructorArg = IntArray(2); - constructorArg[0] = proxy.a - constructorArg[1] = proxy.b - return Example.create(constructorArg) + return proxy.e } } +However, this will not work because what we've created is a recursive loop whereby synthesising a serializer +for the ``Example`` type requires synthesising one for ``ExampleSerializer.Proxy``. However, that requires +one for ``Example`` and so on and so forth until we get a ``StackOverflowException``. + +The solution, as shown initially, is to create the intermediate form (the Proxy object) purely in terms +the serialization framework can reason about. + +.. important:: When composing a proxy object for a class be aware that everything within that structure will be written + into the serialized byte stream. + Whitelisting ------------ By writing a custom serializer for a class it has the effect of adding that class to the whitelist, meaning such diff --git a/docs/source/deterministic-modules.rst b/docs/source/deterministic-modules.rst index f6327f1649..bf86953cec 100644 --- a/docs/source/deterministic-modules.rst +++ b/docs/source/deterministic-modules.rst @@ -1,3 +1,9 @@ +.. raw:: html + + + +.. role:: red + Deterministic Corda Modules =========================== @@ -86,6 +92,103 @@ The build generates each of Corda's deterministic JARs in six steps: This step will fail if ProGuard spots any Java API references that still cannot be satisfied by the deterministic ``rt.jar``, and hence it will break the build. +Configuring IntelliJ with a Deterministic SDK +--------------------------------------------- + +We would like to configure IntelliJ so that it will highlight uses of non-deterministic Java APIs as :red:`not found`. +Or, more specifically, we would like IntelliJ to use the ``deterministic-rt.jar`` as a "Module SDK" for deterministic +modules rather than the ``rt.jar`` from the default project SDK, to make IntelliJ consistent with Gradle. + +This is possible, but slightly tricky to configure because IntelliJ will not recognise an SDK containing only the +``deterministic-rt.jar`` as being valid. It also requires that IntelliJ delegate all build tasks to Gradle, and that +Gradle be configured to use the Project's SDK. + +Creating the Deterministic SDK + #. Create a JDK Home directory with the following contents: + + ``jre/lib/rt.jar`` + + where ``rt.jar`` here is this renamed artifact: + + .. code-block:: xml + + + net.corda + deterministic-rt + api + + + .. + + .. note:: Gradle already creates this JDK in the project's ``jdk8u-deterministic/jdk`` directory, and you could + configure IntelliJ to use this location as well. However, you should also be aware that IntelliJ SDKs + are available for *all* projects to use. + + To create this deterministic JDK image, execute the following: + + .. code-block:: bash + + $ gradlew jdk8u-deterministic:copyJdk + + .. + + #. While IntelliJ is *not* running, locate the ``config/options/jdk.table.xml`` file in IntelliJ's configuration + directory. Add an empty ```` section to this file: + + .. code-block:: xml + + + + + + + + + + + .. + + #. Open IntelliJ and select ``File/Project Structure/Platform Settings/SDKs``. The "1.8 (Deterministic)" SDK should + now be present. Select it and then click on the ``Classpath`` tab. Press the "Add" / "Plus" button to add + ``rt.jar`` to the SDK's classpath. Then select the ``Annotations`` tab and include the same JAR(s) as the other + SDKs. + +Configuring the Corda Project + #. Open the root ``build.gradle`` file and define this property: + + .. code-block:: gradle + + buildscript { + ext { + ... + deterministic_idea_sdk = '1.8 (Deterministic)' + ... + } + } + + .. + +Configuring IntelliJ + #. Go to ``File/Settings/Build, Execution, Deployment/Build Tools/Gradle``, and configure Gradle's JVM to be the + project's JVM. + + #. Go to ``File/Settings/Build, Execution, Deployment/Build Tools/Gradle/Runner``, and select these options: + + - Delegate IDE build/run action to Gradle + - Run tests using the Gradle Test Runner + + #. Delete all of the ``out`` directories that IntelliJ has previously generated for each module. + + #. Go to ``View/Tool Windows/Gradle`` and click the ``Refresh all Gradle projects`` button. + +These steps will enable IntelliJ's presentation compiler to use the deterministic ``rt.jar`` with the following modules: + + - ``core-deterministic`` + - ``serialization-deterministic`` + - ``core-deterministic:testing:common`` + +but still build everything using Gradle with the full JDK. + Testing the Deterministic Modules --------------------------------- @@ -108,7 +211,7 @@ The ``testing`` module also has two sub-modules: .. _deterministic_annotations: Applying @KeepForDJVM and @DeleteForDJVM annotations ---------------------------------------------------------- +---------------------------------------------------- Corda developers need to understand how to annotate classes in the ``core`` and ``serialization`` modules correctly in order to maintain the deterministic JARs. diff --git a/docs/source/network-simulator.rst b/docs/source/network-simulator.rst deleted file mode 100644 index 0d0916d68e..0000000000 --- a/docs/source/network-simulator.rst +++ /dev/null @@ -1,65 +0,0 @@ -Network Simulator -================= - -A network simulator is provided which shows traffic between nodes through the lifecycle of an interest rate swap -contract. It can optionally also show network setup, during which nodes register themselves with the network -map service and are notified of the changes to the map. The network simulator is run from the command line via Gradle: - -**Windows**:: - - gradlew.bat :samples:network-visualiser:run - -**Other**:: - - ./gradlew :samples:network-visualiser:run - -You can produce a standalone JAR of the tool by using the ``:samples:network-visualiser:deployVisualiser`` target -and then using the ``samples/network-visualiser/build/libs/network-visualiser-*-capsule.jar`` file, where * is -whatever the current Corda version is. - -What it is and is not ---------------------- - -The simulator currently exists as an illustrative tool to help with explaining how Corda works in an example scenario. -It utilises the ``Simulator`` tools that support creating a simulated Corda network and the nodes running in it within -a single JVM, as an extension of the ``MockNetwork`` testing framework. See more about the ``MockNetwork`` and -testing flows here: :doc:`flow-testing`. - -Whilst it is not yet fully generic or full featured, the intention is for the simulator to mature into the following, -which it presently cannot do without writing more code: - -1. A tool for visualising new CorDapps and their flows to help with debugging, presentations, explanations and tutorials, - but still running as a simulation in a single JVM. -2. A tool to visualise the activity on a real Corda network deployment, with activity streams being fed from each node - running in its own JVM, most likely on remote hosts. - -Both of these scenarios would be fed by the standard observables in the RPC framework, rather than the local binding -that the simulator uses currently. The ability to step through a flow one step at a time would obviously be restricted -to single JVM simulations. - -Interface ---------- - -.. image:: resources/network-simulator.png - -The network simulator can be run automatically, or stepped manually through each step of the interest rate swap. The -options on the simulator window are: - -Simulate initialisation - If checked, the nodes registering with the network map is shown. Normally this setup step - is not shown, but may be of interest to understand the details of node discovery. -Run - Runs the network simulation in automatic mode, in which it progresses each step on a timed basis. Once running, - the simulation can be paused in order to manually progress it, or reset. -Next - Manually progress the simulation to the next step. -Reset - Reset the simulation (only available when paused). -Map/Circle - How the nodes are shown, by default nodes are rendered on a world map, but alternatively they can rendered - in a circle layout. - -While the simulation runs, details of the steps currently being executed are shown in a sidebar on the left hand side -of the window. - -.. TODO: Add documentation on how to use with different contracts for testing/debugging diff --git a/docs/source/tools-index.rst b/docs/source/tools-index.rst index c9bcafc44b..f2213c7496 100644 --- a/docs/source/tools-index.rst +++ b/docs/source/tools-index.rst @@ -5,6 +5,5 @@ Tools :maxdepth: 1 blob-inspector - network-simulator demobench node-explorer diff --git a/jdk8u-deterministic/.gitignore b/jdk8u-deterministic/.gitignore new file mode 100644 index 0000000000..3216b4dc3d --- /dev/null +++ b/jdk8u-deterministic/.gitignore @@ -0,0 +1 @@ +jdk/ diff --git a/jdk8u-deterministic/build.gradle b/jdk8u-deterministic/build.gradle index 298953c30f..13f468581f 100644 --- a/jdk8u-deterministic/build.gradle +++ b/jdk8u-deterministic/build.gradle @@ -8,7 +8,8 @@ repositories { } ext { - jdk_home = "$buildDir/jdk" + jdk_home = "$projectDir/jdk" + rt_jar = "$jdk_home/jre/lib/rt.jar".toString() } configurations { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index b207671020..da6d35a3a3 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -259,23 +259,27 @@ data class NodeConfigurationImpl( } - override val rpcOptions: NodeRpcOptions = initialiseRpcOptions(rpcAddress, rpcSettings, BrokerRpcSslOptions(baseDirectory / "certificates" / "nodekeystore.jks", keyStorePassword)) + private val actualRpcSettings: NodeRpcSettings - private fun initialiseRpcOptions(explicitAddress: NetworkHostAndPort?, settings: NodeRpcSettings, fallbackSslOptions: BrokerRpcSslOptions): NodeRpcOptions { - return when { - explicitAddress != null -> { - require(settings.address == null) { "Can't provide top-level rpcAddress and rpcSettings.address (they control the same property)." } + init { + actualRpcSettings = when { + rpcAddress != null -> { + require(rpcSettings.address == null) { "Can't provide top-level rpcAddress and rpcSettings.address (they control the same property)." } logger.warn("Top-level declaration of property 'rpcAddress' is deprecated. Please use 'rpcSettings.address' instead.") - settings.copy(address = explicitAddress) + rpcSettings.copy(address = rpcAddress) } else -> { - settings.address ?: throw ConfigException.Missing("rpcSettings.address") - settings + rpcSettings.address ?: throw ConfigException.Missing("rpcSettings.address") + rpcSettings } - }.asOptions(fallbackSslOptions) + } } + override val rpcOptions: NodeRpcOptions + get() { + return actualRpcSettings.asOptions(BrokerRpcSslOptions(baseDirectory / "certificates" / "nodekeystore.jks", keyStorePassword)) + } private fun validateTlsCertCrlConfig(): List { val errors = mutableListOf() @@ -298,7 +302,12 @@ data class NodeConfigurationImpl( override fun validate(): List { val errors = mutableListOf() errors += validateDevModeOptions() - errors += validateRpcOptions(rpcOptions) + val rpcSettingsErrors = validateRpcSettings(rpcSettings) + errors += rpcSettingsErrors + if (rpcSettingsErrors.isEmpty()) { + // Forces lazy property to initialise in order to throw exceptions + rpcOptions + } errors += validateTlsCertCrlConfig() errors += validateNetworkServices() errors += validateH2Settings() @@ -313,7 +322,7 @@ data class NodeConfigurationImpl( return errors } - private fun validateRpcOptions(options: NodeRpcOptions): List { + private fun validateRpcSettings(options: NodeRpcSettings): List { val errors = mutableListOf() if (options.address != null) { if (!options.useSsl && options.adminAddress == null) { diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 76a9a45cc6..cc044e0422 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -238,6 +238,16 @@ class NodeConfigurationImplTest { assertThatCode { rawConfig.parseAsNodeConfiguration() }.doesNotThrowAnyException() } + @Test + fun `missing rpcSettings_adminAddress cause a graceful failure`() { + var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false)) + rawConfig = rawConfig.withoutPath("rpcSettings.adminAddress") + + val config = rawConfig.parseAsNodeConfiguration() + + assertThat(config.validate().filter { it.contains("rpcSettings.adminAddress") }).isNotEmpty + } + @Test fun `compatiilityZoneURL populates NetworkServices`() { val compatibilityZoneURL = URI.create("https://r3.com").toURL() diff --git a/serialization-deterministic/build.gradle b/serialization-deterministic/build.gradle index 3893c56584..256b33b55a 100644 --- a/serialization-deterministic/build.gradle +++ b/serialization-deterministic/build.gradle @@ -9,17 +9,15 @@ */ description 'Corda serialization (deterministic)' -apply plugin: 'kotlin' +apply from: '../deterministic.gradle' apply plugin: 'com.jfrog.artifactory' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'idea' -evaluationDependsOn(':jdk8u-deterministic') evaluationDependsOn(":serialization") def javaHome = System.getProperty('java.home') def jarBaseName = "corda-${project.name}".toString() -def jdkTask = project(':jdk8u-deterministic').assemble -def deterministic_jdk_home = project(':jdk8u-deterministic').jdk_home configurations { runtimeLibraries @@ -38,18 +36,6 @@ dependencies { runtimeLibraries "org.iq80.snappy:snappy:$snappy_version" } -tasks.withType(AbstractCompile) { - dependsOn jdkTask -} - -tasks.withType(JavaCompile) { - options.compilerArgs << "-bootclasspath" << "$deterministic_jdk_home/jre/lib/rt.jar".toString() -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { - kotlinOptions.jdkHome = deterministic_jdk_home -} - jar { baseName 'DOES-NOT-EXIST' // Don't build a jar here because it would be the wrong one. @@ -158,7 +144,7 @@ task metafix(type: MetaFixerTask) { task checkDeterminism(type: ProGuardTask, dependsOn: jdkTask) { injars metafix - libraryjars "$deterministic_jdk_home/jre/lib/rt.jar" + libraryjars deterministic_rt_jar configurations.runtimeLibraries.forEach { libraryjars it.path, filter: '!META-INF/versions/**' @@ -193,3 +179,11 @@ publish { // Must be after publish {} so that the previous install task exists for overwriting. task install(overwrite: true, dependsOn: 'publishToMavenLocal') + +idea { + module { + if (project.hasProperty("deterministic_idea_sdk")) { + jdkName project.property("deterministic_idea_sdk") as String + } + } +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/NotSerializableExceptions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/NotSerializableExceptions.kt new file mode 100644 index 0000000000..df8925e308 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/NotSerializableExceptions.kt @@ -0,0 +1,14 @@ +package net.corda.serialization.internal + +import java.io.IOException +import java.io.NotSerializableException + +class NotSerializableDetailedException(classname: String?, val reason: String) : NotSerializableException(classname) { + override fun toString(): String { + return "Unable to serialize/deserialize $message: $reason" + } +} + +// This exception is thrown when serialization isn't possible but at the point the exception +// is thrown the classname isn't known. It's caught and rethrown as a [NotSerializableDetailedException] +class NotSerializableWithReasonException(message: String?): IOException(message) \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt index 3b74da01cd..ba26017919 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformTypes.kt @@ -15,9 +15,9 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformEnumDefaults import net.corda.core.serialization.CordaSerializationTransformRename +import net.corda.serialization.internal.NotSerializableWithReasonException import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.DescribedTypeConstructor -import java.io.NotSerializableException /** * Enumerated type that represents each transform that can be applied to a class. Used as the key type in @@ -57,25 +57,12 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType */ override fun validate(list: List, constants: Map) { uncheckedCast, List>(list).forEach { - if (!constants.contains(it.new)) { - throw NotSerializableException("Unknown enum constant ${it.new}") - } - - if (!constants.contains(it.old)) { - throw NotSerializableException( - "Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " + - "doesn't exist in constant set $constants") - } - - if (it.old == it.new) { - throw NotSerializableException("Enum extension ${it.new} cannot default to itself") - } - - if (constants[it.old]!! >= constants[it.new]!!) { - throw NotSerializableException( - "Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " + - "defaults to ${it.old}[${constants[it.old]}] which is greater") - } + requireThat(constants.contains(it.new)) {"Unknown enum constant ${it.new}"} + requireThat(constants.contains(it.old)) { "Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " + + "doesn't exist in constant set $constants" } + requireThat(it.old != it.new) { "Enum extension ${it.new} cannot default to itself" } + requireThat(constants[it.old]!! < constants[it.new]!!) { "Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " + + "defaults to ${it.old}[${constants[it.old]}] which is greater" } } } }, @@ -93,21 +80,56 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType * @param constants The list of enum constants on the type the transforms are being applied to */ override fun validate(list: List, constants: Map) { - object : Any() { - val from: MutableSet = mutableSetOf() - val to: MutableSet = mutableSetOf() - }.apply { - @Suppress("UNCHECKED_CAST") (list as List).forEach { rename -> - if (rename.to in this.to || rename.from in this.from) { - throw NotSerializableException("Cyclic renames are not allowed (${rename.to})") - } + data class Node(val transform: RenameSchemaTransform, var next: Node?, var prev: Node?, var visitedBy: Node? = null) { + fun visit(visitedBy: Node) { + this.visitedBy = visitedBy + } + val visited get() = visitedBy != null + } - this.to.add(rename.from) - this.from.add(rename.to) + val graph = mutableListOf() + // Keep two maps of forward links and back links in order to build the graph in one pass + val forwardLinks = hashMapOf() + val reverseLinks = hashMapOf() + + // build a dependency graph + val transforms: List = uncheckedCast(list) + transforms.forEach { rename -> + requireThat(!forwardLinks.contains(rename.from)) { "There are multiple transformations from ${rename.from}, which is not allowed" } + requireThat(!reverseLinks.contains(rename.to)) { "There are multiple transformations to ${rename.to}, which is not allowed" } + val node = Node(rename, forwardLinks[rename.to], reverseLinks[rename.from]) + graph.add(node) + node.next?.prev = node + node.prev?.next = node + forwardLinks[rename.from] = node + reverseLinks[rename.to] = node + } + + // Check that every property in the current type is at the end of a renaming chain, if it is in one + constants.keys.forEach { + requireThat(reverseLinks[it]?.next == null) { "$it is specified as a previously evolved type, but it also exists in the current type" } + } + + // Check for cyclic dependencies + graph.forEach { + if (it.visited) return@forEach + // Find an unvisited node + var currentNode = it + currentNode.visit(it) + while (currentNode.next != null) { + currentNode = currentNode.next!! + if (currentNode.visited) { + requireThat(currentNode.visitedBy != it) { "Cyclic renames are not allowed (${currentNode.transform.from})" } + // we have found the start of another non-cyclic chain of dependencies + // if they were cyclic we would have gone round in a loop and already thrown + break + } + currentNode.visit(it) } } } } + // Transform used to test the unknown handler, leave this at as the final constant, uncomment // when regenerating test cases - if Java had a pre-processor this would be much neater // @@ -131,9 +153,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType override fun newInstance(obj: Any?): TransformTypes { val describedType = obj as DescribedType - if (describedType.descriptor != DESCRIPTOR) { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") - } + requireThat(describedType.descriptor == DESCRIPTOR) { "Unexpected descriptor ${describedType.descriptor}." } return try { values()[describedType.described as Int] @@ -143,5 +163,11 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType } override fun getTypeClass(): Class<*> = TransformTypes::class.java + + protected inline fun requireThat(expr: Boolean, errorMessage: () -> String) { + if (!expr) { + throw NotSerializableWithReasonException(errorMessage()) + } + } } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt index 8f055b2244..4bd90d94b1 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt @@ -13,6 +13,10 @@ package net.corda.serialization.internal.amqp import net.corda.core.KeepForDJVM import net.corda.core.serialization.CordaSerializationTransformEnumDefault import net.corda.core.serialization.CordaSerializationTransformRename +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.trace +import net.corda.serialization.internal.NotSerializableDetailedException +import net.corda.serialization.internal.NotSerializableWithReasonException import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException @@ -202,6 +206,7 @@ class RenameSchemaTransform(val from: String, val to: String) : Transform() { data class TransformsSchema(val types: Map>>) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_SCHEMA.amqpDescriptor + private val logger = contextLogger() /** * Takes a class name and either returns a cached instance of the TransformSet for it or, on a cache miss, @@ -249,10 +254,17 @@ data class TransformsSchema(val types: Map>>) { - get(type, sf).apply { - if (isNotEmpty()) { - map[type] = this + try { + get(type, sf).apply { + if (isNotEmpty()) { + map[type] = this + } } + } catch (e: NotSerializableWithReasonException) { + val message = "Error running transforms for $type: ${e.message}" + logger.error(message) + logger.trace { e.toString() } + throw NotSerializableDetailedException(type, e.message ?: "") } } diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java new file mode 100644 index 0000000000..b6248d9c16 --- /dev/null +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaCustomSerializerTests.java @@ -0,0 +1,108 @@ +package net.corda.serialization.internal.amqp; + +import net.corda.core.serialization.SerializationCustomSerializer; +import net.corda.serialization.internal.AllWhitelist; +import net.corda.serialization.internal.amqp.testutils.TestSerializationContext; +import org.junit.Test; + +import java.io.NotSerializableException; +import java.util.ArrayList; +import java.util.List; + +public class JavaCustomSerializerTests { + /** + * The class lacks a public constructor that takes parameters it can associate + * with its properties and is thus not serializable by the CORDA serialization + * framework. + */ + static class Example { + private Integer a; + private Integer b; + + Integer getA() { return a; } + Integer getB() { return b; } + + public Example(List l) { + this.a = l.get(0); + this.b = l.get(1); + } + } + + /** + * This is the class that will Proxy instances of Example within the serializer + */ + public static class ExampleProxy { + /** + * These properties will be serialized into the byte stream, this is where we choose how to + * represent instances of the object we're proxying. In this example, which is somewhat + * contrived, this choice is obvious. In your own classes / 3rd party libraries, however, this + * may require more thought. + */ + private Integer proxiedA; + private Integer proxiedB; + + /** + * The proxu class itself must be serializable by the framework, it must thus have a constructor that + * can be mapped to the properties of the class via getter methods. + */ + public Integer getProxiedA() { return proxiedA; } + public Integer getProxiedB() { return proxiedB; } + + + public ExampleProxy(Integer proxiedA, Integer proxiedB) { + this.proxiedA = proxiedA; + this.proxiedB = proxiedB; + } + } + + /** + * Finally this is the custom serializer that will automatically loaded into the serialization + * framework when the CorDapp Jar is scanned at runtime. + */ + public static class ExampleSerializer implements SerializationCustomSerializer { + + /** + * Given an instance of the Example class, create an instance of the proxying object ExampleProxy. + * + * Essentially convert Example -> ExampleProxy + */ + public ExampleProxy toProxy(Example obj) { + return new ExampleProxy(obj.getA(), obj.getB()); + } + + /** + * Conversely, given an instance of the proxy object, revert that back to an instance of the + * type being proxied. + * + * Essentially convert ExampleProxy -> Example + * + */ + public Example fromProxy(ExampleProxy proxy) { + List l = new ArrayList(2); + l.add(proxy.getProxiedA()); + l.add(proxy.getProxiedB()); + return new Example(l); + } + + } + + @Test + public void serializeExample() throws NotSerializableException, NoSuchFieldException, IllegalAccessException { + SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(), + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + SerializationOutput ser = new SerializationOutput(factory); + + List l = new ArrayList(2); + l.add(10); + l.add(20); + Example e = new Example(l); + + CorDappCustomSerializer ccs = new CorDappCustomSerializer(new ExampleSerializer(), factory); + factory.registerExternal(ccs); + + ser.serialize(e, TestSerializationContext.testSerializationContext); + + + } +} diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt index d671b897b0..1f622570ce 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt @@ -11,9 +11,12 @@ package net.corda.serialization.internal.amqp import net.corda.core.serialization.* +import net.corda.serialization.internal.NotSerializableDetailedException import net.corda.serialization.internal.amqp.testutils.* import net.corda.testing.common.internal.ProjectStructure.projectRootDir +import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.api.Condition import org.junit.Test import java.io.NotSerializableException import java.net.URI @@ -452,6 +455,68 @@ class EnumEvolvabilityTests { assertTrue(envelope.transformsSchema.types[WithUnknownTest::class.java.name]!!.containsKey(TransformTypes.Unknown)) } + // + // In this test we check that multiple transforms of a property are accepted + // + @CordaSerializationTransformRenames( + CordaSerializationTransformRename(from = "A", to = "B"), + CordaSerializationTransformRename(from = "B", to = "C") + ) + enum class AcceptMultipleRename { C } + + @Test + fun acceptMultipleRename() { + data class C(val e: AcceptMultipleRename) + + val sf = testDefaultFactory() + SerializationOutput(sf).serialize(C(AcceptMultipleRename.C)) + } + + // + // In this example we will try to rename two different things to the same thing, + // which is not allowed + // + @CordaSerializationTransformRenames( + CordaSerializationTransformRename(from = "D", to = "C"), + CordaSerializationTransformRename(from = "E", to = "C") + ) + enum class RejectMultipleRenameTo { A, B, C } + + @Test + fun rejectMultipleRenameTo() { + data class C(val e: RejectMultipleRenameTo) + + val sf = testDefaultFactory() + assertThatThrownBy { + SerializationOutput(sf).serialize(C(RejectMultipleRenameTo.A)) + }.isInstanceOfSatisfying(NotSerializableDetailedException::class.java) { ex -> + assertThat(ex.reason).isEqualToIgnoringCase("There are multiple transformations to C, which is not allowed") + assertThat(ex.message).endsWith(RejectMultipleRenameTo::class.simpleName) + } + } + + // + // In this example we will try to rename two different things from the same thing, + // which is not allowed + // + @CordaSerializationTransformRenames( + CordaSerializationTransformRename(from = "D", to = "C"), + CordaSerializationTransformRename(from = "D", to = "B") + ) + enum class RejectMultipleRenameFrom { A, B, C } + + @Test + fun rejectMultipleRenameFrom() { + data class C(val e: RejectMultipleRenameFrom) + + val sf = testDefaultFactory() + assertThatThrownBy { + SerializationOutput(sf).serialize(C(RejectMultipleRenameFrom.A)) + }.isInstanceOf(NotSerializableException::class.java) + .hasToString("Unable to serialize/deserialize net.corda.serialization.internal.amqp.EnumEvolvabilityTests\$RejectMultipleRenameFrom: " + + "There are multiple transformations from D, which is not allowed") + } + // // In this example we will have attempted to rename D back to C // @@ -544,5 +609,4 @@ class EnumEvolvabilityTests { SerializationOutput(sf).serialize(C(RejectBadDefaultToSelf.D)) }.isInstanceOf(NotSerializableException::class.java) } - } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolveTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolveTests.kt index 32c26d36bc..cbe733637e 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolveTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolveTests.kt @@ -160,7 +160,6 @@ class EnumEvolveTests { // Finally, the version we're using to test with enum class DeserializeWithRename { A, B, C } - @Ignore("https://r3-cev.atlassian.net/browse/CORDA-1498") @Test fun deserializeWithRename() { val resource = "${javaClass.simpleName}.${testName()}" diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.AA b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.AA index 16b9b723f8..2e13e6009e 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.AA and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.AA differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.B b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.B index fb59df3314..238c1852bb 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.B and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.B differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.C b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.C index ef16d41cf7..77d2e4c708 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.C and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.1.C differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.AA b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.AA index 4f75773756..e55546c2ae 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.AA and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.AA differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.BB b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.BB index 521bf9ec2f..29df5002fa 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.BB and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.BB differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.C b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.C index bdf755c4ee..9a73a35316 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.C and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.2.C differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.AA b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.AA index c597affc7b..d8b827969e 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.AA and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.AA differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.C b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.C index 342fdce508..dd259450b3 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.C and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.C differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.XX b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.XX index 86d9a16721..5bd475b237 100644 Binary files a/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.XX and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/EnumEvolveTests.deserializeWithRename.3.XX differ diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 5041c04e6f..36f7264c75 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -104,6 +104,7 @@ class DriverDSLImpl( val networkParameters: NetworkParameters, val notaryCustomOverrides: Map ) : InternalDriverDSL { + private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! private var _shutdownManager: ShutdownManager? = null @@ -660,6 +661,12 @@ class DriverDSLImpl( val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize) + + // Destroy the child process when the parent exits.This is needed even when `waitForAllNodesToFinish` is + // true because we don't want orphaned processes in the case that the parent process is terminated by the + // user, for example when the `tools:explorer:runDemoNodes` gradle task is stopped with CTRL-C. + shutdownManager.registerProcessShutdown(process) + if (waitForAllNodesToFinish) { state.locked { processes += object : Waitable { @@ -668,8 +675,6 @@ class DriverDSLImpl( } } } - } else { - shutdownManager.registerProcessShutdown(process) } val p2pReadyFuture = addressMustBeBoundFuture(executorService, config.corda.p2pAddress, process) return p2pReadyFuture.flatMap { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt index e407d0e8de..f8647a4b72 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ShutdownManager.kt @@ -17,12 +17,18 @@ import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds +import net.corda.nodeapi.internal.addShutdownHook import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicInteger class ShutdownManager(private val executorService: ExecutorService) { + + init { + addShutdownHook { shutdown() } + } + private class State { val registeredShutdowns = ArrayList Unit>>() var isShuttingDown = false