diff --git a/build.gradle b/build.gradle index 48c4c91418..1fe54a2eb3 100644 --- a/build.gradle +++ b/build.gradle @@ -264,6 +264,9 @@ allprojects { force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" force "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + // Force dependencies to use the same version of Guava as Corda. + force "com.google.guava:guava:$guava_version" + // Demand that everything uses our given version of Netty. eachDependency { details -> if (details.requested.group == 'io.netty' && details.requested.name.startsWith('netty-')) { diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt index 5f49d0422a..17810136a8 100644 --- a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/Elements.kt @@ -13,18 +13,19 @@ import org.objectweb.asm.Opcodes.ACC_SYNTHETIC import java.util.* private const val DEFAULT_CONSTRUCTOR_MARKER = "ILkotlin/jvm/internal/DefaultConstructorMarker;" +private const val DEFAULT_FUNCTION_MARKER = "ILjava/lang/Object;" private const val DUMMY_PASSES = 1 private val DECLARES_DEFAULT_VALUE_MASK: Int = DECLARES_DEFAULT_VALUE.toFlags(true).inv() -internal abstract class Element(val name: String, val descriptor: String) { +abstract class Element(val name: String, val descriptor: String) { private var lifetime: Int = DUMMY_PASSES open val isExpired: Boolean get() = --lifetime < 0 } -internal class MethodElement(name: String, descriptor: String, val access: Int = 0) : Element(name, descriptor) { +class MethodElement(name: String, descriptor: String, val access: Int = 0) : Element(name, descriptor) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false @@ -65,7 +66,7 @@ internal class MethodElement(name: String, descriptor: String, val access: Int = * A class cannot have two fields with the same name but different types. However, * it can define extension functions and properties. */ -internal class FieldElement(name: String, descriptor: String = "?", val extension: String = "()") : Element(name, descriptor) { +class FieldElement(name: String, descriptor: String = "?", val extension: String = "()") : Element(name, descriptor) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false @@ -79,6 +80,30 @@ internal class FieldElement(name: String, descriptor: String = "?", val extensio val String.extensionType: String get() = substring(0, 1 + indexOf(')')) +/** + * Returns a fully-qualified class name as it would exist + * in the byte-code, e.g. as "a/b/c/ClassName$Nested". + */ +fun NameResolver.getClassInternalName(idx: Int): String + = getQualifiedClassName(idx).replace('.', '$') + +/** + * Construct the signatures of the synthetic methods that + * Kotlin would create to handle default parameter values. + */ +fun String.toKotlinDefaultConstructor(): String { + val closer = lastIndexOf(')') + return substring(0, closer) + DEFAULT_CONSTRUCTOR_MARKER + substring(closer) +} + +fun String.toKotlinDefaultFunction(classDescriptor: String): String { + val opener = indexOf('(') + val closer = lastIndexOf(')') + return (substring(0, opener) + "\$default(" + + classDescriptor + substring(opener + 1, closer) + DEFAULT_FUNCTION_MARKER + + substring(closer)) +} + /** * Convert Kotlin getter/setter method data to [MethodElement] objects. */ @@ -124,7 +149,7 @@ internal fun JvmProtoBuf.JvmPropertySignature.toFieldElement(property: ProtoBuf. } /** - * Rewrites metadata for constructor parameters. + * Rewrites metadata for function and constructor parameters. */ internal fun ProtoBuf.Constructor.Builder.updateValueParameters( updater: (ProtoBuf.ValueParameter) -> ProtoBuf.ValueParameter @@ -135,6 +160,15 @@ internal fun ProtoBuf.Constructor.Builder.updateValueParameters( return this } +internal fun ProtoBuf.Function.Builder.updateValueParameters( + updater: (ProtoBuf.ValueParameter) -> ProtoBuf.ValueParameter +): ProtoBuf.Function.Builder { + for (idx in 0 until valueParameterList.size) { + setValueParameter(idx, updater(valueParameterList[idx])) + } + return this +} + internal fun ProtoBuf.ValueParameter.clearDeclaresDefaultValue(): ProtoBuf.ValueParameter { return if (DECLARES_DEFAULT_VALUE.get(flags)) { toBuilder().setFlags(flags and DECLARES_DEFAULT_VALUE_MASK).build() @@ -142,3 +176,6 @@ internal fun ProtoBuf.ValueParameter.clearDeclaresDefaultValue(): ProtoBuf.Value this } } + +internal val List.hasAnyDefaultValues + get() = any { DECLARES_DEFAULT_VALUE.get(it.flags) } diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/FilterTransformer.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/FilterTransformer.kt index e6e2089381..164f3bf5d7 100644 --- a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/FilterTransformer.kt +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/FilterTransformer.kt @@ -22,7 +22,7 @@ class FilterTransformer private constructor ( private val removeAnnotations: Set, private val deleteAnnotations: Set, private val stubAnnotations: Set, - private val unwantedClasses: MutableSet, + private val unwantedElements: UnwantedCache, private val unwantedFields: MutableSet, private val deletedMethods: MutableSet, private val stubbedMethods: MutableSet @@ -33,7 +33,7 @@ class FilterTransformer private constructor ( removeAnnotations: Set, deleteAnnotations: Set, stubAnnotations: Set, - unwantedClasses: MutableSet + unwantedElements: UnwantedCache ) : this( visitor = visitor, logger = logger, @@ -41,7 +41,7 @@ class FilterTransformer private constructor ( removeAnnotations = removeAnnotations, deleteAnnotations = deleteAnnotations, stubAnnotations = stubAnnotations, - unwantedClasses = unwantedClasses, + unwantedElements = unwantedElements, unwantedFields = mutableSetOf(), deletedMethods = mutableSetOf(), stubbedMethods = mutableSetOf() @@ -57,7 +57,7 @@ class FilterTransformer private constructor ( || stubbedMethods.isNotEmpty() || super.hasUnwantedElements - private fun isUnwantedClass(name: String): Boolean = unwantedClasses.contains(name) + private fun isUnwantedClass(name: String): Boolean = unwantedElements.containsClass(name) private fun hasDeletedSyntheticMethod(name: String): Boolean = deletedMethods.any { method -> name.startsWith("$className\$${method.visibleName}\$") } @@ -69,7 +69,7 @@ class FilterTransformer private constructor ( removeAnnotations = removeAnnotations, deleteAnnotations = deleteAnnotations, stubAnnotations = stubAnnotations, - unwantedClasses = unwantedClasses, + unwantedElements = unwantedElements, unwantedFields = unwantedFields, deletedMethods = deletedMethods, stubbedMethods = stubbedMethods @@ -86,7 +86,7 @@ class FilterTransformer private constructor ( logger.info("- Removing annotation {}", descriptor) return null } else if (deleteAnnotations.contains(descriptor)) { - if (unwantedClasses.add(className)) { + if (unwantedElements.addClass(className)) { logger.info("- Identified class {} as unwanted", className) } } @@ -110,6 +110,7 @@ class FilterTransformer private constructor ( logger.debug("--- method ---> {}", method) if (deletedMethods.contains(method)) { logger.info("- Deleted method {}{}", method.name, method.descriptor) + unwantedElements.addMethod(className, method) deletedMethods.remove(method) return null } @@ -131,7 +132,7 @@ class FilterTransformer private constructor ( override fun visitInnerClass(clsName: String, outerName: String?, innerName: String?, access: Int) { logger.debug("--- inner class {} [outer: {}, inner: {}]", clsName, outerName, innerName) if (isUnwantedClass || hasDeletedSyntheticMethod(clsName)) { - if (unwantedClasses.add(clsName)) { + if (unwantedElements.addClass(clsName)) { logger.info("- Deleted inner class {}", clsName) } } else if (isUnwantedClass(clsName)) { @@ -143,8 +144,10 @@ class FilterTransformer private constructor ( override fun visitOuterClass(outerName: String, methodName: String?, methodDescriptor: String?) { logger.debug("--- outer class {} [enclosing method {},{}]", outerName, methodName, methodDescriptor) - if (isUnwantedClass(outerName)) { - logger.info("- Deleted reference to outer class {}", outerName) + if (unwantedElements.containsMethod(outerName, methodName, methodDescriptor)) { + if (unwantedElements.addClass(className)) { + logger.info("- Identified class {} as unwanted by its outer class", className) + } } else { super.visitOuterClass(outerName, methodName, methodDescriptor) } @@ -180,8 +183,8 @@ class FilterTransformer private constructor ( deletedFields = unwantedFields, deletedFunctions = partitioned[false] ?: emptyList(), deletedConstructors = partitioned[true] ?: emptyList(), - deletedNestedClasses = unwantedClasses.filter { it.startsWith(prefix) }.map { it.drop(prefix.length) }, - deletedClasses = unwantedClasses, + deletedNestedClasses = unwantedElements.classes.filter { it.startsWith(prefix) }.map { it.drop(prefix.length) }, + deletedClasses = unwantedElements.classes, handleExtraMethod = ::delete, d1 = d1, d2 = d2) diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt index 1d0c99f4cc..78100b2613 100644 --- a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/JarFilterTask.kt @@ -138,7 +138,7 @@ open class JarFilterTask : DefaultTask() { } private inner class Filter(inFile: File) { - private val unwantedClasses: MutableSet = mutableSetOf() + private val unwantedElements = UnwantedCache() private val source: Path = inFile.toPath() private val target: Path = toFiltered(inFile).toPath() @@ -251,7 +251,7 @@ open class JarFilterTask : DefaultTask() { removeAnnotations = descriptorsForRemove, deleteAnnotations = descriptorsForDelete, stubAnnotations = descriptorsForStub, - unwantedClasses = unwantedClasses + unwantedElements = unwantedElements ) /* diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt index 65988db056..f6d97c7a24 100644 --- a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetaFixerTransformer.kt @@ -34,10 +34,11 @@ internal abstract class MetaFixerTransformer( parser: (InputStream, ExtensionRegistryLite) -> T ) { private val stringTableTypes: StringTableTypes - private val nameResolver: NameResolver + protected val nameResolver: NameResolver protected val message: T protected abstract val typeTable: TypeTable + protected open val classDescriptor: String = "" protected open val classKind: ProtoBuf.Class.Kind? = null protected abstract val properties: MutableList protected abstract val functions: MutableList @@ -78,7 +79,7 @@ internal abstract class MetaFixerTransformer( var count = 0 var idx = 0 while (idx < sealedSubclassNames.size) { - val sealedSubclassName = nameResolver.getString(sealedSubclassNames[idx]).replace('.', '$') + val sealedSubclassName = nameResolver.getClassInternalName(sealedSubclassNames[idx]) if (actualClasses.contains(sealedSubclassName)) { ++idx } else { @@ -93,15 +94,25 @@ internal abstract class MetaFixerTransformer( private fun filterFunctions(): Int { var count = 0 var idx = 0 - while (idx < functions.size) { - val signature = JvmProtoBufUtil.getJvmMethodSignature(functions[idx], nameResolver, typeTable) - if ((signature == null) || actualMethods.contains(signature)) { - ++idx - } else { - logger.info("-- removing method: {}", signature) - functions.removeAt(idx) - ++count + removed@ while (idx < functions.size) { + val function = functions[idx] + val signature = JvmProtoBufUtil.getJvmMethodSignature(function, nameResolver, typeTable) + if (signature != null) { + if (!actualMethods.contains(signature)) { + logger.info("-- removing method: {}", signature) + functions.removeAt(idx) + ++count + continue@removed + } else if (function.valueParameterList.hasAnyDefaultValues + && !actualMethods.contains(signature.toKotlinDefaultFunction(classDescriptor))) { + logger.info("-- removing default parameter values: {}", signature) + functions[idx] = function.toBuilder() + .updateValueParameters(ProtoBuf.ValueParameter::clearDeclaresDefaultValue) + .build() + ++count + } } + ++idx } return count } @@ -109,15 +120,25 @@ internal abstract class MetaFixerTransformer( private fun filterConstructors(): Int { var count = 0 var idx = 0 - while (idx < constructors.size) { - val signature = JvmProtoBufUtil.getJvmConstructorSignature(constructors[idx], nameResolver, typeTable) - if ((signature == null) || actualMethods.contains(signature)) { - ++idx - } else { - logger.info("-- removing constructor: {}", signature) - constructors.removeAt(idx) - ++count + removed@ while (idx < constructors.size) { + val constructor = constructors[idx] + val signature = JvmProtoBufUtil.getJvmConstructorSignature(constructor, nameResolver, typeTable) + if (signature != null) { + if (!actualMethods.contains(signature)) { + logger.info("-- removing constructor: {}", signature) + constructors.removeAt(idx) + ++count + continue@removed + } else if (constructor.valueParameterList.hasAnyDefaultValues + && !actualMethods.contains(signature.toKotlinDefaultConstructor())) { + logger.info("-- removing default parameter values: {}", signature) + constructors[idx] = constructor.toBuilder() + .updateValueParameters(ProtoBuf.ValueParameter::clearDeclaresDefaultValue) + .build() + ++count + } } + ++idx } return count } @@ -127,30 +148,32 @@ internal abstract class MetaFixerTransformer( var idx = 0 removed@ while (idx < properties.size) { val property = properties[idx] - val signature = property.getExtensionOrNull(propertySignature) ?: continue - val field = signature.toFieldElement(property, nameResolver, typeTable) - val getterMethod = signature.toGetter(nameResolver) + val signature = property.getExtensionOrNull(propertySignature) + if (signature != null) { + val field = signature.toFieldElement(property, nameResolver, typeTable) + val getterMethod = signature.toGetter(nameResolver) - /** - * A property annotated with [JvmField] will use a field instead of a getter method. - * But properties without [JvmField] will also usually have a backing field. So we only - * remove a property that has either lost its getter method, or never had a getter method - * and has lost its field. - * - * Having said that, we cannot remove [JvmField] properties from a companion object class - * because these properties are implemented as static fields on the companion's host class. - */ - val isValidProperty = if (getterMethod == null) { - actualFields.contains(field) || classKind == COMPANION_OBJECT - } else { - actualMethods.contains(getterMethod.name + getterMethod.descriptor) - } + /** + * A property annotated with [JvmField] will use a field instead of a getter method. + * But properties without [JvmField] will also usually have a backing field. So we only + * remove a property that has either lost its getter method, or never had a getter method + * and has lost its field. + * + * Having said that, we cannot remove [JvmField] properties from a companion object class + * because these properties are implemented as static fields on the companion's host class. + */ + val isValidProperty = if (getterMethod == null) { + actualFields.contains(field) || classKind == COMPANION_OBJECT + } else { + actualMethods.contains(getterMethod.signature) + } - if (!isValidProperty) { - logger.info("-- removing property: {},{}", field.name, field.descriptor) - properties.removeAt(idx) - ++count - continue@removed + if (!isValidProperty) { + logger.info("-- removing property: {},{}", field.name, field.descriptor) + properties.removeAt(idx) + ++count + continue@removed + } } ++idx } @@ -196,6 +219,7 @@ internal class ClassMetaFixerTransformer( ProtoBuf.Class::parseFrom ) { override val typeTable = TypeTable(message.typeTable) + override val classDescriptor = "L${nameResolver.getClassInternalName(message.fqName)};" override val classKind: ProtoBuf.Class.Kind = CLASS_KIND.get(message.flags) override val properties = mutableList(message.propertyList) override val functions = mutableList(message.functionList) @@ -204,18 +228,15 @@ internal class ClassMetaFixerTransformer( override val sealedSubclassNames= mutableList(message.sealedSubclassFqNameList) override fun rebuild(): ProtoBuf.Class = message.toBuilder().apply { + clearConstructor().addAllConstructor(constructors) + clearFunction().addAllFunction(functions) + if (nestedClassNames.size != nestedClassNameCount) { clearNestedClassName().addAllNestedClassName(nestedClassNames) } if (sealedSubclassNames.size != sealedSubclassFqNameCount) { clearSealedSubclassFqName().addAllSealedSubclassFqName(sealedSubclassNames) } - if (constructors.size != constructorCount) { - clearConstructor().addAllConstructor(constructors) - } - if (functions.size != functionCount) { - clearFunction().addAllFunction(functions) - } if (properties.size != propertyCount) { clearProperty().addAllProperty(properties) } @@ -248,9 +269,8 @@ internal class PackageMetaFixerTransformer( override val constructors = mutableListOf() override fun rebuild(): ProtoBuf.Package = message.toBuilder().apply { - if (functions.size != functionCount) { - clearFunction().addAllFunction(functions) - } + clearFunction().addAllFunction(functions) + if (properties.size != propertyCount) { clearProperty().addAllProperty(properties) } diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt index 73d0db24ca..1701654c18 100644 --- a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/MetadataTransformer.kt @@ -200,7 +200,7 @@ internal abstract class MetadataTransformer( var count = 0 var idx = 0 while (idx < sealedSubclassNames.size) { - val subclassName = nameResolver.getString(sealedSubclassNames[idx]).replace('.', '$') + val subclassName = nameResolver.getClassInternalName(sealedSubclassNames[idx]) if (deletedClasses.contains(subclassName)) { logger.info("-- removing sealed subclass: {}", subclassName) sealedSubclassNames.removeAt(idx) @@ -248,7 +248,7 @@ internal class ClassMetadataTransformer( ProtoBuf.Class::parseFrom ) { override val typeTable = TypeTable(message.typeTable) - override val className = nameResolver.getString(message.fqName) + override val className = nameResolver.getClassInternalName(message.fqName) override val nestedClassNames = mutableList(message.nestedClassNameList) override val sealedSubclassNames = mutableList(message.sealedSubclassFqNameList) override val properties = mutableList(message.propertyList) diff --git a/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/UnwantedCache.kt b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/UnwantedCache.kt new file mode 100644 index 0000000000..5a8d28bc73 --- /dev/null +++ b/buildSrc/jarfilter/src/main/kotlin/net/corda/gradle/jarfilter/UnwantedCache.kt @@ -0,0 +1,46 @@ +package net.corda.gradle.jarfilter + +import java.util.Collections.unmodifiableMap + +/** + * A persistent cache of all of the classes and methods that JarFilter has + * removed. This cache belongs to the Gradle task itself and so is shared + * by successive filter passes. + * + * The internal method cache is only required for those classes which are + * being kept. When an entire class is declared as "unwanted", any entry + * it may have in the method cache is removed. + */ +class UnwantedCache { + private val _classes: MutableSet = mutableSetOf() + private val _classMethods: MutableMap> = mutableMapOf() + + val classes: Set get() = _classes + val classMethods: Map> get() = unmodifiableMap(_classMethods) + + fun containsClass(className: String): Boolean = _classes.contains(className) + + fun addClass(className: String): Boolean { + return _classes.add(className).also { isAdded -> + if (isAdded) { + _classMethods.remove(className) + } + } + } + + fun addMethod(className: String, method: MethodElement) { + if (!containsClass(className)) { + _classMethods.getOrPut(className) { mutableSetOf() }.add(method) + } + } + + private fun containsMethod(className: String, method: MethodElement): Boolean { + return _classMethods[className]?.contains(method) ?: false + } + + fun containsMethod(className: String, methodName: String?, methodDescriptor: String?): Boolean { + return containsClass(className) || + (methodName != null && methodDescriptor != null && containsMethod(className, MethodElement(methodName, methodDescriptor))) + } +} + diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteInnerLambdaTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteInnerLambdaTest.kt new file mode 100644 index 0000000000..14f1f4edc9 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/DeleteInnerLambdaTest.kt @@ -0,0 +1,84 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.matcher.isConstructor +import net.corda.gradle.unwanted.HasInt +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.core.IsCollectionContaining.* +import org.hamcrest.core.IsNot.* +import org.junit.Assert.* +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TemporaryFolder +import org.junit.rules.TestRule +import kotlin.test.assertFailsWith + +class DeleteInnerLambdaTest { + companion object { + private const val LAMBDA_CLASS = "net.corda.gradle.HasInnerLambda" + private const val SIZE = 64 + + private val testProjectDir = TemporaryFolder() + private val testProject = JarFilterProject(testProjectDir, "delete-inner-lambda") + private val constructInt = isConstructor(LAMBDA_CLASS, Int::class) + private val constructBytes = isConstructor(LAMBDA_CLASS, ByteArray::class) + + private lateinit var sourceClasses: List + private lateinit var filteredClasses: List + + @ClassRule + @JvmField + val rules: TestRule = RuleChain + .outerRule(testProjectDir) + .around(testProject) + + @BeforeClass + @JvmStatic + fun setup() { + sourceClasses = testProject.sourceJar.getClassNames(LAMBDA_CLASS) + filteredClasses = testProject.filteredJar.getClassNames(LAMBDA_CLASS) + } + } + + @Test + fun `test lambda class is deleted`() { + assertThat(sourceClasses) + .contains(LAMBDA_CLASS) + .hasSize(2) + assertThat(filteredClasses).containsExactly(LAMBDA_CLASS) + } + + @Test + fun `test host class`() { + classLoaderFor(testProject.sourceJar).use { cl -> + cl.load(LAMBDA_CLASS).apply { + getConstructor(Int::class.java).newInstance(SIZE).also { obj -> + assertEquals(SIZE, obj.intData()) + } + kotlin.constructors.also { ctors -> + assertThat("(Int) not found", ctors, hasItem(constructInt)) + assertThat("(byte[]) not found", ctors, hasItem(constructBytes)) + } + + getConstructor(ByteArray::class.java).newInstance(ByteArray(SIZE)).also { obj -> + assertEquals(SIZE, obj.intData()) + } + } + } + + classLoaderFor(testProject.filteredJar).use { cl -> + cl.load(LAMBDA_CLASS).apply { + assertFailsWith { getConstructor(Int::class.java) } + kotlin.constructors.also { ctors -> + assertThat("(Int) still exists", ctors, not(hasItem(constructInt))) + assertThat("(byte[]) not found", ctors, hasItem(constructBytes)) + } + + getConstructor(ByteArray::class.java).newInstance(ByteArray(SIZE)).also { obj -> + assertEquals(SIZE, obj.intData()) + } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt index a6cfdb6a8c..9e99d4e470 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/FieldRemovalTest.kt @@ -52,7 +52,7 @@ class FieldRemovalTest { removeAnnotations = emptySet(), deleteAnnotations = setOf(Deletable::class.jvmName.descriptor), stubAnnotations = emptySet(), - unwantedClasses = mutableSetOf() + unwantedElements = UnwantedCache() ) }, COMPUTE_MAXS) return bytecode.toClass(type, asType) diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorDefaultParameterTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorDefaultParameterTest.kt new file mode 100644 index 0000000000..df36c6e08c --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorDefaultParameterTest.kt @@ -0,0 +1,110 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.recodeMetadataFor +import net.corda.gradle.jarfilter.asm.toClass +import net.corda.gradle.jarfilter.matcher.isConstructor +import net.corda.gradle.unwanted.HasAll +import org.assertj.core.api.Assertions.* +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.* +import org.junit.Assert.* +import org.junit.BeforeClass +import org.junit.Test +import kotlin.reflect.full.primaryConstructor + +class MetaFixConstructorDefaultParameterTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixConstructorDefaultParameterTest::class) + private val primaryCon + = isConstructor(WithConstructorParameters::class, Long::class, Int::class, String::class) + private val secondaryCon + = isConstructor(WithConstructorParameters::class, Char::class, String::class) + + lateinit var sourceClass: Class + lateinit var fixedClass: Class + + @BeforeClass + @JvmStatic + fun setup() { + val bytecode = recodeMetadataFor() + sourceClass = bytecode.toClass() + fixedClass = bytecode.fixMetadata(logger, pathsOf(WithConstructorParameters::class)) + .toClass() + } + } + + @Test + fun `test source constructor has optional parameters`() { + with(sourceClass.kotlin.constructors) { + assertThat(size).isEqualTo(2) + assertThat("source primary constructor missing", this, hasItem(primaryCon)) + assertThat("source secondary constructor missing", this, hasItem(secondaryCon)) + } + + val sourcePrimary = sourceClass.kotlin.primaryConstructor + ?: throw AssertionError("source primary constructor missing") + sourcePrimary.call(BIG_NUMBER, NUMBER, MESSAGE).apply { + assertThat(longData()).isEqualTo(BIG_NUMBER) + assertThat(intData()).isEqualTo(NUMBER) + assertThat(stringData()).isEqualTo(MESSAGE) + } + + val sourceSecondary = sourceClass.kotlin.constructors.firstOrNull { it != sourcePrimary } + ?: throw AssertionError("source secondary constructor missing") + sourceSecondary.call('X', MESSAGE).apply { + assertThat(stringData()).isEqualTo("X$MESSAGE") + } + + assertTrue("All source parameters should have defaults", sourcePrimary.hasAllOptionalParameters) + } + + @Test + fun `test fixed constructors exist`() { + with(fixedClass.kotlin.constructors) { + assertThat(size).isEqualTo(2) + assertThat("fixed primary constructor missing", this, hasItem(primaryCon)) + assertThat("fixed secondary constructor missing", this, hasItem(secondaryCon)) + } + } + + @Test + fun `test fixed primary constructor has mandatory parameters`() { + val fixedPrimary = fixedClass.kotlin.primaryConstructor + ?: throw AssertionError("fixed primary constructor missing") + assertTrue("All fixed parameters should be mandatory", fixedPrimary.hasAllMandatoryParameters) + } + + @Test + fun `test fixed secondary constructor still has optional parameters`() { + val fixedSecondary = (fixedClass.kotlin.constructors - fixedClass.kotlin.primaryConstructor).firstOrNull() + ?: throw AssertionError("fixed secondary constructor missing") + assertTrue("Some fixed parameters should be optional", fixedSecondary.hasAnyOptionalParameters) + } + + class MetadataTemplate( + private val longData: Long = 0, + private val intData: Int = 0, + private val message: String = DEFAULT_MESSAGE + ) : HasAll { + @Suppress("UNUSED") + constructor(prefix: Char, message: String = DEFAULT_MESSAGE) : this(message = prefix + message) + + override fun longData(): Long = longData + override fun intData(): Int = intData + override fun stringData(): String = message + } +} + +class WithConstructorParameters( + private val longData: Long, + private val intData: Int, + private val message: String +) : HasAll { + @Suppress("UNUSED") + constructor(prefix: Char, message: String = DEFAULT_MESSAGE) : this(0, 0, prefix + message) + + override fun longData(): Long = longData + override fun intData(): Int = intData + override fun stringData(): String = message +} + diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt index cbab25571a..35630a82a3 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixConstructorTest.kt @@ -13,14 +13,8 @@ import kotlin.jvm.kotlin class MetaFixConstructorTest { companion object { private val logger: Logger = StdOutLogging(MetaFixConstructorTest::class) - private val unwantedCon = isConstructor( - returnType = WithConstructor::class, - parameters = *arrayOf(Int::class, Long::class) - ) - private val wantedCon = isConstructor( - returnType = WithConstructor::class, - parameters = *arrayOf(Long::class) - ) + private val unwantedCon = isConstructor(WithConstructor::class, Int::class, Long::class) + private val wantedCon = isConstructor(WithConstructor::class, Long::class) } @Test @@ -32,15 +26,19 @@ class MetaFixConstructorTest { // added to the metadata, and that the class is valid. val sourceObj = sourceClass.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER) assertEquals(BIG_NUMBER, sourceObj.longData()) - assertThat("(Int,Long) not found", sourceClass.kotlin.constructors, hasItem(unwantedCon)) - assertThat("(Long) not found", sourceClass.kotlin.constructors, hasItem(wantedCon)) + with(sourceClass.kotlin.constructors) { + assertThat("(Int,Long) not found", this, hasItem(unwantedCon)) + assertThat("(Long) not found", this, hasItem(wantedCon)) + } // Rewrite the metadata according to the contents of the bytecode. val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithConstructor::class)).toClass() val fixedObj = fixedClass.getDeclaredConstructor(Long::class.java).newInstance(BIG_NUMBER) assertEquals(BIG_NUMBER, fixedObj.longData()) - assertThat("(Int,Long) still exists", fixedClass.kotlin.constructors, not(hasItem(unwantedCon))) - assertThat("(Long) not found", fixedClass.kotlin.constructors, hasItem(wantedCon)) + with(fixedClass.kotlin.constructors) { + assertThat("(Int,Long) still exists", this, not(hasItem(unwantedCon))) + assertThat("(Long) not found", this, hasItem(wantedCon)) + } } class MetadataTemplate(private val longData: Long) : HasLong { diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionDefaultParameterTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionDefaultParameterTest.kt new file mode 100644 index 0000000000..65ed6a300f --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionDefaultParameterTest.kt @@ -0,0 +1,96 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.recodeMetadataFor +import net.corda.gradle.jarfilter.asm.toClass +import net.corda.gradle.jarfilter.matcher.isFunction +import org.assertj.core.api.Assertions.* +import org.gradle.api.logging.Logger +import org.hamcrest.core.IsCollectionContaining.* +import org.junit.Assert.* +import org.junit.BeforeClass +import org.junit.Test +import kotlin.reflect.KFunction +import kotlin.reflect.full.declaredFunctions + +class MetaFixFunctionDefaultParameterTest { + companion object { + private val logger: Logger = StdOutLogging(MetaFixFunctionDefaultParameterTest::class) + private val hasMandatoryParams + = isFunction("hasMandatoryParams", String::class, Long::class, Int::class, String::class) + private val hasOptionalParams + = isFunction("hasOptionalParams", String::class, String::class) + + lateinit var sourceClass: Class + lateinit var fixedClass: Class + + @BeforeClass + @JvmStatic + fun setup() { + val bytecode = recodeMetadataFor() + sourceClass = bytecode.toClass() + fixedClass = bytecode.fixMetadata(logger, pathsOf(WithFunctionParameters::class)) + .toClass() + } + } + + @Test + fun `test source functions have default parameters`() { + with(sourceClass.kotlin.declaredFunctions) { + assertThat(size).isEqualTo(2) + assertThat("source mandatory parameters missing", this, hasItem(hasMandatoryParams)) + assertThat("source optional parameters missing", this, hasItem(hasOptionalParams)) + } + + val sourceUnwanted = sourceClass.kotlin.declaredFunctions.findOrFail("hasMandatoryParams") + assertThat(sourceUnwanted.call(sourceClass.newInstance(), BIG_NUMBER, NUMBER, MESSAGE)) + .isEqualTo("Long: $BIG_NUMBER, Int: $NUMBER, String: $MESSAGE") + + assertTrue("All source parameters should be optional", sourceUnwanted.hasAllOptionalParameters) + + val sourceWanted = sourceClass.kotlin.declaredFunctions.findOrFail("hasOptionalParams") + assertThat(sourceWanted.call(sourceClass.newInstance(), MESSAGE)) + .isEqualTo(MESSAGE) + + assertTrue("All source parameters should be optional", sourceWanted.hasAllOptionalParameters) + } + + @Test + fun `test fixed functions exist`() { + with(fixedClass.kotlin.declaredFunctions) { + assertThat(size).isEqualTo(2) + assertThat("fixed mandatory parameters missing", this, hasItem(hasMandatoryParams)) + assertThat("fixed optional parameters missing", this, hasItem(hasOptionalParams)) + } + } + + @Test + fun `test unwanted default parameters are removed`() { + val fixedMandatory = fixedClass.kotlin.declaredFunctions.findOrFail("hasMandatoryParams") + assertTrue("All fixed parameters should be mandatory", fixedMandatory.hasAllMandatoryParameters) + } + + @Test + fun `test wanted default parameters are kept`() { + val fixedOptional = fixedClass.kotlin.declaredFunctions.findOrFail("hasOptionalParams") + assertTrue("All fixed parameters should be optional", fixedOptional.hasAllOptionalParameters) + } + + @Suppress("UNUSED") + abstract class MetadataTemplate { + abstract fun hasMandatoryParams(longData: Long = 0, intData: Int = 0, message: String = DEFAULT_MESSAGE): String + abstract fun hasOptionalParams(message: String = DEFAULT_MESSAGE): String + } + + private fun Iterable>.findOrFail(name: String): KFunction { + return find { it.name == name } ?: throw AssertionError("$name missing") + } +} + +@Suppress("UNUSED") +class WithFunctionParameters { + fun hasMandatoryParams(longData: Long, intData: Int, message: String): String { + return "Long: $longData, Int: $intData, String: $message" + } + + fun hasOptionalParams(message: String = DEFAULT_MESSAGE): String = message +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt index 1f4011d84e..1aad6b1929 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixFunctionTest.kt @@ -15,11 +15,7 @@ class MetaFixFunctionTest { companion object { private val logger: Logger = StdOutLogging(MetaFixFunctionTest::class) private val longData = isFunction("longData", Long::class) - private val unwantedFun = isFunction( - name = "unwantedFun", - returnType = String::class, - parameters = *arrayOf(String::class) - ) + private val unwantedFun = isFunction("unwantedFun", String::class, String::class) } @Test @@ -31,15 +27,19 @@ class MetaFixFunctionTest { // added to the metadata, and that the class is valid. val sourceObj = sourceClass.newInstance() assertEquals(BIG_NUMBER, sourceObj.longData()) - assertThat("unwantedFun(String) not found", sourceClass.kotlin.declaredFunctions, hasItem(unwantedFun)) - assertThat("longData not found", sourceClass.kotlin.declaredFunctions, hasItem(longData)) + with(sourceClass.kotlin.declaredFunctions) { + assertThat("unwantedFun(String) not found", this, hasItem(unwantedFun)) + assertThat("longData not found", this, hasItem(longData)) + } // Rewrite the metadata according to the contents of the bytecode. val fixedClass = bytecode.fixMetadata(logger, pathsOf(WithFunction::class)).toClass() val fixedObj = fixedClass.newInstance() assertEquals(BIG_NUMBER, fixedObj.longData()) - assertThat("unwantedFun(String) still exists", fixedClass.kotlin.declaredFunctions, not(hasItem(unwantedFun))) - assertThat("longData not found", fixedClass.kotlin.declaredFunctions, hasItem(longData)) + with(fixedClass.kotlin.declaredFunctions) { + assertThat("unwantedFun(String) still exists", this, not(hasItem(unwantedFun))) + assertThat("longData not found", this, hasItem(longData)) + } } class MetadataTemplate : HasLong { diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageDefaultParameterTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageDefaultParameterTest.kt new file mode 100644 index 0000000000..74bb986fac --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageDefaultParameterTest.kt @@ -0,0 +1,39 @@ +package net.corda.gradle.jarfilter + +import net.corda.gradle.jarfilter.asm.metadataAs +import net.corda.gradle.jarfilter.asm.toClass +import org.gradle.api.logging.Logger +import org.junit.BeforeClass +import org.junit.Test +import kotlin.reflect.full.declaredFunctions +import kotlin.test.assertFailsWith + +/** + * These tests cannot actually "test" anything until Kotlin reflection + * supports package metadata. Until then, we can only execute the code + * paths to ensure they don't throw any exceptions. + */ +class MetaFixPackageDefaultParameterTest { + companion object { + private const val TEMPLATE_CLASS = "net.corda.gradle.jarfilter.template.PackageWithDefaultParameters" + private const val DEFAULT_PARAMETERS_CLASS = "net.corda.gradle.jarfilter.PackageWithDefaultParameters" + private val logger: Logger = StdOutLogging(MetaFixPackageDefaultParameterTest::class) + + lateinit var sourceClass: Class + lateinit var fixedClass: Class + + @BeforeClass + @JvmStatic + fun setup() { + val defaultParametersClass = Class.forName(DEFAULT_PARAMETERS_CLASS) + val bytecode = defaultParametersClass.metadataAs(Class.forName(TEMPLATE_CLASS)) + sourceClass = bytecode.toClass(defaultParametersClass, Any::class.java) + fixedClass = bytecode.fixMetadata(logger, setOf(DEFAULT_PARAMETERS_CLASS)).toClass(sourceClass, Any::class.java) + } + } + + @Test + fun `test package functions`() { + assertFailsWith { fixedClass.kotlin.declaredFunctions } + } +} diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt index 0ef49ac46b..37948f219b 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/MetaFixPackageTest.kt @@ -41,21 +41,21 @@ class MetaFixPackageTest { @Test fun testPackageFunction() { - assertFailsWith { sourceClass.kotlin.declaredFunctions } + assertFailsWith { fixedClass.kotlin.declaredFunctions } //assertThat("templateFun() not found", sourceClass.kotlin.declaredFunctions, hasItem(staticFun)) //assertThat("templateFun() still exists", fixedClass.kotlin.declaredFunctions, not(hasItem(staticFun))) } @Test fun testPackageVal() { - assertFailsWith { sourceClass.kotlin.declaredMembers } + assertFailsWith { fixedClass.kotlin.declaredMembers } //assertThat("templateVal not found", sourceClass.kotlin.declaredMembers, hasItem(staticVal)) //assertThat("templateVal still exists", fixedClass.kotlin.declaredMembers, not(hasItem(staticVal))) } @Test fun testPackageVar() { - assertFailsWith { sourceClass.kotlin.declaredMembers } + assertFailsWith { fixedClass.kotlin.declaredMembers } //assertThat("templateVar not found", sourceClass.kotlin.declaredMembers, hasItem(staticVar)) //assertThat("templateVar still exists", fixedClass.kotlin.declaredMembers, not(hasItem(staticVar))) } diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/PackageWithDefaultParameters.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/PackageWithDefaultParameters.kt new file mode 100644 index 0000000000..8665cb17cd --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/PackageWithDefaultParameters.kt @@ -0,0 +1,12 @@ +@file:JvmName("PackageWithDefaultParameters") +@file:Suppress("UNUSED") +package net.corda.gradle.jarfilter + +/** + * Example package functions, one with default parameter values and one without. + * We will rewrite this class's metadata so that it expects both functions to + * have default parameter values, and then ask the [MetaFixerTask] to fix it. + */ +fun hasDefaultParameters(intData: Int=0, message: String=DEFAULT_MESSAGE): String = "$message: intData=$intData" + +fun hasMandatoryParameters(longData: Long, message: String): String = "$message: longData=$longData" diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt index 83ef0258ba..e174ed147a 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/StaticFieldRemovalTest.kt @@ -34,7 +34,7 @@ class StaticFieldRemovalTest { removeAnnotations = emptySet(), deleteAnnotations = setOf(Deletable::class.jvmName.descriptor), stubAnnotations = emptySet(), - unwantedClasses = mutableSetOf() + unwantedElements = UnwantedCache() ) }, COMPUTE_MAXS) return bytecode.toClass(type, asType) diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UnwantedCacheTest.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UnwantedCacheTest.kt new file mode 100644 index 0000000000..53abf1de1b --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/UnwantedCacheTest.kt @@ -0,0 +1,54 @@ +package net.corda.gradle.jarfilter + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class UnwantedCacheTest { + private companion object { + private const val CLASS_NAME = "org.testing.MyClass" + private const val LONG_ARG = "(J)V" + private const val NO_ARG = "()V" + } + + private lateinit var cache: UnwantedCache + + @Before + fun setup() { + cache = UnwantedCache() + } + + @Test + fun testEmptyCache() { + assertFalse(cache.containsClass(CLASS_NAME)) + assertFalse(cache.containsMethod(CLASS_NAME, null, null)) + assertFalse(cache.containsMethod(CLASS_NAME, "", NO_ARG)) + } + + @Test + fun testAddingClass() { + cache.addClass(CLASS_NAME) + assertTrue(cache.containsClass(CLASS_NAME)) + assertTrue(cache.containsMethod(CLASS_NAME, null, null)) + assertTrue(cache.containsMethod(CLASS_NAME, "", NO_ARG)) + } + + @Test + fun testAddingMethod() { + cache.addMethod(CLASS_NAME, MethodElement("", LONG_ARG)) + assertTrue(cache.containsMethod(CLASS_NAME, "", LONG_ARG)) + assertFalse(cache.containsMethod(CLASS_NAME, "", NO_ARG)) + assertFalse(cache.containsMethod(CLASS_NAME, "destroy", LONG_ARG)) + assertFalse(cache.containsMethod(CLASS_NAME, null, null)) + assertFalse(cache.containsMethod(CLASS_NAME, "nonsense", null)) + assertFalse(cache.containsClass(CLASS_NAME)) + } + + @Test + fun testAddingMethodFollowedByClass() { + cache.addMethod(CLASS_NAME, MethodElement("", LONG_ARG)) + cache.addClass(CLASS_NAME) + assertTrue(cache.containsMethod(CLASS_NAME, "", LONG_ARG)) + assertEquals(0, cache.classMethods.size) + } +} \ No newline at end of file diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt index b27809b5db..443d2a3ef0 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/Utilities.kt @@ -16,6 +16,7 @@ import java.util.zip.ZipFile import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KParameter +import kotlin.reflect.full.valueParameters const val DEFAULT_MESSAGE = "" const val MESSAGE = "Goodbye, Cruel World!" @@ -67,8 +68,17 @@ fun arrayOfJunk(size: Int) = ByteArray(size).apply { } } +val KFunction<*>.hasAnyOptionalParameters: Boolean + get() = valueParameters.any(KParameter::isOptional) + +val KFunction<*>.hasAllOptionalParameters: Boolean + get() = valueParameters.all(KParameter::isOptional) + +val KFunction<*>.hasAllMandatoryParameters: Boolean + get() = valueParameters.none(KParameter::isOptional) + val KClass.noArgConstructor: KFunction? - get() = constructors.firstOrNull { it.parameters.all(KParameter::isOptional) } + get() = constructors.firstOrNull(KFunction<*>::hasAllOptionalParameters) @Throws(MalformedURLException::class) fun classLoaderFor(jar: Path) = URLClassLoader(arrayOf(jar.toUri().toURL()), classLoader) diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt index b4852deaa7..313a562f05 100644 --- a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/asm/ClassMetadata.kt @@ -1,6 +1,7 @@ package net.corda.gradle.jarfilter.asm import net.corda.gradle.jarfilter.MetadataTransformer +import net.corda.gradle.jarfilter.getClassInternalName import net.corda.gradle.jarfilter.toPackageFormat import net.corda.gradle.jarfilter.mutableList import org.gradle.api.logging.Logger @@ -24,7 +25,7 @@ internal class ClassMetadata( ProtoBuf.Class::parseFrom ) { override val typeTable = TypeTable(message.typeTable) - override val className = nameResolver.getString(message.fqName) + override val className = nameResolver.getClassInternalName(message.fqName) override val nestedClassNames = mutableList(message.nestedClassNameList) override val properties = mutableList(message.propertyList) override val functions = mutableList(message.functionList) @@ -35,13 +36,13 @@ internal class ClassMetadata( override fun rebuild(): ProtoBuf.Class = message val sealedSubclasses: List = sealedSubclassNames.map { - // Transform "a/b/c/BaseName.SubclassName" -> "a.b.c.BaseName$SubclassName" - nameResolver.getString(it).replace('.', '$').toPackageFormat }.toList() + // Transform "a/b/c/BaseName$SubclassName" -> "a.b.c.BaseName$SubclassName" + nameResolver.getClassInternalName(it).toPackageFormat }.toList() val nestedClasses: List init { val internalClassName = className.toPackageFormat - nestedClasses = nestedClassNames.map { "$internalClassName\$${nameResolver.getString(it)}" }.toList() + nestedClasses = nestedClassNames.map { "$internalClassName\$${nameResolver.getClassInternalName(it)}" }.toList() } } diff --git a/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/template/PackageWithDefaultParameters.kt b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/template/PackageWithDefaultParameters.kt new file mode 100644 index 0000000000..186c1eb634 --- /dev/null +++ b/buildSrc/jarfilter/src/test/kotlin/net/corda/gradle/jarfilter/template/PackageWithDefaultParameters.kt @@ -0,0 +1,9 @@ +@file:JvmName("PackageWithDefaultParameters") +@file:Suppress("UNUSED") +package net.corda.gradle.jarfilter.template + +import net.corda.gradle.jarfilter.DEFAULT_MESSAGE + +fun hasDefaultParameters(intData: Int=0, message: String=DEFAULT_MESSAGE): String = "$message: intData=$intData" + +fun hasMandatoryParameters(longData: Long=0, message: String=DEFAULT_MESSAGE): String = "$message: longData=$longData" diff --git a/buildSrc/jarfilter/src/test/resources/delete-inner-lambda/build.gradle b/buildSrc/jarfilter/src/test/resources/delete-inner-lambda/build.gradle new file mode 100644 index 0000000000..f5725b6e52 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-inner-lambda/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$kotlin_version' + id 'net.corda.plugins.jar-filter' +} +apply from: 'repositories.gradle' + +sourceSets { + main { + kotlin { + srcDir files( + '../resources/test/delete-inner-lambda/kotlin', + '../resources/test/annotations/kotlin' + ) + } + } +} + +dependencies { + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compileOnly files('../../unwanteds/build/libs/unwanteds.jar') +} + +jar { + baseName = 'delete-inner-lambda' +} + +import net.corda.gradle.jarfilter.JarFilterTask +task jarFilter(type: JarFilterTask) { + jars jar + annotations { + forDelete = ["net.corda.gradle.jarfilter.DeleteMe"] + } +} diff --git a/buildSrc/jarfilter/src/test/resources/delete-inner-lambda/kotlin/net/corda/gradle/HasInnerLambda.kt b/buildSrc/jarfilter/src/test/resources/delete-inner-lambda/kotlin/net/corda/gradle/HasInnerLambda.kt new file mode 100644 index 0000000000..774ebb80d6 --- /dev/null +++ b/buildSrc/jarfilter/src/test/resources/delete-inner-lambda/kotlin/net/corda/gradle/HasInnerLambda.kt @@ -0,0 +1,19 @@ +@file:Suppress("UNUSED") +package net.corda.gradle + +import net.corda.gradle.jarfilter.DeleteMe +import net.corda.gradle.unwanted.HasInt + +class HasInnerLambda(private val bytes: ByteArray) : HasInt { + @DeleteMe + constructor(size: Int) : this(ZeroArray { size }.bytes) + + override fun intData() = bytes.size +} + +/** + * Do NOT inline this lambda! + */ +class ZeroArray(initialSize: () -> Int) { + val bytes: ByteArray = ByteArray(initialSize()) { 0 } +} diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index ac1f0ab520..f504db23a5 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -24,7 +24,9 @@ dependencies { compile project(':core') // Quasar, for suspendable fibres. - compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" + compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") { + transitive = false + } testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "junit:junit:$junit_version" diff --git a/constants.properties b/constants.properties index 876ab7d370..9e3bdc0ec6 100644 --- a/constants.properties +++ b/constants.properties @@ -8,7 +8,7 @@ # Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. # -gradlePluginsVersion=4.0.25 +gradlePluginsVersion=4.0.26 kotlinVersion=1.2.51 platformVersion=4 guavaVersion=25.1-jre diff --git a/core/build.gradle b/core/build.gradle index 4a98c2bedf..b5ad8f2795 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -82,7 +82,9 @@ dependencies { testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Quasar, for suspendable fibres. - compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" + compileOnly("$quasar_group:quasar-core:$quasar_version:jdk8") { + transitive = false + } // Thread safety annotations compile "com.google.code.findbugs:jsr305:$jsr305_version" diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt index cc6984c7e6..8831a5c1e7 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt @@ -15,6 +15,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.serialization.CordaSerializable +// DOCSTART FlowAsyncOperation /** * Interface for arbitrary operations that can be invoked in a flow asynchronously - the flow will suspend until the * operation completes. Operation parameters are expected to be injected via constructor. @@ -24,10 +25,13 @@ interface FlowAsyncOperation { /** Performs the operation in a non-blocking fashion. */ fun execute(): CordaFuture } +// DOCEND FlowAsyncOperation +// DOCSTART executeAsync /** Executes the specified [operation] and suspends until operation completion. */ @Suspendable fun FlowLogic.executeAsync(operation: FlowAsyncOperation, maySkipCheckpoint: Boolean = false): R { val request = FlowIORequest.ExecuteAsyncOperation(operation) return stateMachine.suspend(request, maySkipCheckpoint) } +// DOCEND executeAsync diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt index 5094a7bae9..aea846f207 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowIORequest.kt @@ -19,6 +19,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NonEmptySet import java.time.Instant +// DOCSTART FlowIORequest /** * A [FlowIORequest] represents an IO request of a flow when it suspends. It is persisted in checkpoints. */ @@ -103,3 +104,4 @@ sealed class FlowIORequest { // TODO: consider using an empty FlowAsyncOperation instead object ForceCheckpoint : FlowIORequest() } +// DOCSEND FlowIORequest diff --git a/core/src/test/kotlin/net/corda/core/identity/CordaX500NameTest.kt b/core/src/test/kotlin/net/corda/core/identity/CordaX500NameTest.kt index 362a3d1e98..7ee561fa10 100644 --- a/core/src/test/kotlin/net/corda/core/identity/CordaX500NameTest.kt +++ b/core/src/test/kotlin/net/corda/core/identity/CordaX500NameTest.kt @@ -15,6 +15,8 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull +import java.lang.Character.MIN_VALUE as NULLCHAR + class CordaX500NameTest { @Test fun `service name with organisational unit`() { @@ -70,17 +72,128 @@ class CordaX500NameTest { } } - @Test - fun `rejects name with wrong organisation name format`() { - assertFailsWith(IllegalArgumentException::class) { - CordaX500Name.parse("O=B, L=New York, C=US, OU=Org Unit, CN=Service Name") - } - } - @Test fun `rejects name with unsupported attribute`() { assertFailsWith(IllegalArgumentException::class) { CordaX500Name.parse("O=Bank A, L=New York, C=US, SN=blah") } } + + @Test + fun `rejects organisation (but not other attributes) with non-latin letters`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=Bཛྷa, L=New York, C=DE, OU=Org Unit, CN=Service Name") + } + // doesn't throw + validateLocalityAndOrganisationalUnitAndCommonName("Bཛྷa") + } + + @Test + fun `organisation (but not other attributes) must have at least two letters`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=B, L=New York, C=DE, OU=Org Unit, CN=Service Name") + } + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=, L=New York, C=DE, OU=Org Unit, CN=Service Name") + } + // doesn't throw + validateLocalityAndOrganisationalUnitAndCommonName("B") + validateLocalityAndOrganisationalUnitAndCommonName("") + } + + @Test + fun `accepts attributes starting with lower case letter`() { + CordaX500Name.parse("O=bank A, L=New York, C=DE, OU=Org Unit, CN=Service Name") + validateLocalityAndOrganisationalUnitAndCommonName("bank") + } + + @Test + fun `accepts attributes starting with numeric character`() { + CordaX500Name.parse("O=8Bank A, L=New York, C=DE, OU=Org Unit, CN=Service Name") + validateLocalityAndOrganisationalUnitAndCommonName("8bank") + } + + @Test + fun `accepts attributes with leading whitespace`() { + CordaX500Name.parse("O= VALID, L=VALID, C=DE, OU=VALID, CN=VALID") + validateLocalityAndOrganisationalUnitAndCommonName(" VALID") + } + + @Test + fun `accepts attributes with trailing whitespace`() { + CordaX500Name.parse("O=VALID , L=VALID, C=DE, OU=VALID, CN=VALID") + validateLocalityAndOrganisationalUnitAndCommonName("VALID ") + } + + @Test + fun `rejects attributes with comma`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=IN,VALID, L=VALID, C=DE, OU=VALID, CN=VALID") + } + checkLocalityAndOrganisationalUnitAndCommonNameReject("IN,VALID") + } + + @Test + fun `accepts org with equals sign`() { + CordaX500Name.parse("O=IN=VALID, L=VALID, C=DE, OU=VALID, CN=VALID") + } + + @Test + fun `accepts organisation with dollar sign`() { + CordaX500Name.parse("O=VA\$LID, L=VALID, C=DE, OU=VALID, CN=VALID") + validateLocalityAndOrganisationalUnitAndCommonName("VA\$LID") + } + @Test + fun `rejects attributes with double quotation mark`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=IN\"VALID, L=VALID, C=DE, OU=VALID, CN=VALID") + } + checkLocalityAndOrganisationalUnitAndCommonNameReject("IN\"VALID") + } + + @Test + fun `accepts organisation with single quotation mark`() { + CordaX500Name.parse("O=VA'LID, L=VALID, C=DE, OU=VALID, CN=VALID") + validateLocalityAndOrganisationalUnitAndCommonName("VA'LID") + } + @Test + fun `rejects organisation with backslash`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=IN\\VALID, L=VALID, C=DE, OU=VALID, CN=VALID") + } + checkLocalityAndOrganisationalUnitAndCommonNameReject("IN\\VALID") + } + + @Test + fun `rejects double spacing only in the organisation attribute`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=IN VALID , L=VALID, C=DE, OU=VALID, CN=VALID") + } + validateLocalityAndOrganisationalUnitAndCommonName("VA LID") + } + @Test + fun `rejects organisation (but not other attributes) containing the null character`() { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=IN${NULLCHAR}VALID , L=VALID, C=DE, OU=VALID, CN=VALID") + } + validateLocalityAndOrganisationalUnitAndCommonName("VA${NULLCHAR}LID") + } + + fun checkLocalityAndOrganisationalUnitAndCommonNameReject(invalid: String) { + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=VALID, L=${invalid}, C=DE, OU=VALID, CN=VALID") + } + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=VALID, L=VALID, C=DE, OU=${invalid}, CN=VALID") + } + assertFailsWith(IllegalArgumentException::class) { + CordaX500Name.parse("O=VALID, L=VALID, C=DE, OU=VALID, CN=${invalid}") + } + } + + fun validateLocalityAndOrganisationalUnitAndCommonName(valid: String) { + CordaX500Name.parse("O=VALID, L=${valid}, C=DE, OU=VALID, CN=VALID") + CordaX500Name.parse("O=VALID, L=VALID, C=DE, OU=${valid}, CN=VALID") + CordaX500Name.parse("O=VALID, L=VALID, C=DE, OU=VALID, CN=${valid}") + } } diff --git a/docs/source/contributing-flow-state-machines.rst b/docs/source/contributing-flow-state-machines.rst new file mode 100644 index 0000000000..7f5a9a37c8 --- /dev/null +++ b/docs/source/contributing-flow-state-machines.rst @@ -0,0 +1,257 @@ +.. highlight:: kotlin +.. raw:: html + + + + +How to extend the state machine +=============================== + +This article explains how to extend the state machine code that underlies flow execution. It is intended for Corda +contributors. + +How to add suspending operations +-------------------------------- + +To add a suspending operation for a simple request-response type function that perhaps involves some external IO we can +use the internal ``FlowAsyncOperation`` interface. + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt + :language: kotlin + :start-after: DOCSTART FlowAsyncOperation + :end-before: DOCEND FlowAsyncOperation + +Let's imagine we want to add a suspending operation that takes two integers and returns their sum. To do this we +implement ``FlowAsyncOperation``: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt + :language: kotlin + :start-after: DOCSTART SummingOperation + :end-before: DOCEND SummingOperation + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java + :language: java + :start-after: DOCSTART SummingOperation + :end-before: DOCEND SummingOperation + +As we can see the constructor of ``SummingOperation`` takes the two numbers, and the ``execute`` function simply returns +a future that is immediately completed by the result of summing the numbers. Note how we don't use ``@Suspendable`` on +``execute``, this is because we'll never suspend inside this function, the suspension will happen before we're calling +it. + +Note also how the input numbers are stored in the class as fields. This is important, because in the flow's checkpoint +we'll store an instance of this class whenever we're suspending on such an operation. If the node fails or restarts +while the operation is underway this class will be deserialized from the checkpoint and ``execute`` will be called +again. + +Now we can use the internal function ``executeAsync`` to execute this operation from a flow. + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt + :language: kotlin + :start-after: DOCSTART executeAsync + :end-before: DOCEND executeAsync + +It simply takes a ``FlowAsyncOperation`` and an optional flag we don't care about for now. We can use this function in a +flow: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt + :language: kotlin + :start-after: DOCSTART ExampleSummingFlow + :end-before: DOCEND ExampleSummingFlow + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/ExampleSummingFlow.java + :language: java + :start-after: DOCSTART ExampleSummingFlow + :end-before: DOCEND ExampleSummingFlow + +That's it! Obviously this is a mostly useless example, but this is the basic code structure one could extend for heavier +computations/other IO. For example the function could call into a ``CordaService`` or something similar. One thing to +note is that the operation executed in ``execute`` must be redoable(= "idempotent") in case the node fails before the +next checkpoint is committed. + +How to test +----------- + +The recommended way to test flows and the state machine is using the Driver DSL. This ensures that you will test your +flow with a full node. + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt + :language: kotlin + :start-after: DOCSTART summingWorks + :end-before: DOCEND summingWorks + + .. literalinclude:: ../../docs/source/example-code/src/integration-test/java/net/corda/docs/java/TutorialFlowAsyncOperationTest.java + :language: java + :start-after: DOCSTART summingWorks + :end-before: DOCEND summingWorks + +The above will spin up a node and run our example flow. + +How to debug issues +------------------- + +Let's assume we made a mistake in our summing operation: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt + :language: kotlin + :start-after: DOCSTART SummingOperationThrowing + :end-before: DOCEND SummingOperationThrowing + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java + :language: java + :start-after: DOCSTART SummingOperationThrowing + :end-before: DOCEND SummingOperationThrowing + +The operation now throws a rude exception. If we modify the example flow to use this and run the same test we will get +a lot of logs about the error condition (as we are in dev mode). The interesting bit looks like this: + +.. parsed-literal:: + [WARN ] 18:38:52,613 [Node thread-1] (DumpHistoryOnErrorInterceptor.kt:39) interceptors.DumpHistoryOnErrorInterceptor.executeTransition - Flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] errored, dumping all transitions: + + --- Transition of flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] --- + Timestamp: 2018-06-01T17:38:52.426Z + Event: DoRemainingWork + Actions: + CreateTransaction + PersistCheckpoint(id=[03ab886e-3fd3-4667-b944-ab6a3b1f90a7], checkpoint=Checkpoint(invocationContext=InvocationContext(origin=RPC(actor=Actor(id=Id(value=aliceUser), serviceId=AuthServiceId(value=NODE_CONFIG), owningLegalIdentity=O=Alice Corp, L=Madrid, C=ES)), trace=Trace(invocationId=26bcf0c3-f1d8-4098-a52d-3780f4095b7a, timestamp: 2018-06-01T17:38:52.234Z, entityType: Invocation, sessionId=393d1175-3bb1-4eb1-bff0-6ba317851260, timestamp: 2018-06-01T17:38:52.169Z, entityType: Session), actor=Actor(id=Id(value=aliceUser), serviceId=AuthServiceId(value=NODE_CONFIG), owningLegalIdentity=O=Alice Corp, L=Madrid, C=ES), externalTrace=null, impersonatedActor=null), ourIdentity=O=Alice Corp, L=Madrid, C=ES, sessions={}, subFlowStack=[Inlined(flowClass=class net.corda.docs.tutorial.flowstatemachines.ExampleSummingFlow, subFlowVersion=CorDappFlow(platformVersion=1, corDappName=net.corda.docs-c6816652-f975-4fb2-aa09-ef1dddea19b3, corDappHash=F4012397D8CF97926B5998E046DBCE16D497318BB87DCED66313912D4B303BB7))], flowState=Unstarted(flowStart=Explicit, frozenFlowLogic=74BA62EC5821EBD4FC4CBE129843F9ED6509DB37E6E3C8F85E3F7A8D84083500), errorState=Clean, numberOfSuspends=0, deduplicationSeed=03ab886e-3fd3-4667-b944-ab6a3b1f90a7)) + PersistDeduplicationFacts(deduplicationHandlers=[net.corda.node.internal.FlowStarterImpl$startFlow$startFlowEvent$1@69326343]) + CommitTransaction + AcknowledgeMessages(deduplicationHandlers=[net.corda.node.internal.FlowStarterImpl$startFlow$startFlowEvent$1@69326343]) + SignalFlowHasStarted(flowId=[03ab886e-3fd3-4667-b944-ab6a3b1f90a7]) + CreateTransaction + Continuation: Resume(result=null) + Diff between previous and next state: + isAnyCheckpointPersisted: + false + true + pendingDeduplicationHandlers: + [net.corda.node.internal.FlowStarterImpl$startFlow$startFlowEvent$1@69326343] + [] + isFlowResumed: + false + true + + + --- Transition of flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] --- + Timestamp: 2018-06-01T17:38:52.487Z + Event: Suspend(ioRequest=ExecuteAsyncOperation(operation=net.corda.docs.tutorial.flowstatemachines.SummingOperationThrowing@40f4c23d), maySkipCheckpoint=false, fiber=15EC69204562BB396846768169AD4A339569D97AE841D805C230C513A8BA5BDE, ) + Actions: + PersistCheckpoint(id=[03ab886e-3fd3-4667-b944-ab6a3b1f90a7], checkpoint=Checkpoint(invocationContext=InvocationContext(origin=RPC(actor=Actor(id=Id(value=aliceUser), serviceId=AuthServiceId(value=NODE_CONFIG), owningLegalIdentity=O=Alice Corp, L=Madrid, C=ES)), trace=Trace(invocationId=26bcf0c3-f1d8-4098-a52d-3780f4095b7a, timestamp: 2018-06-01T17:38:52.234Z, entityType: Invocation, sessionId=393d1175-3bb1-4eb1-bff0-6ba317851260, timestamp: 2018-06-01T17:38:52.169Z, entityType: Session), actor=Actor(id=Id(value=aliceUser), serviceId=AuthServiceId(value=NODE_CONFIG), owningLegalIdentity=O=Alice Corp, L=Madrid, C=ES), externalTrace=null, impersonatedActor=null), ourIdentity=O=Alice Corp, L=Madrid, C=ES, sessions={}, subFlowStack=[Inlined(flowClass=class net.corda.docs.tutorial.flowstatemachines.ExampleSummingFlow, subFlowVersion=CorDappFlow(platformVersion=1, corDappName=net.corda.docs-c6816652-f975-4fb2-aa09-ef1dddea19b3, corDappHash=F4012397D8CF97926B5998E046DBCE16D497318BB87DCED66313912D4B303BB7))], flowState=Started(flowIORequest=ExecuteAsyncOperation(operation=net.corda.docs.tutorial.flowstatemachines.SummingOperationThrowing@40f4c23d), frozenFiber=15EC69204562BB396846768169AD4A339569D97AE841D805C230C513A8BA5BDE), errorState=Clean, numberOfSuspends=1, deduplicationSeed=03ab886e-3fd3-4667-b944-ab6a3b1f90a7)) + PersistDeduplicationFacts(deduplicationHandlers=[]) + CommitTransaction + AcknowledgeMessages(deduplicationHandlers=[]) + ScheduleEvent(event=DoRemainingWork) + Continuation: ProcessEvents + Diff between previous and next state: + checkpoint.numberOfSuspends: + 0 + 1 + checkpoint.flowState: + Unstarted(flowStart=Explicit, frozenFlowLogic=74BA62EC5821EBD4FC4CBE129843F9ED6509DB37E6E3C8F85E3F7A8D84083500) + Started(flowIORequest=ExecuteAsyncOperation(operation=net.corda.docs.tutorial.flowstatemachines.SummingOperationThrowing@40f4c23d), frozenFiber=15EC69204562BB396846768169AD4A339569D97AE841D805C230C513A8BA5BDE) + isFlowResumed: + true + false + + + --- Transition of flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] --- + Timestamp: 2018-06-01T17:38:52.549Z + Event: DoRemainingWork + Actions: + ExecuteAsyncOperation(operation=net.corda.docs.tutorial.flowstatemachines.SummingOperationThrowing@40f4c23d) + Continuation: ProcessEvents + Diff between previous and intended state: + null + Diff between previous and next state: + checkpoint.errorState: + Clean + Errored(errors=[FlowError(errorId=-8704604242619505379, exception=java.lang.IllegalStateException: You shouldn't be calling me)], propagatedIndex=0, propagating=false) + + + --- Transition of flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] --- + Timestamp: 2018-06-01T17:38:52.555Z + Event: DoRemainingWork + Actions: + + Continuation: ProcessEvents + Diff between previous and next state: + null + + --- Transition of flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] --- + Timestamp: 2018-06-01T17:38:52.556Z + Event: StartErrorPropagation + Actions: + ScheduleEvent(event=DoRemainingWork) + Continuation: ProcessEvents + Diff between previous and next state: + checkpoint.errorState.propagating: + false + true + + + --- Transition of flow [03ab886e-3fd3-4667-b944-ab6a3b1f90a7] --- + Timestamp: 2018-06-01T17:38:52.606Z + Event: DoRemainingWork + Actions: + PropagateErrors(errorMessages=[ErrorSessionMessage(flowException=null, errorId=-8704604242619505379)], sessions=[], senderUUID=861f07d6-4b8f-42bd-9b52-5152812db2ba) + CreateTransaction + RemoveCheckpoint(id=[03ab886e-3fd3-4667-b944-ab6a3b1f90a7]) + PersistDeduplicationFacts(deduplicationHandlers=[]) + ReleaseSoftLocks(uuid=03ab886e-3fd3-4667-b944-ab6a3b1f90a7) + CommitTransaction + AcknowledgeMessages(deduplicationHandlers=[]) + RemoveSessionBindings(sessionIds=[]) + RemoveFlow(flowId=[03ab886e-3fd3-4667-b944-ab6a3b1f90a7], removalReason=ErrorFinish(flowErrors=[FlowError(errorId=-8704604242619505379, exception=java.lang.IllegalStateException: You shouldn't be calling me)]), lastState=StateMachineState(checkpoint=Checkpoint(invocationContext=InvocationContext(origin=RPC(actor=Actor(id=Id(value=aliceUser), serviceId=AuthServiceId(value=NODE_CONFIG), owningLegalIdentity=O=Alice Corp, L=Madrid, C=ES)), trace=Trace(invocationId=26bcf0c3-f1d8-4098-a52d-3780f4095b7a, timestamp: 2018-06-01T17:38:52.234Z, entityType: Invocation, sessionId=393d1175-3bb1-4eb1-bff0-6ba317851260, timestamp: 2018-06-01T17:38:52.169Z, entityType: Session), actor=Actor(id=Id(value=aliceUser), serviceId=AuthServiceId(value=NODE_CONFIG), owningLegalIdentity=O=Alice Corp, L=Madrid, C=ES), externalTrace=null, impersonatedActor=null), ourIdentity=O=Alice Corp, L=Madrid, C=ES, sessions={}, subFlowStack=[Inlined(flowClass=class net.corda.docs.tutorial.flowstatemachines.ExampleSummingFlow, subFlowVersion=CorDappFlow(platformVersion=1, corDappName=net.corda.docs-c6816652-f975-4fb2-aa09-ef1dddea19b3, corDappHash=F4012397D8CF97926B5998E046DBCE16D497318BB87DCED66313912D4B303BB7))], flowState=Started(flowIORequest=ExecuteAsyncOperation(operation=net.corda.docs.tutorial.flowstatemachines.SummingOperationThrowing@40f4c23d), frozenFiber=15EC69204562BB396846768169AD4A339569D97AE841D805C230C513A8BA5BDE), errorState=Errored(errors=[FlowError(errorId=-8704604242619505379, exception=java.lang.IllegalStateException: You shouldn't be calling me)], propagatedIndex=1, propagating=true), numberOfSuspends=1, deduplicationSeed=03ab886e-3fd3-4667-b944-ab6a3b1f90a7), flowLogic=net.corda.docs.tutorial.flowstatemachines.ExampleSummingFlow@600b0c6c, pendingDeduplicationHandlers=[], isFlowResumed=false, isTransactionTracked=false, isAnyCheckpointPersisted=true, isStartIdempotent=false, isRemoved=true, senderUUID=861f07d6-4b8f-42bd-9b52-5152812db2ba)) + Continuation: Abort + Diff between previous and next state: + checkpoint.errorState.propagatedIndex: + 0 + 1 + isRemoved: + false + true + +Whoa that's a lot of stuff. Now we get a glimpse into the bowels of the flow state machine. As we can see the flow did +quite a few things, even though the flow code looks simple. + +What we can see here is the different transitions the flow's state machine went through that led up to the error +condition. For each transition we see what *Event* triggered the transition, what *Action* s were taken as a consequence, +and how the internal *State* of the state machine was modified in the process. It also prints the transition's +*Continuation*, which indicates how the flow should proceed after the transition. + +For example in the first transition we can see that the triggering event was a ``DoRemainingWork``, this is a generic +event that instructs the state machine to check its own state to see whether there's any work left to do, and does it if +there's any. + +In this case the work involves persisting a checkpoint together with some deduplication data in a database transaction, +then acknowledging any triggering messages, signalling that the flow has started, and creating a fresh database +transaction, to be used by user code. + +The continuation is a ``Resume``, which instructs the state machine to hand control to user code. The state change is +a simple update of bookkeeping data. + +In other words the first transition concerns the initialization of the flow, which includes the creation of the +checkpoint. + +The next transition is the suspension of our summing operation, triggered by the ``Suspend`` event. As we can see in +this transition we aren't doing any work related to the summation yet, we're merely persisting the checkpoint that +indicates that we want to do the summation. Had we added a ``toString`` method to our ``SummingOperationThrowing`` we +would see a nicer message. + +The next transition is the faulty one, as we can see it was also triggered by a ``DoRemainingWork``, and executed our +operation. We can see that there are two state "diff"s printed, one that would've happened had the transition succeeded, +and one that actually happened, which marked the flow's state as errored. The rest of the transitions involve error +propagation (triggered by the ``FlowHospital``) and notification of failure, which ultimately raises the exception on +the RPC ``resultFuture``. diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst index 777d57a443..e10db32a70 100644 --- a/docs/source/corda-networks-index.rst +++ b/docs/source/corda-networks-index.rst @@ -13,4 +13,5 @@ Networks azure-vm-explore aws-vm-explore gcp-vm + deploy-locally cipher-suites diff --git a/docs/source/deploy-locally.rst b/docs/source/deploy-locally.rst new file mode 100644 index 0000000000..9180c78247 --- /dev/null +++ b/docs/source/deploy-locally.rst @@ -0,0 +1,157 @@ +Setting up your local environment to allow the deployment of a Corda node +========================================================================= + +.. contents:: + +This document explains how to set up your local network to enable a +Corda node to connect to the Corda Testnet. This assumes you are +downloading a node ZIP from: https://testnet.corda.network. + + +Pre-requisites +-------------- +* Register for an account on https://testnet.corda.network. + + +Set up your local network +------------------------- + +For a Corda node to be able to connect to the Corda Testnet and be +reachable by counterparties on that network it needs to be reachable +on the open internet. Corda is a server which requires an externally +visible IP address and several ports in order to operate correctly. + +We recommend running your Coda node on cloud infrastructure. If you +wish to run Corda on your local machine then you will need to +configure your network to enable the Corda node to be reachable from +the internet. + +.. note:: You will need access to your network router/gateway to the internet. If you do not have direct access then contact your administrator. + +The following steps will describe how to use port forwarding on your +router to make sure packets intended for Corda are routed to the right +place on your local network. + +Set up static IP address local host machine +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The next steps will configure your router to forward +packets to the Corda node, but for this it is required to set the host +machine to have a static IP address. If this isn't done, and the +network is using DHCP dynamic address allocation then the next time +the host machine is rebooted it may be on a different IP and the port +forwarding will no longer work. + +Please consult your operating system documentation for instructions on +setting a static IP on the host machine. + + +Set up port forwarding on your router +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Port forwarding is a method of making a computer on your network +accessible to computers on the Internet, even though it is behind a router. + +.. note:: All routers are slightly different and you will need to consult the documentation for your specific make and model. + +Log in to the admin page of your router (often ``192.168.0.1``) in your +browser bar. + +.. note:: Router administration IP and log in credentials are usually on the bottom or side of your router. + +Navigate to the ``port forwarding`` section of the admin console. + +Add rules for the following ports which Corda uses: + +.. code:: bash + + 10002 + 10003 + 8080 + +.. note:: These ports are the defaults for Testnet which are specified + in the node.conf. If these conflict with existing services + on your host machine they can be changed in the + ``/opt/corda/node.conf`` file. + +For each rule you will also typically have to specify the rule name, +the static IP address of the host machine we configured earlier (the +same in each case) and the protocol (which is TCP in all cases here). + +Please consult your router documentation for specific details on +enabling port forwarding. + + +Open firewall ports +~~~~~~~~~~~~~~~~~~~ + +If you are operating a firewall on your host machine or local network +you will also need to open the above ports for incoming traffic. + +Please consult your firewall documentation for details. + + +Optional: Configure a static external IP address +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Corda expects nodes to have stable addresses over long periods of +time. ISPs typically assign dynamic IP addresses to a router and so if +your router is rebooted it may not obtain the same external IP and +therefore your Corda node will change its address on the Testnet. + +You can request a static IP address from your ISP however this may +incur a cost. + +If the IP address does change then this doesn't cause issues but it +will result in an update to the network map which then needs to be +propagated to all peers in the network. There may be some delay in the +ability to transact while this happens. + +.. warning:: Corda nodes are expected to be online all the time and + will send a heartbeat to the network map server to + indicate they are operational. If they go offline for a + period of time (~24 hours in the case of Testnet) then + the node will be removed from the network map. Any nodes + which have queued messages for your node will drop these messages, + they won't be delivered and unexpected behaviour may + occur. + + +Test if the ports are open +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use a port checking tool to make sure the ports are open +properly. + + +Download and install your node +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Navigate to https://testnet.corda.network/platform. + +Click on the ``Download`` button and wait for the ZIP +file to download: + +.. image:: resources/testnet-download.png + +.. note: This may take several seconds. + +Unzip the file in your Corda root directory: + +.. code:: bash + + mkdir corda + cd corda + cp /node.zip . + unzip node.zip + cd node + +Run the ``run-corda.sh`` script to start your Corda node. + +.. code:: bash + + ./run-corda.sh + +Congratulations! You now have a running Corda node on Testnet. + +.. warning:: It is possible to copy the ``node.zip`` file from your local machine to any other host machine and run the Corda node from there. Do not run multiple copies of the same node (i.e. with the same identity). If a new copy of the node appears on the network then the network map server will interpret this as a change in the address of the node and route traffic to the most recent instance. Any states which are on the old node will no longer be available and undefined behaviour may result. Please provision a new node from the application instead. diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 452732ea00..c1cf994177 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -34,6 +34,11 @@ sourceSets { runtimeClasspath += main.output + test.output srcDir file('src/integration-test/kotlin') } + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/java') + } resources { srcDir file('../../testing/test-utils/src/main/resources') } diff --git a/docs/source/example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java b/docs/source/example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java new file mode 100644 index 0000000000..912a996537 --- /dev/null +++ b/docs/source/example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java @@ -0,0 +1,124 @@ +package net.corda.docs; + +import net.corda.client.rpc.CordaRPCClient; +import net.corda.core.concurrent.CordaFuture; +import net.corda.core.contracts.Amount; +import net.corda.core.contracts.Issued; +import net.corda.core.contracts.Structures; +import net.corda.core.messaging.CordaRPCOps; +import net.corda.core.node.services.Vault; +import net.corda.core.utilities.OpaqueBytes; +import net.corda.finance.contracts.asset.Cash; +import net.corda.finance.flows.CashIssueAndPaymentFlow; +import net.corda.finance.flows.CashPaymentFlow; +import net.corda.testing.driver.DriverParameters; +import net.corda.testing.driver.NodeHandle; +import net.corda.testing.driver.NodeParameters; +import net.corda.testing.node.User; +import org.junit.Test; +import rx.Observable; + +import java.util.Currency; +import java.util.HashSet; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static net.corda.finance.Currencies.DOLLARS; +import static net.corda.node.services.Permissions.invokeRpc; +import static net.corda.node.services.Permissions.startFlow; +import static net.corda.testing.core.ExpectKt.expect; +import static net.corda.testing.core.ExpectKt.expectEvents; +import static net.corda.testing.core.TestConstants.ALICE_NAME; +import static net.corda.testing.core.TestConstants.BOB_NAME; +import static net.corda.testing.driver.Driver.driver; +import static org.junit.Assert.assertEquals; + +public class JavaIntegrationTestingTutorial { + @Test + public void aliceBobCashExchangeExample() { + // START 1 + driver(new DriverParameters() + .withStartNodesInProcess(true) + .withExtraCordappPackagesToScan(singletonList("net.corda.finance.contracts.asset")), dsl -> { + + User aliceUser = new User("aliceUser", "testPassword1", new HashSet<>(asList( + startFlow(CashIssueAndPaymentFlow.class), + invokeRpc("vaultTrack") + ))); + + User bobUser = new User("bobUser", "testPassword2", new HashSet<>(asList( + startFlow(CashPaymentFlow.class), + invokeRpc("vaultTrack") + ))); + + try { + List> nodeHandleFutures = asList( + dsl.startNode(new NodeParameters().withProvidedName(ALICE_NAME).withRpcUsers(singletonList(aliceUser))), + dsl.startNode(new NodeParameters().withProvidedName(BOB_NAME).withRpcUsers(singletonList(bobUser))) + ); + + NodeHandle alice = nodeHandleFutures.get(0).get(); + NodeHandle bob = nodeHandleFutures.get(1).get(); + // END 1 + + // START 2 + CordaRPCClient aliceClient = new CordaRPCClient(alice.getRpcAddress()); + CordaRPCOps aliceProxy = aliceClient.start("aliceUser", "testPassword1").getProxy(); + + CordaRPCClient bobClient = new CordaRPCClient(bob.getRpcAddress()); + CordaRPCOps bobProxy = bobClient.start("bobUser", "testPassword2").getProxy(); + // END 2 + + // START 3 + Observable> bobVaultUpdates = bobProxy.vaultTrack(Cash.State.class).getUpdates(); + Observable> aliceVaultUpdates = aliceProxy.vaultTrack(Cash.State.class).getUpdates(); + // END 3 + + // START 4 + OpaqueBytes issueRef = OpaqueBytes.of((byte)0); + aliceProxy.startFlowDynamic( + CashIssueAndPaymentFlow.class, + DOLLARS(1000), + issueRef, + bob.getNodeInfo().getLegalIdentities().get(0), + true, + dsl.getDefaultNotaryIdentity() + ).getReturnValue().get(); + + @SuppressWarnings("unchecked") + Class> cashVaultUpdateClass = (Class>)(Class)Vault.Update.class; + + expectEvents(bobVaultUpdates, true, () -> + expect(cashVaultUpdateClass, update -> true, update -> { + System.out.println("Bob got vault update of " + update); + Amount> amount = update.getProduced().iterator().next().getState().getData().getAmount(); + assertEquals(DOLLARS(1000), Structures.withoutIssuer(amount)); + return null; + }) + ); + // END 4 + + // START 5 + bobProxy.startFlowDynamic( + CashPaymentFlow.class, + DOLLARS(1000), + alice.getNodeInfo().getLegalIdentities().get(0) + ).getReturnValue().get(); + + expectEvents(aliceVaultUpdates, true, () -> + expect(cashVaultUpdateClass, update -> true, update -> { + System.out.println("Alice got vault update of " + update); + Amount> amount = update.getProduced().iterator().next().getState().getData().getAmount(); + assertEquals(DOLLARS(1000), Structures.withoutIssuer(amount)); + return null; + }) + ); + // END 5 + } catch (Exception e) { + throw new RuntimeException("Exception thrown in driver DSL", e); + } + return null; + }); + } +} diff --git a/docs/source/example-code/src/integration-test/java/net/corda/docs/java/TutorialFlowAsyncOperationTest.java b/docs/source/example-code/src/integration-test/java/net/corda/docs/java/TutorialFlowAsyncOperationTest.java new file mode 100644 index 0000000000..410f2fcf73 --- /dev/null +++ b/docs/source/example-code/src/integration-test/java/net/corda/docs/java/TutorialFlowAsyncOperationTest.java @@ -0,0 +1,42 @@ +package net.corda.docs.java; + +import kotlin.Unit; +import net.corda.client.rpc.CordaRPCClient; +import net.corda.core.messaging.CordaRPCOps; +import net.corda.core.utilities.KotlinUtilsKt; +import net.corda.docs.java.tutorial.flowstatemachines.ExampleSummingFlow; +import net.corda.node.services.Permissions; +import net.corda.testing.driver.*; +import net.corda.testing.node.User; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashSet; +import java.util.concurrent.Future; + +import static net.corda.testing.core.TestConstants.ALICE_NAME; +import static org.junit.Assert.assertEquals; + +public final class TutorialFlowAsyncOperationTest { + // DOCSTART summingWorks + @Test + public final void summingWorks() { + Driver.driver(new DriverParameters(), (DriverDSL dsl) -> { + User aliceUser = new User("aliceUser", "testPassword1", + new HashSet<>(Collections.singletonList(Permissions.all())) + ); + Future aliceFuture = dsl.startNode(new NodeParameters() + .withProvidedName(ALICE_NAME) + .withRpcUsers(Collections.singletonList(aliceUser)) + ); + NodeHandle alice = KotlinUtilsKt.getOrThrow(aliceFuture, null); + CordaRPCClient aliceClient = new CordaRPCClient(alice.getRpcAddress()); + CordaRPCOps aliceProxy = aliceClient.start("aliceUser", "testPassword1").getProxy(); + Future answerFuture = aliceProxy.startFlowDynamic(ExampleSummingFlow.class).getReturnValue(); + int answer = KotlinUtilsKt.getOrThrow(answerFuture, null); + assertEquals(3, answer); + return Unit.INSTANCE; + }); + } + // DOCEND summingWorks +} diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt new file mode 100644 index 0000000000..ee017c77a7 --- /dev/null +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt @@ -0,0 +1,97 @@ +package net.corda.docs + +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.withoutIssuer +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow +import net.corda.core.messaging.vaultTrackBy +import net.corda.core.node.services.Vault +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.services.Permissions.Companion.invokeRpc +import net.corda.node.services.Permissions.Companion.startFlow +import net.corda.testing.core.* +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.junit.Test +import rx.Observable +import java.util.* +import kotlin.test.assertEquals + +class KotlinIntegrationTestingTutorial { + @Test + fun `alice bob cash exchange example`() { + // START 1 + driver(DriverParameters( + startNodesInProcess = true, + extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset") + )) { + val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( + startFlow(), + invokeRpc("vaultTrackBy") + )) + + val bobUser = User("bobUser", "testPassword2", permissions = setOf( + startFlow(), + invokeRpc("vaultTrackBy") + )) + + val (alice, bob) = listOf( + startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)), + startNode(providedName = BOB_NAME, rpcUsers = listOf(bobUser)) + ).map { it.getOrThrow() } + // END 1 + + // START 2 + val aliceClient = CordaRPCClient(alice.rpcAddress) + val aliceProxy: CordaRPCOps = aliceClient.start("aliceUser", "testPassword1").proxy + + val bobClient = CordaRPCClient(bob.rpcAddress) + val bobProxy: CordaRPCOps = bobClient.start("bobUser", "testPassword2").proxy + // END 2 + + // START 3 + val bobVaultUpdates: Observable> = bobProxy.vaultTrackBy().updates + val aliceVaultUpdates: Observable> = aliceProxy.vaultTrackBy().updates + // END 3 + + // START 4 + val issueRef = OpaqueBytes.of(0) + aliceProxy.startFlow(::CashIssueAndPaymentFlow, + 1000.DOLLARS, + issueRef, + bob.nodeInfo.singleIdentity(), + true, + defaultNotaryIdentity + ).returnValue.getOrThrow() + + bobVaultUpdates.expectEvents { + expect { update -> + println("Bob got vault update of $update") + val amount: Amount> = update.produced.first().state.data.amount + assertEquals(1000.DOLLARS, amount.withoutIssuer()) + } + } + // END 4 + + // START 5 + bobProxy.startFlow(::CashPaymentFlow, 1000.DOLLARS, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow() + + aliceVaultUpdates.expectEvents { + expect { update -> + println("Alice got vault update of $update") + val amount: Amount> = update.produced.first().state.data.amount + assertEquals(1000.DOLLARS, amount.withoutIssuer()) + } + } + // END 5 + } + } +} diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt new file mode 100644 index 0000000000..fb3717f109 --- /dev/null +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt @@ -0,0 +1,29 @@ +package net.corda.docs + +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.docs.tutorial.flowstatemachines.ExampleSummingFlow +import net.corda.node.services.Permissions +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.User +import org.junit.Test +import kotlin.test.assertEquals + +class TutorialFlowAsyncOperationTest { + // DOCSTART summingWorks + @Test + fun summingWorks() { + driver(DriverParameters(startNodesInProcess = true)) { + val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(Permissions.all())) + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)).getOrThrow() + val aliceClient = CordaRPCClient(alice.rpcAddress) + val aliceProxy = aliceClient.start("aliceUser", "testPassword1").proxy + val answer = aliceProxy.startFlow(::ExampleSummingFlow).returnValue.getOrThrow() + assertEquals(3, answer) + } + } + // DOCEND summingWorks +} diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/ExampleSummingFlow.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/ExampleSummingFlow.java new file mode 100644 index 0000000000..c8e93d7025 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/ExampleSummingFlow.java @@ -0,0 +1,19 @@ +package net.corda.docs.java.tutorial.flowstatemachines; + +import co.paralleluniverse.fibers.Suspendable; +import net.corda.core.flows.FlowLogic; +import net.corda.core.flows.StartableByRPC; +import net.corda.core.internal.FlowAsyncOperationKt; +import org.jetbrains.annotations.NotNull; + +// DOCSTART ExampleSummingFlow +@StartableByRPC +public final class ExampleSummingFlow extends FlowLogic { + @Suspendable + @NotNull + @Override + public Integer call() { + return FlowAsyncOperationKt.executeAsync(this, new SummingOperation(1, 2), false); + } +} +// DOCEND ExampleSummingFlow diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java new file mode 100644 index 0000000000..d313fdb8ce --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java @@ -0,0 +1,32 @@ +package net.corda.docs.java.tutorial.flowstatemachines; + +import net.corda.core.concurrent.CordaFuture; +import net.corda.core.internal.FlowAsyncOperation; +import net.corda.core.internal.concurrent.CordaFutureImplKt; +import org.jetbrains.annotations.NotNull; + +// DOCSTART SummingOperation +public final class SummingOperation implements FlowAsyncOperation { + private final int a; + private final int b; + + @NotNull + @Override + public CordaFuture execute() { + return CordaFutureImplKt.doneFuture(this.a + this.b); + } + + public final int getA() { + return this.a; + } + + public final int getB() { + return this.b; + } + + public SummingOperation(int a, int b) { + this.a = a; + this.b = b; + } +} +// DOCEND SummingOperation diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java new file mode 100644 index 0000000000..1a759074b0 --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java @@ -0,0 +1,31 @@ +package net.corda.docs.java.tutorial.flowstatemachines; + +import net.corda.core.concurrent.CordaFuture; +import net.corda.core.internal.FlowAsyncOperation; +import org.jetbrains.annotations.NotNull; + +// DOCSTART SummingOperationThrowing +public final class SummingOperationThrowing implements FlowAsyncOperation { + private final int a; + private final int b; + + @NotNull + @Override + public CordaFuture execute() { + throw new IllegalStateException("You shouldn't be calling me"); + } + + public final int getA() { + return this.a; + } + + public final int getB() { + return this.b; + } + + public SummingOperationThrowing(int a, int b) { + this.a = a; + this.b = b; + } +} +// DOCEND SummingOperationThrowing diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt new file mode 100644 index 0000000000..8619361485 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt @@ -0,0 +1,38 @@ +package net.corda.docs.tutorial.flowstatemachines + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.concurrent.CordaFuture +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.FlowAsyncOperation +import net.corda.core.internal.concurrent.doneFuture +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.executeAsync + +// DOCSTART SummingOperation +class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation { + override fun execute(): CordaFuture { + return doneFuture(a + b) + } +} +// DOCEND SummingOperation + +// DOCSTART SummingOperationThrowing +class SummingOperationThrowing(val a: Int, val b: Int) : FlowAsyncOperation { + override fun execute(): CordaFuture { + throw IllegalStateException("You shouldn't be calling me") + } +} +// DOCEND SummingOperationThrowing + +// DOCSTART ExampleSummingFlow +@StartableByRPC +class ExampleSummingFlow : FlowLogic() { + @Suspendable + override fun call(): Int { + val answer = executeAsync(SummingOperation(1, 2)) + return answer // hopefully 3 + } +} +// DOCEND ExampleSummingFlow + diff --git a/docs/source/key-concepts-transactions.rst b/docs/source/key-concepts-transactions.rst index 6521ef2898..9f3cb8bfef 100644 --- a/docs/source/key-concepts-transactions.rst +++ b/docs/source/key-concepts-transactions.rst @@ -115,9 +115,11 @@ As well as input states and output states, transactions contain: * Commands * Attachments * Time-Window +* Notary -For example, a transaction where Alice pays off £5 of an IOU with Bob using a £5 cash payment, supported by two -attachments and a time-window, may look as follows: +For example, suppose we have a transaction where Alice uses a £5 cash payment to pay off £5 of an IOU with Bob. +This transaction has two supporting attachments and will only be notarised by NotaryClusterA if the notary pool +receives it within the specified time-window. This transaction would look as follows: .. image:: resources/full-tx.png :scale: 25% @@ -184,3 +186,13 @@ In some cases, we want a transaction proposed to only be approved during a certa In such cases, we can add a *time-window* to the transaction. Time-windows specify the time window during which the transaction can be committed. We discuss time-windows in the section on :doc:`key-concepts-time-windows`. + +Notary +^^^^^^ +A notary pool is a network service that provides uniqueness consensus by attesting that, for a given transaction, +it has not already signed other transactions that consume any of the proposed transaction’s input states. +The notary pool provides the point of finality in the system. + +Note that if the notary entity is absent then the transaction is not notarised at all. This is intended for +issuance/genesis transactions that don't consume any other states and thus can't double spend anything. +For more information on the notary services, see :doc:`key-concepts-notaries`. \ No newline at end of file diff --git a/docs/source/messaging.rst b/docs/source/messaging.rst index ebba7c3013..4fe8cc39a5 100644 --- a/docs/source/messaging.rst +++ b/docs/source/messaging.rst @@ -31,10 +31,10 @@ regular time interval for network map and applies any related changes locally. Nodes do not automatically deregister themselves, so (for example) nodes going offline briefly for maintenance are retained in the network map, and messages for them will be queued, minimising disruption. -Additionally, on every restart and on daily basis nodes submit signed `NodeInfo`s to the map service. When network map gets -signed, these changes are distributed as new network data. `NodeInfo` republishing is treated as a heartbeat from the node, +Additionally, on every restart and on daily basis nodes submit signed ``NodeInfo`` s to the map service. When network map gets +signed, these changes are distributed as new network data. ``NodeInfo`` republishing is treated as a heartbeat from the node, based on that network map service is able to figure out which nodes can be considered as stale and removed from the network -map document after `eventHorizon` time. +map document after ``eventHorizon`` time. Message queues -------------- @@ -61,10 +61,10 @@ for maintenance and other minor purposes. corresponding bridge is used to forward the message to an advertising peer's p2p queue. Once a peer is picked the session continues on as normal. -:``rpc.requests``: +:``rpc.server``: RPC clients send their requests here, and it's only open for sending by clients authenticated as RPC users. -:``clients.$user.rpc.$random``: +:``rpc.client.$user.$random``: RPC clients are given permission to create a temporary queue incorporating their username (``$user``) and sole permission to receive messages from it. RPC requests are required to include a random number (``$random``) from which the node is able to construct the queue the user is listening on and send the response to that. This mechanism @@ -80,7 +80,7 @@ Clients attempting to connect to the node's broker fall in one of four groups: they are given full access to all valid queues, otherwise they are rejected. #. Anyone connecting with the username ``SystemUsers/Peer`` is treated as a peer on the same Corda network as the node. Their - TLS root CA must be the same as the node's root CA - the root CA is the doorman of the network and having the same root CA + TLS root CA must be the same as the node's root CA -- the root CA is the doorman of the network and having the same root CA implies we've been let in by the same doorman. If they are part of the same network then they are only given permission to send to our ``p2p.inbound.$identity`` queue, otherwise they are rejected. @@ -98,20 +98,19 @@ this to determine what permissions the user has. The broker also does host verification when connecting to another peer. It checks that the TLS certificate subject matches with the advertised X.500 legal name from the network map service. - Implementation details ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ The components of the system that need to communicate and authenticate each other are: - - The Artemis P2P broker (Currently runs inside the Nodes JVM process, but in the future it will be able to run as a separate server) - * opens Acceptor configured with the doorman's certificate in the truststore and the node's ssl certificate in the keystore - - The Artemis RPC broker (Currently runs inside the Nodes JVM process, but in the future it will be able to run as a separate server) - * opens "Admin" Acceptor configured with the doorman's certificate in the truststore and the node's ssl certificate in the keystore - * opens "Client" Acceptor with the ssl settings configurable. This acceptor does not require ssl client-auth. - - The current node hosting the brokers - * connects to the P2P broker using the ``SystemUsers/Node`` user and the node's keystore and trustore - * connects to the "Admin" Acceptor of the RPC broker using the ``SystemUsers/NodeRPC`` user and the node's keystore and trustore - - RPC clients ( Third party applications that need to communicate with the Node. ) - * connect to the "Client" Acceptor of the RPC broker using the username/password provided by the node's admin. The client verifies the node's certificate using a truststore provided by the node's admin. - - Peer nodes (Other nodes on the network) - * connect to the P2P broker using the ``SystemUsers/Peer`` user and a doorman signed certificate. The authentication is performed based on the root CA. + - The Artemis P2P broker (currently runs inside the node's JVM process, but in the future it will be able to run as a separate server): + * Opens Acceptor configured with the doorman's certificate in the trustStore and the node's SSL certificate in the keyStore. + - The Artemis RPC broker (currently runs inside the node's JVM process, but in the future it will be able to run as a separate server): + * Opens "Admin" Acceptor configured with the doorman's certificate in the trustStore and the node's SSL certificate in the keyStore. + * Opens "Client" Acceptor with the SSL settings configurable. This acceptor does not require SSL client-auth. + - The current node hosting the brokers: + * Connects to the P2P broker using the ``SystemUsers/Node`` user and the node's keyStore and trustStore. + * Connects to the "Admin" Acceptor of the RPC broker using the ``SystemUsers/NodeRPC`` user and the node's keyStore and trustStore. + - RPC clients (third party applications that need to communicate with the node): + * Connect to the "Client" Acceptor of the RPC broker using the username/password provided by the node's admin. The client verifies the node's certificate using a trustStore provided by the node's admin. + - Peer nodes (other nodes on the network): + * Connect to the P2P broker using the ``SystemUsers/Peer`` user and a doorman signed certificate. The authentication is performed based on the root CA. \ No newline at end of file diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 0931a5f28f..bded7edfa7 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -14,27 +14,15 @@ The node services represent the various sub functions of the Corda node. Some are directly accessible to contracts and flows through the ``ServiceHub``, whilst others are the framework internals used to host the node functions. Any public service interfaces are defined in the -``:core`` gradle project in the -``src/main/kotlin/net/corda/core/node/services`` folder. The -``ServiceHub`` interface exposes functionality suitable for flows. -The implementation code for all standard services lives in the gradle -``:node`` project under the ``src/main/kotlin/net/corda/node/services`` -folder. The ``src/main/kotlin/net/corda/node/services/api`` folder -contains declarations for internal only services and for interoperation -between services. +``net.corda.core.node.services`` package. The ``ServiceHub`` interface exposes +functionality suitable for flows. +The implementation code for all standard services lives in the ``net.corda.node.services`` package. All the services are constructed in the ``AbstractNode`` ``start`` -method (and the extension in ``Node``). They may also register a -shutdown handler during initialisation, which will be called in reverse -order to the start registration sequence when the ``Node.stop`` -is called. +method. They may also register a shutdown handler during initialisation, +which will be called in reverse order to the start registration sequence when the ``Node.stop`` is called. -For unit testing a number of non-persistent, memory only services are -defined in the ``:node`` and ``:test-utils`` projects. The -``:test-utils`` project also provides an in-memory networking simulation -to allow unit testing of flows and service functions. - -The roles of the individual services are described below. +The roles of the individual services are described below. Key management and identity services ------------------------------------ @@ -43,15 +31,15 @@ InMemoryIdentityService ~~~~~~~~~~~~~~~~~~~~~~~ The ``InMemoryIdentityService`` implements the ``IdentityService`` -interface and provides a store of remote mappings between ``CompositeKey`` +interface and provides a store of remote mappings between ``PublicKey`` and remote ``Parties``. It is automatically populated from the -``NetworkMapCache`` updates and is used when translating ``CompositeKey`` +``NetworkMapCache`` updates and is used when translating ``PublicKey`` exposed in transactions into fully populated ``Party`` identities. This service is also used in the default JSON mapping of parties in the web server, thus allowing the party names to be used to refer to other nodes' legal identities. In the future the Identity service will be made persistent and extended to allow anonymised session keys to be used in -flows where the well-known ``CompositeKey`` of nodes need to be hidden +flows where the well-known ``PublicKey`` of nodes need to be hidden to non-involved parties. PersistentKeyManagementService and E2ETestKeyManagementService @@ -95,7 +83,7 @@ of this component, because the ``ArtemisMessagingServer`` is responsible for configuring the network ports (based upon settings in ``node.conf``) and the service configures the security settings of the ``ArtemisMQ`` middleware and acts to form bridges between node mailbox queues based -upon connection details advertised by the ``NetworkMapService``. The +upon connection details advertised by the ``NetworkMapCache``. The ``ArtemisMQ`` broker is configured to use TLS1.2 with a custom ``TrustStore`` containing a Corda root certificate and a ``KeyStore`` with a certificate and key signed by a chain back to this root @@ -105,13 +93,13 @@ each other it is essential that the entire set of nodes are able to authenticate against each other and thus typically that they share a common root certificate. Also note that the address configuration defined for the server is the basis for the address advertised in the -NetworkMapService and thus must be externally connectable by all nodes +``NetworkMapCache`` and thus must be externally connectable by all nodes in the network. -NodeMessagingClient -~~~~~~~~~~~~~~~~~~~ +P2PMessagingClient +~~~~~~~~~~~~~~~~~~ -The ``NodeMessagingClient`` is the implementation of the +The ``P2PMessagingClient`` is the implementation of the ``MessagingService`` interface operating across the ``ArtemisMQ`` middleware layer. It typically connects to the local ``ArtemisMQ`` hosted within the ``ArtemisMessagingServer`` service. However, the @@ -133,7 +121,7 @@ services of authorised nodes provided by the remote specific advertised services e.g. a Notary service, or an Oracle service. Also, this service allows mapping of friendly names, or ``Party`` identities to the full ``NodeInfo`` which is used in the -``StateMachineManager`` to convert between the ``CompositeKey``, or +``StateMachineManager`` to convert between the ``PublicKey``, or ``Party`` based addressing used in the flows/contracts and the physical host and port information required for the physical ``ArtemisMQ`` messaging layer. @@ -141,13 +129,6 @@ physical host and port information required for the physical Storage and persistence related services ---------------------------------------- -StorageServiceImpl -~~~~~~~~~~~~~~~~~~ - -The ``StorageServiceImpl`` service simply hold references to the various -persistence related services and provides a single grouped interface on -the ``ServiceHub``. - DBCheckpointStorage ~~~~~~~~~~~~~~~~~~~ @@ -247,40 +228,7 @@ a reference to the state that triggered the event. The flow can then begin whatever action is required. Note that the scheduled activity occurs in all nodes holding the state in their Vault, it may therefore be required for the flow to exit early if the current node is not -the intended initiator. - -Notary flow implementation services ------------------------------------ - -PersistentUniquenessProvider, InMemoryUniquenessProvider and RaftUniquenessProvider -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -These variants of ``UniquenessProvider`` service are used by the notary -flows to track consumed states and thus reject double-spend -scenarios. The ``InMemoryUniquenessProvider`` is for unit testing only, -the default being the ``PersistentUniquenessProvider`` which records the -changes to the DB. When the Raft based notary is active the states are -tracked by the whole cluster using a ``RaftUniquenessProvider``. Outside -of the notary flows themselves this service should not be accessed -by any CorDapp components. - -NotaryService (SimpleNotaryService, ValidatingNotaryService, RaftValidatingNotaryService) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``NotaryService`` is an abstract base class for the various concrete -implementations of the Notary server flow. By default, a node does -not run any ``NotaryService`` server component. For that you need to specify the ``notary`` config. -The node may then participate in controlling state uniqueness when contacted by nodes -using the ``NotaryFlow.Client`` ``subFlow``. The -``SimpleNotaryService`` only offers protection against double spend, but -does no further verification. The ``ValidatingNotaryService`` checks -that proposed transactions are correctly signed by all keys listed in -the commands and runs the contract verify to ensure that the rules of -the state transition are being followed. The -``RaftValidatingNotaryService`` further extends the flow to operate -against a cluster of nodes running shared consensus state across the -RAFT protocol (note this requires the additional configuration of the -``notaryClusterAddresses`` property). +the intended initiator. Vault related services ---------------------- diff --git a/docs/source/node-structure.rst b/docs/source/node-structure.rst index 0b65211a96..478a06bc49 100644 --- a/docs/source/node-structure.rst +++ b/docs/source/node-structure.rst @@ -71,22 +71,18 @@ The name must also obey the following constraints: * The ``country`` attribute is a valid ISO 3166-1 two letter code in upper-case -* All attributes must obey the following constraints: - - * Upper-case first letter +* The ``organisation`` field of the name obeys the following constraints: * Has at least two letters - * No leading or trailing whitespace - * Does not include the following characters: ``,`` , ``=`` , ``$`` , ``"`` , ``'`` , ``\`` + * Does not include the following characters: ``,`` , ``"``, ``\`` * Is in NFKC normalization form * Does not contain the null character * Only the latin, common and inherited unicode scripts are supported - -* The ``organisation`` field of the name also obeys the following constraints: - * No double-spacing - * This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and - character confusability attacks +This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and +character confusability attacks. + +.. note:: The network operator of a Corda Network may put additional constraints on node naming in place. External identifiers ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/resources/full-tx.png b/docs/source/resources/full-tx.png index 354fc1c5e4..408dd049bc 100644 Binary files a/docs/source/resources/full-tx.png and b/docs/source/resources/full-tx.png differ diff --git a/docs/source/resources/testnet-download.png b/docs/source/resources/testnet-download.png new file mode 100644 index 0000000000..537cb91ab0 Binary files /dev/null and b/docs/source/resources/testnet-download.png differ diff --git a/docs/source/tutorial-integration-testing.rst b/docs/source/tutorial-integration-testing.rst index f2e410d347..1647d1bf43 100644 --- a/docs/source/tutorial-integration-testing.rst +++ b/docs/source/tutorial-integration-testing.rst @@ -1,119 +1,128 @@ +.. highlight:: kotlin +.. raw:: html + + + + Integration testing =================== -Integration testing involves bringing up nodes locally and testing -invariants about them by starting flows and inspecting their state. +Integration testing involves bringing up nodes locally and testing invariants about them by starting flows and inspecting +their state. -In this tutorial we will bring up three nodes - Alice, Bob and a -notary. Alice will issue cash to Bob, then Bob will send this cash -back to Alice. We will see how to test some simple deterministic and -nondeterministic invariants in the meantime. +In this tutorial we will bring up three nodes - Alice, Bob and a notary. Alice will issue cash to Bob, then Bob will send +this cash back to Alice. We will see how to test some simple deterministic and nondeterministic invariants in the meantime. -.. note:: This example where Alice is self-issuing cash is purely for - demonstration purposes, in reality, cash would be issued by a bank - and subsequently passed around. +.. note:: This example where Alice is self-issuing cash is purely for demonstration purposes, in reality, cash would be + issued by a bank and subsequently passed around. -In order to spawn nodes we will use the Driver DSL. This DSL allows -one to start up node processes from code. It manages a network map -service and safe shutting down of nodes in the background. +In order to spawn nodes we will use the Driver DSL. This DSL allows one to start up node processes from code. It creates +a local network where all the nodes see each other and provides safe shutting down of nodes in the background. -.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt - :language: kotlin - :start-after: START 1 - :end-before: END 1 - :dedent: 8 +.. container:: codeset -The above code starts three nodes: + .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 1 + :end-before: END 1 + :dedent: 8 -* Alice, who has user permissions to start the ``CashIssueFlow`` and - ``CashPaymentFlow`` flows -* Bob, who only has user permissions to start the ``CashPaymentFlow`` -* A notary that offers a ``ValidatingNotaryService``. We won't connect - to the notary directly, so there's no need to provide a ``User`` + .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java + :language: java + :start-after: START 1 + :end-before: END 1 + :dedent: 8 -The ``startNode`` function returns a future that completes once the -node is fully started. This allows starting of the nodes to be -parallel. We wait on these futures as we need the information -returned; their respective ``NodeHandles`` s. +The above code starts two nodes: -.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt - :language: kotlin - :start-after: START 2 - :end-before: END 2 - :dedent: 12 +* Alice, configured with an RPC user who has permissions to start the ``CashIssueAndPaymentFlow`` flow on it and query + Alice's vault. +* Bob, configured with an RPC user who only has permissions to start the ``CashPaymentFlow`` and query Bob's vault. -After getting the handles we wait for both parties to register with -the network map to ensure we don't have race conditions with network -map registration. Next we connect to Alice and Bob respectively from -the test process using the test user we created. Then we establish RPC -links that allow us to start flows and query state. +.. note:: You will notice that we did not start a notary. This is done automatically for us by the driver - it creates + a notary node with the name ``DUMMY_NOTARY_NAME`` which is visible to both nodes. If you wish to customise this, for + example create more notaries, then specify the ``DriverParameters.notarySpecs`` parameter. -.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt - :language: kotlin - :start-after: START 3 - :end-before: END 3 - :dedent: 12 +The ``startNode`` function returns a ``CordaFuture`` object that completes once the node is fully started and visible on +the local network. Returning a future allows starting of the nodes to be parallel. We wait on these futures as we need +the information returned; their respective ``NodeHandles`` s. -We will be interested in changes to Alice's and Bob's vault, so we -query a stream of vault updates from each. +.. container:: codeset + + .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 2 + :end-before: END 2 + :dedent: 12 + + .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java + :language: java + :start-after: START 2 + :end-before: END 2 + :dedent: 16 + +Next we connect to Alice and Bob from the test process using the test users we created. We establish RPC links that allow +us to start flows and query state. + +.. container:: codeset + + .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 3 + :end-before: END 3 + :dedent: 12 + + .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java + :language: java + :start-after: START 3 + :end-before: END 3 + :dedent: 16 + +We will be interested in changes to Alice's and Bob's vault, so we query a stream of vault updates from each. Now that we're all set up we can finally get some cash action going! -.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt - :language: kotlin - :start-after: START 4 - :end-before: END 4 - :dedent: 12 +.. container:: codeset -The first loop creates 10 threads, each starting a ``CashFlow`` flow -on the Alice node. We specify that we want to issue ``i`` dollars to -Bob, setting our notary as the notary responsible for notarising the -created states. Note that no notarisation will occur yet as we're not -spending any states, only creating new ones on the ledger. + .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 4 + :end-before: END 4 + :dedent: 12 -We started the flows from different threads for the sake of the -tutorial, to demonstrate how to test non-determinism, which is what -the ``expectEvents`` block does. + .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java + :language: java + :start-after: START 4 + :end-before: END 4 + :dedent: 16 -The Expect DSL allows ordering constraints to be checked on a stream -of events. The above code specifies that we are expecting 10 updates -to be emitted on the ``bobVaultUpdates`` stream in unspecified order -(this is what the ``parallel`` construct does). We specify an -(otherwise optional) ``match`` predicate to identify specific updates -we are interested in, which we then print. +We start a ``CashIssueAndPaymentFlow`` flow on the Alice node. We specify that we want Alice to self-issue $1000 which is +to be payed to Bob. We specify the default notary identity created by the driver as the notary responsible for notarising +the created states. Note that no notarisation will occur yet as we're not spending any states, only creating new ones on +the ledger. -If we run the code written so far we should see 4 nodes starting up -(Alice, Bob, the notary and an implicit Network Map service), then -10 logs of Bob receiving 1,2,...10 dollars from Alice in some unspecified -order. +We expect a single update to Bob's vault when it receives the $1000 from Alice. This is what the ``expectEvents`` call +is asserting. + +.. container:: codeset + + .. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt + :language: kotlin + :start-after: START 5 + :end-before: END 5 + :dedent: 12 + + .. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java + :language: java + :start-after: START 5 + :end-before: END 5 + :dedent: 16 Next we want Bob to send this cash back to Alice. -.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt - :language: kotlin - :start-after: START 5 - :end-before: END 5 - :dedent: 12 +That's it! We saw how to start up several corda nodes locally, how to connect to them, and how to test some simple invariants +about ``CashIssueAndPaymentFlow`` and ``CashPaymentFlow``. -This time we'll do it sequentially. We make Bob pay 1,2,..10 dollars -to Alice in order. We make sure that a the ``CashFlow`` has finished -by waiting on ``startFlow`` 's ``returnValue``. - -Then we use the Expect DSL again, this time using ``sequence`` to test -for the updates arriving in the order we expect them to. - -Note that ``parallel`` and ``sequence`` may be nested into each other -arbitrarily to test more complex scenarios. - -That's it! We saw how to start up several corda nodes locally, how to -connect to them, and how to test some simple invariants about -``CashFlow``. - -To run the complete test you can open -``example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt`` -from IntelliJ and run the test, or alternatively use gradle: - -.. sourcecode:: bash - - # Run example-code integration tests - ./gradlew docs/source/example-code:integrationTest -i \ No newline at end of file +You can find the complete test at ``example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java`` +(Java) and ``example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt`` (Kotlin) in the +`Corda repo `_. diff --git a/docs/source/vault.rst b/docs/source/vault.rst index 62b4df0bb4..371a4781d6 100644 --- a/docs/source/vault.rst +++ b/docs/source/vault.rst @@ -6,9 +6,9 @@ that can be easily queried and worked with. The vault keeps track of both unconsumed and consumed states: - * Unconsumed (or unspent) states represent fungible states available for spending (including spend-to-self transactions) + * **Unconsumed** (or unspent) states represent fungible states available for spending (including spend-to-self transactions) and linear states available for evolution (eg. in response to a lifecycle event on a deal) or transfer to another party. - * Consumed (or spent) states represent ledger immutable state for the purpose of transaction reporting, audit and archival, including the ability to perform joins with app-private data (like customer notes) + * **Consumed** (or spent) states represent ledger immutable state for the purpose of transaction reporting, audit and archival, including the ability to perform joins with app-private data (like customer notes). By fungible we refer to assets of measurable quantity (eg. a cash currency, units of stock) which can be combined together to represent a single ledger state. @@ -40,15 +40,15 @@ The following diagram illustrates the breakdown of the vault into sub-system com Note the following: -* the vault "On Ledger" store tracks unconsumed state and is updated internally by the node upon recording of a transaction on the ledger - (following successful smart contract verification and signature by all participants) -* the vault "Off Ledger" store refers to additional data added by the node owner subsequent to transaction recording -* the vault performs fungible state spending (and in future, fungible state optimisation management including merging, splitting and re-issuance) -* vault extensions represent additional custom plugin code a developer may write to query specific custom contract state attributes. -* customer "Off Ledger" (private store) represents internal organisational data that may be joined with the vault data to perform additional reporting or processing -* a :doc:`Vault Query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms -* a vault update API is internally used by transaction recording flows. -* the vault database schemas are directly accessible via JDBC for customer joins and queries +* The vault "On Ledger" store tracks unconsumed state and is updated internally by the node upon recording of a transaction on the ledger + (following successful smart contract verification and signature by all participants). +* The vault "Off Ledger" store refers to additional data added by the node owner subsequent to transaction recording. +* The vault performs fungible state spending (and in future, fungible state optimisation management including merging, splitting and re-issuance). +* Vault extensions represent additional custom plugin code a developer may write to query specific custom contract state attributes. +* Customer "Off Ledger" (private store) represents internal organisational data that may be joined with the vault data to perform additional reporting or processing. +* A :doc:`Vault Query API ` is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms. +* A vault update API is internally used by transaction recording flows. +* The vault database schemas are directly accessible via JDBC for customer joins and queries. Section 8 of the `Technical white paper`_ describes features of the vault yet to be implemented including private key management, state splitting and merging, asset re-issuance and node event scheduling. diff --git a/node-api/build.gradle b/node-api/build.gradle index bc642a4116..2aa8f97de4 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -94,26 +94,19 @@ shadowJar { // systems, the folder and file end up clashing, causing an error when trying to build the JAR. relocate 'META-INF/LICENSE', 'META-INF/LICENCE-2' dependencies { + // Apache Curator 4.0.1 already contains a shaded copy of Guava 20.0 include(dependency("org.apache.curator:curator-client:${curator_version}")) include(dependency("org.apache.curator:curator-recipes:${curator_version}")) include(dependency("org.apache.curator:curator-framework:${curator_version}")) include(dependency('org.apache.zookeeper:zookeeper:3.5.3-beta')) include(dependency('commons-cli:commons-cli:1.2')) include(dependency('io.netty:netty:3.10.5.Final')) - include(dependency('com.google.guava:guava:20.0')) } relocate 'org.apache.curator.', 'net.corda.shaded.org.apache.curator.' relocate 'org.apache.zookeeper.', 'net.corda.shaded.org.apache.zookeeper.' relocate 'org.apache.jute.', 'net.corda.shaded.org.apache.jute.' relocate 'org.apache.commons.', 'net.corda.shaded.org.apache.commons.' relocate 'org.jboss.netty.', 'net.corda.shaded.org.jboss.netty.' - relocate ('com.google.', 'net.corda.shaded.com.google.') { - // This JAR uses annotations from these packages. However, - // the annotation classes themselves are not included here - // and so we cannot relocate their references. - exclude 'com.google.errorprone.**' - exclude 'com.google.j2objc.**' - } } task testJar(type: Jar) { diff --git a/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java b/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java new file mode 100644 index 0000000000..f1f3af69c6 --- /dev/null +++ b/node/src/integration-test/java/net/corda/serialization/reproduction/GenericReturnFailureReproductionIntegrationTest.java @@ -0,0 +1,73 @@ +package net.corda.serialization.reproduction; + +import net.corda.client.rpc.CordaRPCClient; +import net.corda.core.concurrent.CordaFuture; +import net.corda.core.flows.FlowLogic; +import net.corda.core.flows.StartableByRPC; +import net.corda.core.serialization.CordaSerializable; +import net.corda.node.services.Permissions; +import net.corda.testing.driver.Driver; +import net.corda.testing.driver.DriverParameters; +import net.corda.testing.driver.NodeHandle; +import net.corda.testing.driver.NodeParameters; +import net.corda.testing.node.User; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class GenericReturnFailureReproductionIntegrationTest { + + @Test() + public void flowShouldReturnGenericList() { + User user = new User("yes", "yes", Collections.singleton(Permissions.startFlow(SuperSimpleGenericFlow.class))); + DriverParameters defaultParameters = new DriverParameters(); + Driver.driver(defaultParameters, (driver) -> { + NodeHandle startedNode = getOrThrow(driver.startNode(new NodeParameters().withRpcUsers(Collections.singletonList(user)).withStartInSameProcess(true))); + (new CordaRPCClient(startedNode.getRpcAddress())).use("yes", "yes", (cordaRPCConnection -> { + getOrThrow(cordaRPCConnection.getProxy().startFlowDynamic(SuperSimpleGenericFlow.class).getReturnValue()); + return null; + })); + return null; + }); + + } + + @StartableByRPC + public static class SuperSimpleGenericFlow extends FlowLogic> { + public SuperSimpleGenericFlow() { + } + + @Override + public GenericHolder call() { + return new GenericHolder<>(IntStream.of(100).mapToObj((i) -> "" + i).collect(Collectors.toList())); + } + } + + @CordaSerializable + public static class GenericHolder { + private final List items; + + public GenericHolder(List items) { + this.items = items; + } + + public List getItems() { + return items; + } + } + + + private static Y getOrThrow(CordaFuture future) { + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt index 7fd45428ce..ea7693fd63 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt @@ -13,7 +13,6 @@ import net.corda.node.services.Permissions import net.corda.testing.core.* import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName @@ -48,7 +47,6 @@ class FlowRetryTest : IntegrationTest() { val user = User("mark", "dadada", setOf(Permissions.startFlow())) val result: Any? = driver(DriverParameters( startNodesInProcess = isQuasarAgentSpecified(), - portAllocation = RandomFree, notarySpecs = emptyList() )) { val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() diff --git a/node/src/integration-test/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt index 39c9acbc8a..0a3193077b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/persistence/NodeStatePersistenceTests.kt @@ -10,10 +10,6 @@ package net.corda.node.persistence import co.paralleluniverse.fibers.Suspendable -import net.corda.testMessage.MESSAGE_CONTRACT_PROGRAM_ID -import net.corda.testMessage.Message -import net.corda.testMessage.MessageContract -import net.corda.testMessage.MessageState import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Command import net.corda.core.contracts.StateAndContract @@ -30,11 +26,14 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow +import net.corda.testMessage.MESSAGE_CONTRACT_PROGRAM_ID +import net.corda.testMessage.Message +import net.corda.testMessage.MessageContract +import net.corda.testMessage.MessageState import net.corda.testing.core.singleIdentity import net.corda.testing.core.* import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas @@ -57,8 +56,11 @@ class NodeStatePersistenceTests : IntegrationTest() { fun `persistent state survives node restart`() { val user = User("mark", "dadada", setOf(startFlow(), invokeRpc("vaultQuery"))) val message = Message("Hello world!") - val stateAndRef: StateAndRef? = driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), - portAllocation = RandomFree, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) { + val stateAndRef: StateAndRef? = driver(DriverParameters( + inMemoryDB = false, + startNodesInProcess = isQuasarAgentSpecified(), + extraCordappPackagesToScan = listOf(MessageState::class.packageName) + )) { val nodeName = { val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.singleIdentity().name @@ -90,7 +92,11 @@ class NodeStatePersistenceTests : IntegrationTest() { val user = User("mark", "dadada", setOf(startFlow(), invokeRpc("vaultQuery"))) val message = Message("Hello world!") - val stateAndRef: StateAndRef? = driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = RandomFree, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) { + val stateAndRef: StateAndRef? = driver(DriverParameters( + inMemoryDB = false, + startNodesInProcess = isQuasarAgentSpecified(), + extraCordappPackagesToScan = listOf(MessageState::class.packageName) + )) { val nodeName = { val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.singleIdentity().name diff --git a/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowMultiThreadedSMMTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowMultiThreadedSMMTests.kt index 89b64b1106..4a3e8064d0 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowMultiThreadedSMMTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/TimedFlowMultiThreadedSMMTests.kt @@ -32,7 +32,6 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName @@ -65,7 +64,7 @@ class TimedFlowMultiThreadedSMMTests : IntegrationTest() { @Test fun `timed flow is retried`() { val user = User("test", "pwd", setOf(Permissions.startFlow(), Permissions.startFlow())) - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree)) { + driver(DriverParameters(startNodesInProcess = true)) { val configOverrides = mapOf("flowTimeout" to mapOf( "timeout" to Duration.ofSeconds(3), "maxRestartCount" to 2, @@ -86,7 +85,7 @@ class TimedFlowMultiThreadedSMMTests : IntegrationTest() { @Test fun `progress tracker is preserved after flow is retried`() { val user = User("test", "pwd", setOf(Permissions.startFlow(), Permissions.startFlow())) - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree)) { + driver(DriverParameters(startNodesInProcess = true)) { val configOverrides = mapOf("flowTimeout" to mapOf( "timeout" to Duration.ofSeconds(2), diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 42cd453b6d..2beb8e1e8d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -25,8 +25,8 @@ import net.corda.nodeapi.internal.network.SignedNetworkParameters import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.internal.NodeHandleInternal -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName @@ -52,7 +52,7 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP val testSerialization = SerializationEnvironmentRule(true) private val cacheTimeout = 1.seconds - private val portAllocation = RandomFree + private val portAllocation = PortAllocation.Incremental(10000) private lateinit var networkMapServer: NetworkMapServer private lateinit var compatibilityZone: CompatibilityZoneParams diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt index 164b826f6d..11d33e4210 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt @@ -13,13 +13,13 @@ package net.corda.node.services.rpc import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.RPCException import net.corda.core.internal.div +import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.all -import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.core.messaging.ClientRpcSslOptions import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore +import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_RPC_USER import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME @@ -27,7 +27,6 @@ import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName @@ -55,7 +54,7 @@ class RpcSslTest : IntegrationTest() { @JvmField val tempFolder = TemporaryFolder() - val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB") + private val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB") @Test fun `RPC client using ssl is able to run a command`() { @@ -70,7 +69,7 @@ class RpcSslTest : IntegrationTest() { val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow() val client = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions) val connection = client.start(user.username, user.password) @@ -108,7 +107,7 @@ class RpcSslTest : IntegrationTest() { val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow() Assertions.assertThatThrownBy { val connection = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password) @@ -128,7 +127,7 @@ class RpcSslTest : IntegrationTest() { fun `RPC client not using ssl can run commands`() { val user = User("mark", "dadada", setOf(all())) var successful = false - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(rpcUsers = listOf(user)).getOrThrow() val connection = CordaRPCClient(node.rpcAddress).start(user.username, user.password) connection.proxy.apply { @@ -148,7 +147,7 @@ class RpcSslTest : IntegrationTest() { val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow() val client = CordaRPCClient.createWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index 9713ad28d9..169fd75a98 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -13,13 +13,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.ReceiveTransactionFlow -import net.corda.core.flows.SendTransactionFlow -import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.* import net.corda.core.internal.InputStreamAndHash import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow @@ -29,17 +23,12 @@ import net.corda.node.services.config.MB import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.core.TestIdentity -import net.corda.testing.core.dummyCommand +import net.corda.testing.core.* import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.node.User import org.junit.ClassRule import org.junit.Test @@ -105,8 +94,8 @@ class LargeTransactionsTest : IntegrationTest() { driver(DriverParameters( startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), - networkParameters = testNetworkParameters(maxTransactionSize = 13.MB.toInt()), - portAllocation = RandomFree)) { + networkParameters = testNetworkParameters(maxTransactionSize = 13.MB.toInt()) + )) { val rpcUser = User("admin", "admin", setOf("ALL")) val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow() CordaRPCClient(alice.rpcAddress).use(rpcUser.username, rpcUser.password) { diff --git a/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt b/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt index 71ae041f81..2851bab505 100644 --- a/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/rpc/ArtemisRpcTests.kt @@ -33,7 +33,6 @@ import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.internal.createNodeSslConfig import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl @@ -46,7 +45,7 @@ import java.nio.file.Path import javax.security.auth.x500.X500Principal class ArtemisRpcTests { - private val ports: PortAllocation = RandomFree + private val ports: PortAllocation = PortAllocation.Incremental(10000) private val user = User("mark", "dadada", setOf(all())) private val users = listOf(user) @@ -106,8 +105,14 @@ class ArtemisRpcTests { }.isInstanceOf(RPCException::class.java) } - private fun testSslCommunication(nodeSSlconfig: SSLConfiguration, brokerSslOptions: BrokerRpcSslOptions?, useSslForBroker: Boolean, clientSslOptions: ClientRpcSslOptions?, address: NetworkHostAndPort = ports.nextHostAndPort(), - adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), baseDirectory: Path = tempFolder.root.toPath()) { + private fun testSslCommunication(nodeSSlconfig: SSLConfiguration, + brokerSslOptions: BrokerRpcSslOptions?, + useSslForBroker: Boolean, + clientSslOptions: ClientRpcSslOptions?, + address: NetworkHostAndPort = ports.nextHostAndPort(), + adminAddress: NetworkHostAndPort = ports.nextHostAndPort(), + baseDirectory: Path = tempFolder.root.toPath() + ) { val maxMessageSize = 10000 val jmxEnabled = false diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 1ec5a6de09..0c455cdca2 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -8,8 +8,10 @@ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited. */ -buildscript { - ext.strata_version = '1.1.2' +allprojects { + ext { + strata_version = '1.1.2' + } } apply plugin: 'java' @@ -52,7 +54,7 @@ dependencies { // The SIMM demo CorDapp depends upon Cash CorDapp features cordapp project(':finance') - cordapp project(':samples:simm-valuation-demo:contracts-states') + cordapp project(path: ':samples:simm-valuation-demo:contracts-states', configuration: 'shrinkArtifacts') cordapp project(':samples:simm-valuation-demo:flows') // Corda integration dependencies @@ -62,26 +64,26 @@ dependencies { cordaCompile project(':webserver') // Javax is required for webapis - compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" + compile "org.glassfish.jersey.core:jersey-server:$jersey_version" // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps - compile "com.opengamma.strata:strata-basics:${strata_version}" - compile "com.opengamma.strata:strata-product:${strata_version}" - compile "com.opengamma.strata:strata-data:${strata_version}" - compile "com.opengamma.strata:strata-calc:${strata_version}" - compile "com.opengamma.strata:strata-pricer:${strata_version}" - compile "com.opengamma.strata:strata-report:${strata_version}" - compile "com.opengamma.strata:strata-market:${strata_version}" - compile "com.opengamma.strata:strata-collect:${strata_version}" - compile "com.opengamma.strata:strata-loader:${strata_version}" - compile "com.opengamma.strata:strata-math:${strata_version}" + compile "com.opengamma.strata:strata-basics:$strata_version" + compile "com.opengamma.strata:strata-product:$strata_version" + compile "com.opengamma.strata:strata-data:$strata_version" + compile "com.opengamma.strata:strata-calc:$strata_version" + compile "com.opengamma.strata:strata-pricer:$strata_version" + compile "com.opengamma.strata:strata-report:$strata_version" + compile "com.opengamma.strata:strata-market:$strata_version" + compile "com.opengamma.strata:strata-collect:$strata_version" + compile "com.opengamma.strata:strata-loader:$strata_version" + compile "com.opengamma.strata:strata-math:$strata_version" // Test dependencies testCompile project(':node-driver') scenarioTestCompile project(path: ":experimental:behave", configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" - testCompile "org.assertj:assertj-core:${assertj_version}" + testCompile "org.assertj:assertj-core:$assertj_version" } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { diff --git a/samples/simm-valuation-demo/contracts-states/build.gradle b/samples/simm-valuation-demo/contracts-states/build.gradle index 5b3bc5f3f3..146966bbee 100644 --- a/samples/simm-valuation-demo/contracts-states/build.gradle +++ b/samples/simm-valuation-demo/contracts-states/build.gradle @@ -1,24 +1,76 @@ -buildscript { - ext.strata_version = '1.1.2' +apply plugin: 'net.corda.plugins.cordapp' + +def javaHome = System.getProperty('java.home') +def shrinkJar = file("$buildDir/libs/${project.name}-${project.version}-tiny.jar") + +cordapp { + info { + vendor = 'R3' + } } -apply plugin: 'net.corda.plugins.cordapp' -apply plugin: 'net.corda.plugins.cordformation' +configurations { + shrinkArtifacts +} dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + cordaCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // The SIMM demo CorDapp depends upon Cash CorDapp features cordapp project(':finance') // Corda integration dependencies - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps - compile "com.opengamma.strata:strata-product:${strata_version}" - compile "com.opengamma.strata:strata-market:${strata_version}" + compile "com.opengamma.strata:strata-product:$strata_version" + compile "com.opengamma.strata:strata-market:$strata_version" +} -} \ No newline at end of file +jar { + classifier = 'fat' +} + +import proguard.gradle.ProGuardTask +task shrink(type: ProGuardTask) { + injars jar + outjars shrinkJar + + libraryjars "$javaHome/lib/rt.jar" + libraryjars "$javaHome/lib/jce.jar" + configurations.runtime.forEach { + libraryjars it.path, filter: '!META-INF/versions/**' + } + + dontwarn 'afu.org.checkerframework.**' + dontwarn 'co.paralleluniverse.**' + dontwarn 'org.checkerframework.**' + dontwarn 'org.joda.**' + dontnote + + // We need to preserve our CorDapp's own directory structure so that Corda + // can find the contract classes. + keepdirectories 'net/corda/**' + keepattributes '*' + dontobfuscate + dontoptimize + verbose + + // These are our CorDapp classes, so don't change these. + keep 'class net.corda.vega.** { *; }', includedescriptorclasses:true + + // Until CorDapps are isolated from each other, we need to ensure that the + // versions of the classes that this CorDapp needs are still usable by other + // CorDapps. Unfortunately, this means that we cannot shrink them as much as + // we'd like to. + keepclassmembers 'class com.opengamma.strata.** { *; }', includedescriptorclasses:true + keepclassmembers 'class com.google.** { *; }', includedescriptorclasses:true + keepclassmembers 'class org.joda.** { *; }', includedescriptorclasses:true +} +jar.finalizedBy shrink + +artifacts { + shrinkArtifacts file: shrinkJar, name: project.name, type: 'jar', extension: 'jar', classifier: 'tiny', builtBy: shrink +} diff --git a/samples/simm-valuation-demo/flows/build.gradle b/samples/simm-valuation-demo/flows/build.gradle index ffe044fa55..fd3b512fab 100644 --- a/samples/simm-valuation-demo/flows/build.gradle +++ b/samples/simm-valuation-demo/flows/build.gradle @@ -1,33 +1,32 @@ -buildscript { - ext.strata_version = '1.1.2' -} - apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.cordapp' -apply plugin: 'net.corda.plugins.cordformation' + +cordapp { + info { + vendor = 'R3' + } +} dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + cordaCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // The SIMM demo CorDapp depends upon Cash CorDapp features cordapp project(':finance') - cordapp project(':samples:simm-valuation-demo:contracts-states') + cordapp project(path: ':samples:simm-valuation-demo:contracts-states', configuration: 'shrinkArtifacts') // Corda integration dependencies - cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps - compile "com.opengamma.strata:strata-basics:${strata_version}" - compile "com.opengamma.strata:strata-product:${strata_version}" - compile "com.opengamma.strata:strata-data:${strata_version}" - compile "com.opengamma.strata:strata-calc:${strata_version}" - compile "com.opengamma.strata:strata-pricer:${strata_version}" - compile "com.opengamma.strata:strata-report:${strata_version}" - compile "com.opengamma.strata:strata-market:${strata_version}" - compile "com.opengamma.strata:strata-collect:${strata_version}" - compile "com.opengamma.strata:strata-loader:${strata_version}" - compile "com.opengamma.strata:strata-math:${strata_version}" - + compile "com.opengamma.strata:strata-basics:$strata_version" + compile "com.opengamma.strata:strata-product:$strata_version" + compile "com.opengamma.strata:strata-data:$strata_version" + compile "com.opengamma.strata:strata-calc:$strata_version" + compile "com.opengamma.strata:strata-pricer:$strata_version" + compile "com.opengamma.strata:strata-report:$strata_version" + compile "com.opengamma.strata:strata-market:$strata_version" + compile "com.opengamma.strata:strata-collect:$strata_version" + compile "com.opengamma.strata:strata-loader:$strata_version" + compile "com.opengamma.strata:strata-math:$strata_version" } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt index 46fc05271e..5dafefb790 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt @@ -73,9 +73,13 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory: private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList()) + private val outboundType = resolveTypeVariables(declaredType.actualTypeArguments[0], null) + private val inboundType = declaredType.actualTypeArguments[0] + + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { if (output.writeTypeNotations(typeNotation)) { - output.requireSerializer(declaredType.actualTypeArguments[0]) + output.requireSerializer(outboundType) } } @@ -90,12 +94,13 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory: data.withDescribed(typeNotation.descriptor) { withList { for (entry in obj as Collection<*>) { - output.writeObjectOrNull(entry, this, declaredType.actualTypeArguments[0], context, debugIndent) + output.writeObjectOrNull(entry, this, outboundType, context, debugIndent) } } } } + override fun readObject( obj: Any, schemas: SerializationSchemas, @@ -103,7 +108,7 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory: context: SerializationContext): Any = ifThrowsAppend({ declaredType.typeName }) { // TODO: Can we verify the entries in the list? concreteBuilder((obj as List<*>).map { - input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0], context) + input.readObjectOrNull(it, schemas, inboundType, context) }) } } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt index 586b16beb4..30b78d925e 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt @@ -80,10 +80,15 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList()) + private val inboundKeyType = declaredType.actualTypeArguments[0] + private val outboundKeyType = resolveTypeVariables(inboundKeyType, null) + private val inboundValueType = declaredType.actualTypeArguments[1] + private val outboundValueType = resolveTypeVariables(inboundValueType, null) + override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) { if (output.writeTypeNotations(typeNotation)) { - output.requireSerializer(declaredType.actualTypeArguments[0]) - output.requireSerializer(declaredType.actualTypeArguments[1]) + output.requireSerializer(outboundKeyType) + output.requireSerializer(outboundValueType) } } @@ -101,8 +106,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial data.putMap() data.enter() for ((key, value) in obj as Map<*, *>) { - output.writeObjectOrNull(key, data, declaredType.actualTypeArguments[0], context, debugIndent) - output.writeObjectOrNull(value, data, declaredType.actualTypeArguments[1], context, debugIndent) + output.writeObjectOrNull(key, data, outboundKeyType, context, debugIndent) + output.writeObjectOrNull(value, data, outboundValueType, context, debugIndent) } data.exit() // exit map } @@ -118,8 +123,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial private fun readEntry(schemas: SerializationSchemas, input: DeserializationInput, entry: Map.Entry, context: SerializationContext - ) = input.readObjectOrNull(entry.key, schemas, declaredType.actualTypeArguments[0], context) to - input.readObjectOrNull(entry.value, schemas, declaredType.actualTypeArguments[1], context) + ) = input.readObjectOrNull(entry.key, schemas, inboundKeyType, context) to + input.readObjectOrNull(entry.value, schemas, inboundValueType, context) // Cannot use * as a bound for EnumMap and EnumSet since * is not an enum. So, we use a sample enum instead. // We don't actually care about the type, we just need to make the compiler happier. 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 630c0d5af1..d5728542ba 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 @@ -437,7 +437,7 @@ fun Data.writeReferencedObject(refObject: ReferencedObject) { exit() // exit described } -private fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { +fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { val resolvedType = if (contextType != null) TypeToken.of(contextType).resolveType(actualType).type else actualType // TODO: surely we check it is concrete at this point with no TypeVariables return if (resolvedType is TypeVariable<*>) { diff --git a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java index 1c789ec0e2..4410758973 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java +++ b/serialization/src/test/java/net/corda/serialization/internal/amqp/JavaGenericsTest.java @@ -10,27 +10,181 @@ package net.corda.serialization.internal.amqp; +import net.corda.core.serialization.CordaSerializable; import net.corda.core.serialization.SerializedBytes; +import net.corda.serialization.internal.amqp.custom.BigIntegerSerializer; +import net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt; import net.corda.serialization.internal.amqp.testutils.TestSerializationContext; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; import org.junit.Test; + import java.io.NotSerializableException; +import java.math.BigInteger; +import java.util.*; import static net.corda.serialization.internal.amqp.testutils.AMQPTestUtilsKt.testDefaultFactory; import static org.jgroups.util.Util.assertEquals; +@SuppressWarnings("unchecked") public class JavaGenericsTest { private static class Inner { private final Integer v; - private Inner(Integer v) { this.v = v; } - Integer getV() { return v; } + private Inner(Integer v) { + this.v = v; + } + + Integer getV() { + return v; + } } private static class A { private final T t; - private A(T t) { this.t = t; } - public T getT() { return t; } + private A(T t) { + this.t = t; + } + + public T getT() { + return t; + } + } + + @CordaSerializable + private static class ConcreteClass { + private final String theItem; + + private ConcreteClass(String theItem) { + this.theItem = theItem; + } + + public String getTheItem() { + return theItem; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConcreteClass that = (ConcreteClass) o; + return Objects.equals(theItem, that.theItem); + } + + @Override + public int hashCode() { + + return Objects.hash(theItem); + } + } + + + @CordaSerializable + private static class GenericClassWithList { + private final List items; + + private GenericClassWithList(List items) { + this.items = items; + } + + public List getItems() { + return items; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GenericClassWithList that = (GenericClassWithList) o; + return Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + + return Objects.hash(items); + } + } + + @CordaSerializable + private static class GenericClassWithMap { + private final Map theMap; + + private GenericClassWithMap(Map theMap) { + this.theMap = new LinkedHashMap<>(theMap); + } + + public Map getTheMap() { + return theMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GenericClassWithMap that = (GenericClassWithMap) o; + return Objects.equals(theMap, that.theMap); + } + + @Override + public int hashCode() { + + return Objects.hash(theMap); + } + } + + @CordaSerializable + private static class HolderOfGeneric { + private final G theGeneric; + + private HolderOfGeneric(G theGeneric) { + this.theGeneric = theGeneric; + } + + public G getTheGeneric() { + return theGeneric; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HolderOfGeneric that = (HolderOfGeneric) o; + return Objects.equals(theGeneric, that.theGeneric); + } + + @Override + public int hashCode() { + return Objects.hash(theGeneric); + } + } + + @Test + + public void shouldSupportNestedGenericsFromJavaWithCollections() throws NotSerializableException { + ConcreteClass concreteClass = new ConcreteClass("How to make concrete, $99/class"); + HolderOfGeneric> genericList = new HolderOfGeneric<>(new GenericClassWithList<>(Collections.singletonList(concreteClass))); + SerializerFactory factory = AMQPTestUtilsKt.testDefaultFactoryWithWhitelist(); + SerializationOutput ser = new SerializationOutput(factory); + SerializedBytes bytes = ser.serialize(genericList, TestSerializationContext.testSerializationContext); + DeserializationInput des = new DeserializationInput(factory); + HolderOfGeneric> genericList2 = des.deserialize(bytes, HolderOfGeneric.class, TestSerializationContext.testSerializationContext); + Assert.assertThat(genericList, CoreMatchers.is(CoreMatchers.equalTo(genericList2))); + + } + + @Test + public void shouldSupportNestedGenericsFromJavaWithMaps() throws NotSerializableException { + ConcreteClass concreteClass = new ConcreteClass("How to make concrete, $99/class"); + GenericClassWithMap genericMap = new GenericClassWithMap<>(Collections.singletonMap(concreteClass, BigInteger.ONE)); + SerializerFactory factory = AMQPTestUtilsKt.testDefaultFactoryWithWhitelist(); + factory.register(BigIntegerSerializer.INSTANCE); + SerializationOutput ser = new SerializationOutput(factory); + SerializedBytes bytes = ser.serialize(genericMap, TestSerializationContext.testSerializationContext); + DeserializationInput des = new DeserializationInput(factory); + GenericClassWithMap genericMap2 = des.deserialize(bytes, GenericClassWithMap.class, TestSerializationContext.testSerializationContext); + Assert.assertThat(genericMap2, CoreMatchers.is(CoreMatchers.equalTo(genericMap2))); } @Test @@ -77,7 +231,7 @@ public class JavaGenericsTest { @Test public void forceWildcard() throws NotSerializableException { SerializedBytes bytes = forceWildcardSerialize(new A<>(new Inner(29))); - Inner i = (Inner)forceWildcardDeserialize(bytes).getT(); + Inner i = (Inner) forceWildcardDeserialize(bytes).getT(); assertEquals(29, i.getV()); } @@ -86,7 +240,7 @@ public class JavaGenericsTest { SerializerFactory factory = testDefaultFactory(); SerializedBytes bytes = forceWildcardSerializeFactory(new A<>(new Inner(29)), factory); - Inner i = (Inner)forceWildcardDeserializeFactory(bytes, factory).getT(); + Inner i = (Inner) forceWildcardDeserializeFactory(bytes, factory).getT(); assertEquals(29, i.getV()); } diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 3baabd14c5..5d257d020a 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -27,7 +27,6 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.http.HttpApi import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas @@ -109,15 +108,6 @@ class DriverTests : IntegrationTest() { } } - @Test - fun `random free port allocation`() { - val nodeHandle = driver(DriverParameters(portAllocation = RandomFree, notarySpecs = emptyList())) { - val nodeInfo = startNode(providedName = DUMMY_BANK_A_NAME) - nodeMustBeUp(nodeInfo) - } - nodeMustBeDown(nodeHandle) - } - @Test fun `debug mode enables debug logging level`() { // Make sure we're using the log4j2 config which writes to the log file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt index e060181dcc..da1ab4b34c 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt @@ -22,11 +22,8 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.driver.InProcess import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.OutOfProcess -import net.corda.testing.driver.PortAllocation import net.corda.testing.node.User import rx.Observable -import java.net.InetSocketAddress -import java.net.ServerSocket import java.nio.file.Path interface NodeHandleInternal : NodeHandle { @@ -88,12 +85,3 @@ data class InProcessImpl( } val InProcess.internalServices: StartedNodeServices get() = services as StartedNodeServices - -object RandomFree : PortAllocation() { - override fun nextPort(): Int { - return ServerSocket().use { - it.bind(InetSocketAddress(0)) - it.localPort - } - } -} \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt index bc89fed111..c58e2d5741 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt @@ -314,12 +314,17 @@ class CashViewer : CordaView("Cash") { tickLabelFormatter = stringConverter { Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString() } + label= "Timeline" + style = "-fx-font-size: 10px;" + } val yAxis = NumberAxis().apply { isAutoRanging = true isMinorTickVisible = false isForceZeroInRange = false tickLabelFormatter = stringConverter { it.toStringWithSuffix() } + label= "Cash" + style = "-fx-font-size: 10px;" } linechart(null, xAxis, yAxis) { series("USD") { diff --git a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt index c22a142050..5dfbd9a8a0 100644 --- a/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt +++ b/tools/shell/src/integration-test/kotlin/net/corda/tools/shell/InteractiveShellIntegrationTest.kt @@ -15,16 +15,16 @@ import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.JSch import net.corda.client.rpc.RPCException import net.corda.core.internal.div +import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.config.shell.toShellConfig -import net.corda.nodeapi.BrokerRpcSslOptions -import net.corda.core.messaging.ClientRpcSslOptions import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate import net.corda.node.utilities.saveToKeyStore import net.corda.node.utilities.saveToTrustStore +import net.corda.nodeapi.BrokerRpcSslOptions import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME @@ -32,7 +32,6 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.driver.internal.NodeHandleInternal -import net.corda.testing.driver.internal.RandomFree import net.corda.testing.internal.IntegrationTest import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName @@ -67,7 +66,7 @@ class InteractiveShellIntegrationTest : IntegrationTest() { @Test fun `shell should not log in with invalid credentials`() { val user = User("u", "p", setOf()) - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true) val node = nodeFuture.getOrThrow() @@ -108,7 +107,7 @@ class InteractiveShellIntegrationTest : IntegrationTest() { val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), @@ -136,7 +135,7 @@ class InteractiveShellIntegrationTest : IntegrationTest() { val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1) val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(), @@ -153,7 +152,7 @@ class InteractiveShellIntegrationTest : IntegrationTest() { @Test fun `internal shell user should not be able to connect if node started with devMode=false`() { - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { startNode().getOrThrow().use { node -> val conf = (node as NodeHandleInternal).configuration.toShellConfig() InteractiveShell.startShellInternal(conf) @@ -219,7 +218,7 @@ class InteractiveShellIntegrationTest : IntegrationTest() { val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password") var successful = false - driver(DriverParameters(startNodesInProcess = true, portAllocation = RandomFree, notarySpecs = emptyList())) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),