diff --git a/build.gradle b/build.gradle index 5a841245ad..20cee98ce9 100644 --- a/build.gradle +++ b/build.gradle @@ -100,7 +100,6 @@ buildscript { ext.jcabi_manifests_version = '1.1' ext.picocli_version = '3.0.0' - 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)' @@ -413,6 +412,7 @@ bintrayConfig { 'corda-serialization', 'corda-serialization-deterministic', 'corda-tools-blob-inspector', + 'corda-tools-explorer', 'corda-tools-network-bootstrapper', 'corda-bridgeserver', 'corda-ptflows', diff --git a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt index fb3743688d..247e15c464 100644 --- a/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt +++ b/core-deterministic/testing/common/src/main/kotlin/net/corda/deterministic/common/TransactionVerificationRequest.kt @@ -19,7 +19,7 @@ class TransactionVerificationRequest(val wtxToVerify: SerializedBytes() } val attachmentMap = attachments.mapNotNull { it as? MockContractAttachment } - .associateBy(Attachment::id, { ContractAttachment(it, it.contract, uploader=TEST_UPLOADER) }) + .associateBy(Attachment::id) { ContractAttachment(it, it.contract, uploader=TEST_UPLOADER) } val contractAttachmentMap = emptyMap() @Suppress("DEPRECATION") return wtxToVerify.deserialize().toLedgerTransaction( diff --git a/create-jdk8u/build.gradle b/create-jdk8u/build.gradle index db71504863..b0a67e20cd 100644 --- a/create-jdk8u/build.gradle +++ b/create-jdk8u/build.gradle @@ -57,6 +57,7 @@ task makeJdk(type: Exec) { task runtimeJar(type: Jar, dependsOn: makeJdk) { baseName 'deterministic-rt' + inputs.dir "libs" from(zipTree("libs/rt.jar")) from(zipTree("libs/jce.jar")) diff --git a/docs/source/deterministic-modules.rst b/docs/source/deterministic-modules.rst index 0c77c088d1..0525de7027 100644 --- a/docs/source/deterministic-modules.rst +++ b/docs/source/deterministic-modules.rst @@ -104,54 +104,60 @@ This is possible, but slightly tricky to configure because IntelliJ will not rec Gradle be configured to use the Project's SDK. Creating the Deterministic SDK - #. Create a JDK Home directory with the following contents: + Gradle creates a suitable JDK image in the project's ``jdk8u-deterministic/jdk`` directory, and you can + configure IntelliJ to use this location for this SDK. However, you should also be aware that IntelliJ SDKs + are available for *all* projects to use. - ``jre/lib/rt.jar`` + To create this JDK image, execute the following: - where ``rt.jar`` here is this renamed artifact: + .. code-block:: bash - .. code-block:: xml + $ gradlew jdk8u-deterministic:copyJdk - - net.corda - deterministic-rt - api - + .. - .. + Now select ``File/Project Structure/Platform Settings/SDKs`` and add a new JDK SDK with the + ``jdk8u-deterministic/jdk`` directory as its home. Rename this SDK to something like "1.8 (Deterministic)". - .. 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. + This *should* be sufficient for IntelliJ. However, if IntelliJ realises that this SDK does not contain a + full JDK then you will need to configure the new SDK by hand: - To create this deterministic JDK image, execute the following: + #. Create a JDK Home directory with the following contents: - .. code-block:: bash + ``jre/lib/rt.jar`` - $ gradlew jdk8u-deterministic:copyJdk + where ``rt.jar`` here is this renamed artifact: - .. + .. code-block:: xml - #. 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: + + net.corda + deterministic-rt + api + - .. code-block:: xml + .. - - - - - - - - + #. 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. + + + + + + + + + + .. + + #. 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: diff --git a/jdk8u-deterministic/build.gradle b/jdk8u-deterministic/build.gradle index 13f468581f..2e4d0a2153 100644 --- a/jdk8u-deterministic/build.gradle +++ b/jdk8u-deterministic/build.gradle @@ -8,23 +8,39 @@ repositories { } ext { - jdk_home = "$projectDir/jdk" + jdk_home = "$projectDir/jdk".toString() rt_jar = "$jdk_home/jre/lib/rt.jar".toString() } configurations { - jdk + jdk.resolutionStrategy { + cacheChangingModulesFor 0, 'seconds' + } } dependencies { - jdk "net.corda:deterministic-rt:$deterministic_rt_version:api" + // Ensure everyone uses the latest SNAPSHOT. + jdk "net.corda:deterministic-rt:latest.integration:api" } task copyJdk(type: Copy) { + outputs.dir jdk_home + from(configurations.jdk.asPath) { rename 'deterministic-rt-(.*).jar', 'rt.jar' } into "$jdk_home/jre/lib" + + doLast { + def eol = System.getProperty('line.separator') + file("$jdk_home/release").write "JAVA_VERSION=\"1.8.0_172\"$eol" + mkdir "$jdk_home/bin" + file("$jdk_home/bin/javac").with { + write "#!/bin/sh\necho \"javac 1.8.0_172\"\n" + setExecutable true, false + return + } + } } assemble.dependsOn copyJdk diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 9db888b4c9..af281a5bd8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -26,6 +26,7 @@ import net.corda.node.NodeRegistrationOption import net.corda.node.SerialFilter import net.corda.node.VersionInfo import net.corda.node.defaultSerialFilter +import net.corda.node.internal.cordapp.MultipleCordappsForFlowException import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfigurationImpl import net.corda.node.services.config.shouldStartLocalShell @@ -148,9 +149,13 @@ open class NodeStartup(val args: Array) { try { cmdlineOptions.baseDirectory.createDirectories() startNode(conf, versionInfo, startTime, cmdlineOptions) + } catch (e: DatabaseMigrationException) { logger.error(e.message) return false + } catch (e: MultipleCordappsForFlowException) { + logger.error(e.message) + return false } catch (e: CouldNotCreateDataSourceException) { logger.error(e.message, e.cause) return false 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 317df6a66c..d09bbecd07 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 @@ -80,7 +80,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List corDapp.allFlows.map { flow -> flow to corDapp } } .groupBy { it.first } .mapValues { - require(it.value.size == 1) { "There are multiple CorDapp jars on the classpath for flow ${it.value.first().first.name}: ${it.value.map { it.second.name }.joinToString()}." } + if(it.value.size > 1) { throw MultipleCordappsForFlowException("There are multiple CorDapp JARs on the classpath for flow ${it.value.first().first.name}: [ ${it.value.joinToString { it.second.name }} ].") } it.value.single().second } } @@ -401,3 +401,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List): AMQPSerializer { val constructorArgs = arrayOfNulls(constructor.parameters.size) + // Java doesn't care about nullability unless it's a primitive in which + // case it can't be referenced. Unfortunately whilst Kotlin does apply + // Nullability annotations we cannot use them here as they aren't + // retained at runtime so we cannot rely on the absence of + // any particular NonNullable annotation type to indicate cross + // compiler nullability + val isKotlin = (new.type.javaClass.declaredAnnotations.any { + it.annotationClass.qualifiedName == "kotlin.Metadata" + }) + constructor.parameters.withIndex().forEach { - readersAsSerialized[it.value.name!!]?.apply { - this.resultsIndex = it.index - } ?: if (!it.value.type.isMarkedNullable) { - throw NotSerializableException( - "New parameter ${it.value.name} is mandatory, should be nullable for evolution to work") + if ((readersAsSerialized[it.value.name!!] ?.apply { this.resultsIndex = it.index }) == null) { + // If there is no value in the byte stream to map to the parameter of the constructor + // this is ok IFF it's a Kotlin class and the parameter is non nullable OR + // its a Java class and the parameter is anything but an unboxed primitive. + // Otherwise we throw the error and leave + if ((isKotlin && !it.value.type.isMarkedNullable) + || (!isKotlin && isJavaPrimitive(it.value.type.jvmErasure.java)) + ) { + throw NotSerializableException( + "New parameter \"${it.value.name}\" is mandatory, should be nullable for evolution " + + "to work, isKotlinClass=$isKotlin type=${it.value.type}") + } } } return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor, constructorArgs) @@ -161,8 +179,10 @@ abstract class EvolutionSerializer( * @param factory the [SerializerFactory] associated with the serialization * context this serializer is being built for */ - fun make(old: CompositeType, new: ObjectSerializer, - factory: SerializerFactory): AMQPSerializer { + fun make(old: CompositeType, + new: ObjectSerializer, + factory: SerializerFactory + ): AMQPSerializer { // The order in which the properties were serialised is important and must be preserved val readersAsSerialized = LinkedHashMap() old.fields.forEach { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index ed4148147c..ff19337f20 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -550,3 +550,18 @@ fun hasCordaSerializable(type: Class<*>): Boolean { || type.interfaces.any(::hasCordaSerializable) || (type.superclass != null && hasCordaSerializable(type.superclass)) } + +fun isJavaPrimitive(type: Class<*>) = type in JavaPrimitiveTypes.primativeTypes + +private object JavaPrimitiveTypes { + val primativeTypes = hashSetOf>( + Boolean::class.java, + Char::class.java, + Byte::class.java, + Short::class.java, + Int::class.java, + Long::class.java, + Float::class.java, + Double::class.java, + Void::class.java) +} diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index 7331bb29d4..f4043673c9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -88,7 +88,7 @@ open class SerializerFactory( lenientCarpenter: Boolean = false, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), fingerPrinter: FingerPrinter = SerializerFingerPrinter() - ) : this(whitelist, ClassCarpenterImpl(classLoader, whitelist, lenientCarpenter), evolutionSerializerGetter, fingerPrinter) + ) : this(whitelist, ClassCarpenterImpl(whitelist, classLoader, lenientCarpenter), evolutionSerializerGetter, fingerPrinter) init { fingerPrinter.setOwner(this) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt index efcced603c..b329360d18 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/carpenter/ClassCarpenter.kt @@ -114,10 +114,10 @@ interface ClassCarpenter { * Equals/hashCode methods are not yet supported. */ @DeleteForDJVM -class ClassCarpenterImpl(cl: ClassLoader = Thread.currentThread().contextClassLoader, - override val whitelist: ClassWhitelist, - private val lenient: Boolean = false) : ClassCarpenter { - +class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: ClassWhitelist, + cl: ClassLoader = Thread.currentThread().contextClassLoader, + private val lenient: Boolean = false +) : ClassCarpenter { // TODO: Generics. // TODO: Sandbox the generated code when a security manager is in use. // TODO: Generate equals/hashCode. diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaEvolutionTests.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaEvolutionTests.java new file mode 100644 index 0000000000..fa8449840e --- /dev/null +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaEvolutionTests.java @@ -0,0 +1,100 @@ +package net.corda.serialization.internal.amqp; + +import kotlin.Suppress; +import net.corda.core.serialization.SerializedBytes; +import net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt; +import net.corda.serialization.internal.amqp.testutils.TestSerializationContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class JavaEvolutionTests { + @Rule + public final ExpectedException exception = ExpectedException.none(); + + // Class as it was when it was serialized and written to disk. Uncomment + // if the test referencing the object needs regenerating. + /* + static class N1 { + private String word; + public N1(String word) { this.word = word; } + public String getWord() { return word; } + } + */ + // Class as it exists now with the newly added element + static class N1 { + private String word; + private Integer wibble; + + public N1(String word, Integer wibble) { + this.word = word; + this.wibble = wibble; + } + public String getWord() { return word; } + public Integer getWibble() { return wibble; } + } + + // Class as it was when it was serialized and written to disk. Uncomment + // if the test referencing the object needs regenerating. + /* + static class N2 { + private String word; + public N2(String word) { this.word = word; } + public String getWord() { return word; } + } + */ + + // Class as it exists now with the newly added element + @SuppressWarnings("unused") + static class N2 { + private String word; + private float wibble; + + public N2(String word, float wibble) { + this.word = word; + this.wibble = wibble; + } + public String getWord() { return word; } + public float getWibble() { return wibble; } + } + + SerializerFactory factory = AMQPTestUtilsKt.testDefaultFactory(); + + @Test + public void testN1AddsNullableInt() throws IOException { + // Uncomment to regenerate the base state of the test + /* + N1 n = new N1("potato"); + AMQPTestUtilsKt.writeTestResource(this, new SerializationOutput(factory).serialize( + n, TestSerializationContext.testSerializationContext)); + */ + + N1 n2 = new DeserializationInput(factory).deserialize( + new SerializedBytes<>(AMQPTestUtilsKt.readTestResource(this)), + N1.class, + TestSerializationContext.testSerializationContext); + assertEquals(n2.getWord(), "potato"); + assertNull(n2.getWibble()); + } + + @Test + public void testN2AddsPrimitive() throws IOException { + // Uncomment to regenerate the base state of the test + /* + N2 n = new N2("This is only a test"); + + AMQPTestUtilsKt.writeTestResource(this, new SerializationOutput(factory).serialize( + n, TestSerializationContext.testSerializationContext)); + */ + + exception.expect(NotSerializableException.class); + new DeserializationInput(factory).deserialize( + new SerializedBytes<>(AMQPTestUtilsKt.readTestResource(this)), + N2.class, + TestSerializationContext.testSerializationContext); + } +} diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt index 7de5078164..ea9e9d7302 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -108,7 +108,7 @@ class StaticInitialisationOfSerializedObjectTest { // Version of a serializer factory that will allow the class carpenter living on the // factory to have a different whitelist applied to it than the factory class TestSerializerFactory(wl1: ClassWhitelist, wl2: ClassWhitelist) : - SerializerFactory(wl1, ClassCarpenterImpl(ClassLoader.getSystemClassLoader(), wl2)) + SerializerFactory(wl1, ClassCarpenterImpl(wl2, ClassLoader.getSystemClassLoader())) // This time have the serialization factory and the carpenter use different whitelists @Test diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CarpenterExceptionTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CarpenterExceptionTests.kt index a3f936b4d9..8aeaa99ac7 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CarpenterExceptionTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/carpenter/CarpenterExceptionTests.kt @@ -89,7 +89,7 @@ class CarpenterExceptionTests { // carpent that class up. However, when looking at the fields specified as properties of that class // we set the class loader of the ClassCarpenter to reject one of them, resulting in a CarpentryError // which we then want the code to wrap in a NotSerializeableException - val cc = ClassCarpenterImpl(TestClassLoader(listOf(C2::class.jvmName)), AllWhitelist) + val cc = ClassCarpenterImpl(AllWhitelist, TestClassLoader(listOf(C2::class.jvmName))) val factory = TestFactory(cc) Assertions.assertThatThrownBy { diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/JavaEvolutionTests.testN1AddsNullableInt b/serialization/src/test/resources/net/corda/serialization/internal/amqp/JavaEvolutionTests.testN1AddsNullableInt new file mode 100644 index 0000000000..334de45fa2 Binary files /dev/null and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/JavaEvolutionTests.testN1AddsNullableInt differ diff --git a/serialization/src/test/resources/net/corda/serialization/internal/amqp/JavaEvolutionTests.testN2AddsPrimitive b/serialization/src/test/resources/net/corda/serialization/internal/amqp/JavaEvolutionTests.testN2AddsPrimitive new file mode 100644 index 0000000000..ffa5f54f8b Binary files /dev/null and b/serialization/src/test/resources/net/corda/serialization/internal/amqp/JavaEvolutionTests.testN2AddsPrimitive differ diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index 6d28eb9b98..839f901f7a 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -12,9 +12,15 @@ * This build.gradle exists to package Node Explorer as an executable fat jar. */ apply plugin: 'us.kirchmeier.capsule' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Node Explorer' +configurations { + runtimeArtifacts.extendsFrom runtime +} + repositories { mavenLocal() mavenCentral() @@ -57,3 +63,19 @@ task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').co } build.dependsOn buildExplorerJAR + +artifacts { + runtimeArtifacts buildExplorerJAR + publish buildExplorerJAR { + classifier "" + } +} + +jar { + classifier "ignore" +} + +publish { + disableDefaultJar = true + name 'corda-tools-explorer' +}