mirror of
https://github.com/corda/corda.git
synced 2025-06-10 11:21:45 +00:00
Merge pull request #1575 from corda/tudor-os-merge-19-11
Tudor os merge 19 11
This commit is contained in:
commit
a3a9be6594
1232
.ci/api-current.txt
1232
.ci/api-current.txt
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
package net.corda.client.jackson.internal
|
||||
|
||||
import com.fasterxml.jackson.annotation.*
|
||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
|
||||
import com.fasterxml.jackson.annotation.JsonCreator.Mode.DISABLED
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
@ -38,7 +38,10 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.parseAsHex
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.serialization.internal.AllWhitelist
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
|
||||
import net.corda.serialization.internal.amqp.constructorForDeserialization
|
||||
import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||
import net.corda.serialization.internal.amqp.propertiesForSerialization
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
@ -327,11 +330,11 @@ private class PartialTreeJson(val includedLeaf: SecureHash? = null,
|
||||
val right: PartialTreeJson? = null) {
|
||||
init {
|
||||
if (includedLeaf != null) {
|
||||
require(leaf == null && left == null && right == null)
|
||||
require(leaf == null && left == null && right == null) { "Invalid JSON structure" }
|
||||
} else if (leaf != null) {
|
||||
require(left == null && right == null)
|
||||
require(left == null && right == null) { "Invalid JSON structure" }
|
||||
} else {
|
||||
require(left != null && right != null)
|
||||
require(left != null && right != null) { "Invalid JSON structure" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ class NodeMonitorModel : AutoCloseable {
|
||||
val _connection = client.start(username, password)
|
||||
// Check connection is truly operational before returning it.
|
||||
val nodeInfo = _connection.proxy.nodeInfo()
|
||||
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty())
|
||||
require(nodeInfo.legalIdentitiesAndCerts.isNotEmpty()){"No identity certificates found"}
|
||||
_connection
|
||||
} catch (exception: Exception) {
|
||||
if (shouldRetry) {
|
||||
|
@ -126,7 +126,7 @@ interface Validated<TARGET, ERROR> {
|
||||
*/
|
||||
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
|
||||
init {
|
||||
require(errors.isNotEmpty())
|
||||
require(errors.isNotEmpty()) { "No errors encountered during validation" }
|
||||
}
|
||||
|
||||
override fun value(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
|
||||
|
@ -40,14 +40,14 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
fun getInstance(asn1: ASN1Primitive): PublicKey {
|
||||
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY) { "Key must be composite" }
|
||||
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
|
||||
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
|
||||
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
|
||||
val builder = Builder()
|
||||
val listOfChildren = sequenceOfChildren.objects.toList()
|
||||
listOfChildren.forEach { childAsn1 ->
|
||||
require(childAsn1 is ASN1Sequence)
|
||||
require(childAsn1 is ASN1Sequence) { "Child key is not in ASN1 format" }
|
||||
val childSeq = childAsn1 as ASN1Sequence
|
||||
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
|
||||
val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1))
|
||||
@ -274,7 +274,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
* is invalid (for example it would contain no keys).
|
||||
*/
|
||||
fun build(threshold: Int? = null): PublicKey {
|
||||
require(threshold == null || threshold > 0)
|
||||
require(threshold == null || threshold > 0) { "Threshold must not be specified or its value must be greater than zero" }
|
||||
val n = children.size
|
||||
return when {
|
||||
n > 1 -> CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children)
|
||||
|
@ -21,7 +21,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes). */
|
||||
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
|
||||
init {
|
||||
require(bytes.size == 32)
|
||||
require(bytes.size == 32) { "Invalid hash size, must be 32 bytes" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ val cordaBouncyCastleProvider = BouncyCastleProvider().apply {
|
||||
Security.addProvider(it)
|
||||
}
|
||||
val bouncyCastlePQCProvider = BouncyCastlePQCProvider().apply {
|
||||
require(name == "BCPQC") // The constant it comes from is not final.
|
||||
require(name == "BCPQC") { "Invalid PQCProvider name" }
|
||||
}.also {
|
||||
Security.addProvider(it)
|
||||
}
|
||||
|
@ -27,6 +27,11 @@ fun isUploaderTrusted(uploader: String?): Boolean = uploader in TRUSTED_UPLOADER
|
||||
@KeepForDJVM
|
||||
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
companion object {
|
||||
/**
|
||||
* Returns a function that knows how to load an attachment.
|
||||
*
|
||||
* TODO - this code together with the rest of the Attachment handling (including [FetchedAttachment]) needs some refactoring as it is really hard to follow.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
|
||||
return {
|
||||
|
@ -1,6 +1,10 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.cordapp.CordappConfig
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
@ -8,11 +12,14 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.ZoneVersionTooLowException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.slf4j.MDC
|
||||
|
||||
// *Internal* Corda-specific utilities
|
||||
@ -73,3 +80,11 @@ class LazyMappedList<T, U>(val originalList: List<T>, val transform: (T, Int) ->
|
||||
override fun get(index: Int) = partialResolvedList[index]
|
||||
?: transform(originalList[index], index).also { computed -> partialResolvedList[index] = computed }
|
||||
}
|
||||
|
||||
/**
|
||||
* A SerializedStateAndRef is a pair (BinaryStateRepresentation, StateRef).
|
||||
* The [serializedState] is the actual component from the original transaction.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
data class SerializedStateAndRef(val serializedState: SerializedBytes<TransactionState<ContractState>>, val ref: StateRef)
|
@ -5,7 +5,10 @@ package net.corda.core.internal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
@ -109,7 +112,7 @@ fun <T> List<T>.randomOrNull(): T? {
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
val i = indexOf(item)
|
||||
require(i != -1)
|
||||
require(i != -1){"No such element"}
|
||||
return i
|
||||
}
|
||||
|
||||
@ -218,7 +221,8 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
|
||||
* Note that a slightly bigger than numOfExpectedBytes size is expected.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte): InputStreamAndHash {
|
||||
fun createInMemoryTestZip(numOfExpectedBytes: Int, content: Byte, entryName: String = "z"): InputStreamAndHash {
|
||||
require(numOfExpectedBytes > 0){"Expected bytes must be greater than zero"}
|
||||
require(numOfExpectedBytes > 0)
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use { zos ->
|
||||
@ -226,7 +230,7 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
|
||||
val bytes = ByteArray(arraySize) { content }
|
||||
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
zos.putNextEntry(ZipEntry("z"))
|
||||
zos.putNextEntry(ZipEntry(entryName))
|
||||
for (i in 0 until n) {
|
||||
zos.write(bytes, 0, arraySize)
|
||||
}
|
||||
@ -498,3 +502,18 @@ fun <T : Any> SerializedBytes<Any>.checkPayloadIs(type: Class<T>): Untrustworthy
|
||||
return type.castIfPossible(payloadData)?.let { UntrustworthyData(it) }
|
||||
?: throw IllegalArgumentException("We were expecting a ${type.name} but we instead got a ${payloadData.javaClass.name} ($payloadData)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Map structure that can be used as a cache in the DJVM.
|
||||
*/
|
||||
fun <K, V> createSimpleCache(maxSize: Int, onEject: (MutableMap.MutableEntry<K, V>) -> Unit = {}): MutableMap<K, V> {
|
||||
return object : LinkedHashMap<K, V>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
|
||||
val eject = size > maxSize
|
||||
if (eject) onEject(eldest!!)
|
||||
return eject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <K,V> MutableMap<K,V>.toSynchronised(): MutableMap<K,V> = Collections.synchronizedMap(this)
|
@ -14,8 +14,8 @@ interface NamedCacheFactory {
|
||||
* the name can be used to create a file name or a metric name.
|
||||
*/
|
||||
fun checkCacheName(name: String) {
|
||||
require(!name.isBlank())
|
||||
require(allowedChars.matches(name))
|
||||
require(!name.isBlank()){"Name must not be empty or only whitespace"}
|
||||
require(allowedChars.matches(name)){"Invalid characters in cache name"}
|
||||
}
|
||||
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.nodeapi.internal
|
||||
package net.corda.core.internal
|
||||
|
||||
// TODO: Add to Corda node.conf to allow customisation
|
||||
const val NODE_INFO_DIRECTORY = "additional-node-infos"
|
@ -1,15 +1,23 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.componentHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.ComponentGroup
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||
import net.corda.core.transactions.FilteredComponentGroup
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.lazyMapped
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.PublicKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/** Constructs a [NotaryChangeWireTransaction]. */
|
||||
class NotaryChangeTransactionBuilder(val inputs: List<StateRef>,
|
||||
@ -42,4 +50,75 @@ fun combinedHash(components: Iterable<SecureHash>): SecureHash {
|
||||
stream.write(it.bytes)
|
||||
}
|
||||
return stream.toByteArray().sha256()
|
||||
}
|
||||
|
||||
/**
|
||||
* This function knows how to deserialize a transaction component group.
|
||||
*
|
||||
* In case the [componentGroups] is an instance of [LazyMappedList], this function will just use the original deserialized version, and avoid an unnecessary deserialization.
|
||||
* The [forceDeserialize] will force deserialization. In can be used in case the SerializationContext changes.
|
||||
*/
|
||||
fun <T : Any> deserialiseComponentGroup(componentGroups: List<ComponentGroup>,
|
||||
clazz: KClass<T>,
|
||||
groupEnum: ComponentGroupEnum,
|
||||
forceDeserialize: Boolean = false,
|
||||
factory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
context: SerializationContext = factory.defaultContext): List<T> {
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal }
|
||||
|
||||
if (group == null || group.components.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
||||
val components = group.components
|
||||
if (!forceDeserialize && components is LazyMappedList<*, OpaqueBytes>) {
|
||||
return components.originalList as List<T>
|
||||
}
|
||||
|
||||
return components.lazyMapped { component, internalIndex ->
|
||||
try {
|
||||
factory.deserialize(component, clazz.java, context)
|
||||
} catch (e: MissingAttachmentsException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Malformed transaction, $groupEnum at index $internalIndex cannot be deserialised", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to deserialise Commands from its two groups:
|
||||
* * COMMANDS_GROUP which contains the CommandData part
|
||||
* * and SIGNERS_GROUP which contains the Signers part.
|
||||
*
|
||||
* This method used the [deserialiseComponentGroup] method.
|
||||
*/
|
||||
fun deserialiseCommands(componentGroups: List<ComponentGroup>,
|
||||
forceDeserialize: Boolean = false,
|
||||
factory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
context: SerializationContext = factory.defaultContext): List<Command<*>> {
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
// and it will throw if any of the signers objects is not List of public keys).
|
||||
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(componentGroups, List::class, ComponentGroupEnum.SIGNERS_GROUP, forceDeserialize))
|
||||
val commandDataList: List<CommandData> = deserialiseComponentGroup(componentGroups, CommandData::class, ComponentGroupEnum.COMMANDS_GROUP, forceDeserialize)
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.COMMANDS_GROUP.ordinal }
|
||||
return if (group is FilteredComponentGroup) {
|
||||
check(commandDataList.size <= signersList.size) {
|
||||
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
|
||||
}
|
||||
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
|
||||
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
// or a FilteredTransaction with no Commands (in which case group is null).
|
||||
check(commandDataList.size == signersList.size) {
|
||||
"Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match"
|
||||
}
|
||||
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[index]) }
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ class AddressBindingException(val addresses: Set<NetworkHostAndPort>) : CordaRun
|
||||
|
||||
private companion object {
|
||||
private fun message(addresses: Set<NetworkHostAndPort>): String {
|
||||
require(addresses.isNotEmpty())
|
||||
require(addresses.isNotEmpty()) { "Address list must not be empty" }
|
||||
return if (addresses.size > 1) {
|
||||
"Failed to bind on an address in ${addresses.joinToString(", ", "[", "]")}."
|
||||
} else {
|
||||
|
@ -177,10 +177,10 @@ interface SerializationContext {
|
||||
fun withClassLoader(classLoader: ClassLoader): SerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers.
|
||||
* (Requires the attachment storage to have been enabled).
|
||||
* Does not do anything.
|
||||
*/
|
||||
@Throws(MissingAttachmentsException::class)
|
||||
@Deprecated("There is no reason to call this. This method does not actually do anything.")
|
||||
fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext
|
||||
|
||||
/**
|
||||
@ -300,13 +300,8 @@ class SerializedBytes<T : Any>(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/**
|
||||
* Serializes the given object and returns a [SerializedBytes] wrapper for it. An alias for [Any.serialize]
|
||||
* intended to make the calling smoother for Java users.
|
||||
*
|
||||
* TODO: Take out the @CordaInternal annotation post-Enterprise GA when we can add API again.
|
||||
*
|
||||
* @suppress
|
||||
*/
|
||||
@JvmStatic
|
||||
@CordaInternal
|
||||
@JvmOverloads
|
||||
fun <T : Any> from(obj: T, serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
|
||||
context: SerializationContext = serializationFactory.defaultContext): SerializedBytes<T> {
|
||||
|
@ -0,0 +1,161 @@
|
||||
package net.corda.core.serialization.internal
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory.toUrl
|
||||
import net.corda.core.internal.createSimpleCache
|
||||
import net.corda.core.internal.toSynchronised
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.*
|
||||
|
||||
/**
|
||||
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
|
||||
* need to provide JAR streams, and so could be fetched from a database, local disk, etc. Constructing an
|
||||
* AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping
|
||||
* file paths.
|
||||
*/
|
||||
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) :
|
||||
URLClassLoader(attachments.map(::toUrl).toTypedArray(), parent) {
|
||||
|
||||
companion object {
|
||||
|
||||
init {
|
||||
// This is required to register the AttachmentURLStreamHandlerFactory.
|
||||
URL.setURLStreamHandlerFactory(AttachmentURLStreamHandlerFactory)
|
||||
}
|
||||
|
||||
private const val `META-INF` = "meta-inf"
|
||||
private val excludeFromNoOverlapCheck = setOf(
|
||||
"manifest.mf",
|
||||
"license",
|
||||
"license.txt",
|
||||
"notice",
|
||||
"notice.txt",
|
||||
"index.list"
|
||||
)
|
||||
|
||||
private fun shouldCheckForNoOverlap(path: String): Boolean {
|
||||
if (!path.startsWith(`META-INF`)) return true
|
||||
val p = path.substring(`META-INF`.length + 1)
|
||||
if (p in excludeFromNoOverlapCheck) return false
|
||||
if (p.endsWith(".sf") || p.endsWith(".dsa")) return false
|
||||
return true
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class OverlappingAttachments(val path: String) : Exception() {
|
||||
override fun toString() = "Multiple attachments define a file at path $path"
|
||||
}
|
||||
|
||||
private fun requireNoDuplicates(attachments: List<Attachment>) {
|
||||
val classLoaderEntries = mutableSetOf<String>()
|
||||
for (attachment in attachments) {
|
||||
attachment.openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
val entry = jar.nextJarEntry ?: break
|
||||
|
||||
// We already verified that paths are not strange/game playing when we inserted the attachment
|
||||
// into the storage service. So we don't need to repeat it here.
|
||||
//
|
||||
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
|
||||
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
|
||||
//
|
||||
// Also convert to Unix path separators as all resource/class lookups will expect this.
|
||||
// If 2 entries have the same CRC, it means the same file is present in both attachments, so that is ok. TODO - Mike, wdyt?
|
||||
val path = entry.name.toLowerCase().replace('\\', '/')
|
||||
if (shouldCheckForNoOverlap(path)) {
|
||||
if (path in classLoaderEntries) throw OverlappingAttachments(path)
|
||||
classLoaderEntries.add(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
|
||||
"Attempting to load Contract Attachments downloaded from the network"
|
||||
}
|
||||
|
||||
requireNoDuplicates(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is just a factory that provides a cache to avoid constructing expensive [AttachmentsClassLoader]s.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal object AttachmentsClassLoaderBuilder {
|
||||
|
||||
private const val ATTACHMENT_CLASSLOADER_CACHE_SIZE = 1000
|
||||
|
||||
// This runs in the DJVM so it can't use caffeine.
|
||||
private val cache: MutableMap<List<SecureHash>, AttachmentsClassLoader> = createSimpleCache<List<SecureHash>, AttachmentsClassLoader>(ATTACHMENT_CLASSLOADER_CACHE_SIZE)
|
||||
.toSynchronised()
|
||||
|
||||
fun build(attachments: List<Attachment>): AttachmentsClassLoader {
|
||||
return cache.computeIfAbsent(attachments.map { it.id }.sorted()) {
|
||||
AttachmentsClassLoader(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> withAttachmentsClassloaderContext(attachments: List<Attachment>, block: (ClassLoader) -> T): T {
|
||||
|
||||
// Create classloader from the attachments.
|
||||
val transactionClassLoader = AttachmentsClassLoaderBuilder.build(attachments)
|
||||
|
||||
// Create a new serializationContext for the current Transaction.
|
||||
val transactionSerializationContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(transactionClassLoader)
|
||||
|
||||
// Deserialize all relevant classes in the transaction classloader.
|
||||
return SerializationFactory.defaultFactory.withCurrentContext(transactionSerializationContext) {
|
||||
block(transactionClassLoader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new internal "attachment" protocol.
|
||||
* This will not be exposed as an API.
|
||||
*/
|
||||
object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
|
||||
private const val attachmentScheme = "attachment"
|
||||
|
||||
// TODO - what happens if this grows too large?
|
||||
private val loadedAttachments = mutableMapOf<String, Attachment>().toSynchronised()
|
||||
|
||||
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
|
||||
return if (attachmentScheme == protocol) {
|
||||
AttachmentURLStreamHandler
|
||||
} else null
|
||||
}
|
||||
|
||||
fun toUrl(attachment: Attachment): URL {
|
||||
val id = attachment.id.toString()
|
||||
loadedAttachments[id] = attachment
|
||||
return URL(attachmentScheme, "", -1, id, AttachmentURLStreamHandler)
|
||||
}
|
||||
|
||||
private object AttachmentURLStreamHandler : URLStreamHandler() {
|
||||
override fun openConnection(url: URL): URLConnection {
|
||||
if (url.protocol != attachmentScheme) throw IOException("Cannot handle protocol: ${url.protocol}")
|
||||
val attachment = loadedAttachments[url.path] ?: throw IOException("Could not load url: $url .")
|
||||
return AttachmentURLConnection(url, attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) {
|
||||
override fun getContentLengthLong(): Long = attachment.size.toLong()
|
||||
override fun getInputStream(): InputStream = attachment.open()
|
||||
override fun connect() {
|
||||
connected = true
|
||||
}
|
||||
}
|
||||
}
|
@ -73,13 +73,6 @@ interface CheckpointSerializationContext {
|
||||
*/
|
||||
fun withClassLoader(classLoader: ClassLoader): CheckpointSerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the appropriate class loader constructed from the passed attachment identifiers.
|
||||
* (Requires the attachment storage to have been enabled).
|
||||
*/
|
||||
@Throws(MissingAttachmentsException::class)
|
||||
fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): CheckpointSerializationContext
|
||||
|
||||
/**
|
||||
* Helper method to return a new context based on this context with the given class specifically whitelisted.
|
||||
*/
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -11,10 +12,12 @@ import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.combinedHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.transactions.ContractUpgradeFilteredTransaction.FilteredComponent
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction.Companion.calculateUpgradedState
|
||||
import net.corda.core.transactions.ContractUpgradeWireTransaction.Component.*
|
||||
import net.corda.core.transactions.WireTransaction.Companion.resolveStateRefBinaryComponent
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.security.PublicKey
|
||||
@ -35,6 +38,32 @@ data class ContractUpgradeWireTransaction(
|
||||
/** Required for hiding components in [ContractUpgradeFilteredTransaction]. */
|
||||
val privacySalt: PrivacySalt = PrivacySalt()
|
||||
) : CoreTransaction() {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Runs the explicit upgrade logic.
|
||||
*/
|
||||
@CordaInternal
|
||||
internal fun <T : ContractState, S : ContractState> calculateUpgradedState(state: TransactionState<T>, upgradedContract: UpgradedContract<T, S>, upgradedContractAttachment: Attachment): TransactionState<S> {
|
||||
// TODO: if there are encumbrance states in the inputs, just copy them across without modifying
|
||||
val upgradedState: S = upgradedContract.upgrade(state.data)
|
||||
val inputConstraint = state.constraint
|
||||
val outputConstraint = when (inputConstraint) {
|
||||
is HashAttachmentConstraint -> HashAttachmentConstraint(upgradedContractAttachment.id)
|
||||
WhitelistedByZoneAttachmentConstraint -> WhitelistedByZoneAttachmentConstraint
|
||||
else -> throw IllegalArgumentException("Unsupported input contract constraint $inputConstraint")
|
||||
}
|
||||
// TODO: re-map encumbrance pointers
|
||||
return TransactionState(
|
||||
data = upgradedState,
|
||||
contract = upgradedContract::class.java.name,
|
||||
constraint = outputConstraint,
|
||||
notary = state.notary,
|
||||
encumbrance = state.encumbrance
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val inputs: List<StateRef> = serializedComponents[INPUTS.ordinal].deserialize()
|
||||
override val notary: Party by lazy { serializedComponents[NOTARY.ordinal].deserialize<Party>() }
|
||||
val legacyContractAttachmentId: SecureHash by lazy { serializedComponents[LEGACY_ATTACHMENT.ordinal].deserialize<SecureHash>() }
|
||||
@ -90,6 +119,32 @@ data class ContractUpgradeWireTransaction(
|
||||
)
|
||||
}
|
||||
|
||||
private fun upgradedContract(className: ContractClassName, classLoader: ClassLoader): UpgradedContract<ContractState, ContractState> = try {
|
||||
classLoader.loadClass(className).asSubclass(UpgradedContract::class.java as Class<UpgradedContract<ContractState, ContractState>>)
|
||||
.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a binary serialized component for a virtual output state serialised and executed with the attachments from the transaction.
|
||||
*/
|
||||
@CordaInternal
|
||||
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef): SerializedBytes<TransactionState<ContractState>> {
|
||||
val binaryInput = resolveStateRefBinaryComponent(inputs[stateRef.index], services)!!
|
||||
val legacyAttachment = services.attachments.openAttachment(legacyContractAttachmentId)
|
||||
?: throw MissingContractAttachments(emptyList())
|
||||
val upgradedAttachment = services.attachments.openAttachment(upgradedContractAttachmentId)
|
||||
?: throw MissingContractAttachments(emptyList())
|
||||
|
||||
return AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(listOf(legacyAttachment, upgradedAttachment)) { transactionClassLoader ->
|
||||
val resolvedInput = binaryInput.deserialize<TransactionState<ContractState>>()
|
||||
val upgradedContract = upgradedContract(upgradedContractClassName, transactionClassLoader)
|
||||
val outputState = calculateUpgradedState(resolvedInput, upgradedContract, upgradedAttachment)
|
||||
outputState.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
/** Constructs a filtered transaction: the inputs and the notary party are always visible, while the rest are hidden. */
|
||||
fun buildFilteredTransaction(): ContractUpgradeFilteredTransaction {
|
||||
val totalComponents = (0 until serializedComponents.size).toSet()
|
||||
@ -222,22 +277,7 @@ data class ContractUpgradeLedgerTransaction(
|
||||
* Outputs are computed by running the contract upgrade logic on input states. This is done eagerly so that the
|
||||
* transaction is verified during construction.
|
||||
*/
|
||||
override val outputs: List<TransactionState<ContractState>> = inputs.map { (state) ->
|
||||
// TODO: if there are encumbrance states in the inputs, just copy them across without modifying
|
||||
val upgradedState = upgradedContract.upgrade(state.data)
|
||||
val inputConstraint = state.constraint
|
||||
val outputConstraint = when (inputConstraint) {
|
||||
is HashAttachmentConstraint -> HashAttachmentConstraint(upgradedContractAttachment.id)
|
||||
WhitelistedByZoneAttachmentConstraint -> WhitelistedByZoneAttachmentConstraint
|
||||
else -> throw IllegalArgumentException("Unsupported input contract constraint $inputConstraint")
|
||||
}
|
||||
// TODO: re-map encumbrance pointers
|
||||
state.copy(
|
||||
data = upgradedState,
|
||||
contract = upgradedContractClassName,
|
||||
constraint = outputConstraint
|
||||
)
|
||||
}
|
||||
override val outputs: List<TransactionState<ContractState>> = inputs.map { calculateUpgradedState(it.state, upgradedContract, upgradedContractAttachment) }
|
||||
|
||||
/** The required signers are the set of all input states' participants. */
|
||||
override val requiredSigningKeys: Set<PublicKey>
|
||||
|
@ -1,22 +1,21 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.internal.checkMinimumPlatformVersion
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.warnOnce
|
||||
import java.util.*
|
||||
import java.util.function.Predicate
|
||||
import kotlin.collections.HashSet
|
||||
import net.corda.core.utilities.warnOnce
|
||||
|
||||
/**
|
||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||
@ -34,7 +33,7 @@ import net.corda.core.utilities.warnOnce
|
||||
// DOCSTART 1
|
||||
@KeepForDJVM
|
||||
@CordaSerializable
|
||||
data class LedgerTransaction @JvmOverloads constructor(
|
||||
data class LedgerTransaction private constructor(
|
||||
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
|
||||
override val inputs: List<StateAndRef<ContractState>>,
|
||||
override val outputs: List<TransactionState<ContractState>>,
|
||||
@ -47,9 +46,38 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
override val notary: Party?,
|
||||
val timeWindow: TimeWindow?,
|
||||
val privacySalt: PrivacySalt,
|
||||
private val networkParameters: NetworkParameters? = null,
|
||||
override val references: List<StateAndRef<ContractState>> = emptyList()
|
||||
private val networkParameters: NetworkParameters?,
|
||||
override val references: List<StateAndRef<ContractState>>,
|
||||
val componentGroups: List<ComponentGroup>?,
|
||||
val resolvedInputBytes: List<SerializedStateAndRef>?,
|
||||
val resolvedReferenceBytes: List<SerializedStateAndRef>?
|
||||
) : FullTransaction() {
|
||||
|
||||
@Deprecated("Client code should not instantiate LedgerTransaction.")
|
||||
constructor(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
attachments: List<Attachment>,
|
||||
id: SecureHash,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt
|
||||
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, null, emptyList(), null, null, null)
|
||||
|
||||
@Deprecated("Client code should not instantiate LedgerTransaction.")
|
||||
constructor(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
attachments: List<Attachment>,
|
||||
id: SecureHash,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters?
|
||||
) : this(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, emptyList(), null, null, null)
|
||||
|
||||
//DOCEND 1
|
||||
init {
|
||||
checkBaseInvariants()
|
||||
@ -58,19 +86,25 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
checkEncumbrancesValid()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<LedgerTransaction>()
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try<Class<out Contract>> {
|
||||
return Try.on {
|
||||
(classLoader ?: this::class.java.classLoader)
|
||||
.loadClass(className)
|
||||
.asSubclass(Contract::class.java)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private val logger = loggerFor<LedgerTransaction>()
|
||||
|
||||
private fun stateToContractClass(state: TransactionState<ContractState>): Try<Class<out Contract>> {
|
||||
return contractClassFor(state.contract, state.data::class.java.classLoader)
|
||||
}
|
||||
@CordaInternal
|
||||
internal fun makeLedgerTransaction(
|
||||
inputs: List<StateAndRef<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>,
|
||||
commands: List<CommandWithParties<CommandData>>,
|
||||
attachments: List<Attachment>,
|
||||
id: SecureHash,
|
||||
notary: Party?,
|
||||
timeWindow: TimeWindow?,
|
||||
privacySalt: PrivacySalt,
|
||||
networkParameters: NetworkParameters?,
|
||||
references: List<StateAndRef<ContractState>>,
|
||||
componentGroups: List<ComponentGroup>,
|
||||
resolvedInputBytes: List<SerializedStateAndRef>,
|
||||
resolvedReferenceBytes: List<SerializedStateAndRef>
|
||||
) = LedgerTransaction(inputs, outputs, commands, attachments, id, notary, timeWindow, privacySalt, networkParameters, references, componentGroups, resolvedInputBytes, resolvedReferenceBytes)
|
||||
}
|
||||
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
@ -88,6 +122,12 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
|
||||
/**
|
||||
* Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.
|
||||
|
||||
* The contract verification logic is run in a custom [AttachmentsClassLoader] created for the current transaction.
|
||||
* This classloader is only used during verification and does not leak to the client code.
|
||||
*
|
||||
* The reason for this is that classes (contract states) deserialized in this classloader would actually be a different type from what
|
||||
* the calling code would expect.
|
||||
*
|
||||
* @throws TransactionVerificationException if anything goes wrong.
|
||||
*/
|
||||
@ -95,12 +135,17 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
fun verify() {
|
||||
val contractAttachmentsByContract: Map<ContractClassName, ContractAttachment> = getUniqueContractAttachmentsByContract()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract()
|
||||
verifyConstraintsValidity(contractAttachmentsByContract)
|
||||
verifyConstraints(contractAttachmentsByContract)
|
||||
verifyContracts()
|
||||
AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(this.attachments) { transactionClassLoader ->
|
||||
|
||||
val internalTx = createInternalLedgerTransaction()
|
||||
|
||||
// TODO - verify for version downgrade
|
||||
validatePackageOwnership(contractAttachmentsByContract)
|
||||
validateStatesAgainstContract(internalTx)
|
||||
verifyConstraintsValidity(internalTx, contractAttachmentsByContract, transactionClassLoader)
|
||||
verifyConstraints(internalTx, contractAttachmentsByContract)
|
||||
verifyContracts(internalTx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,7 +178,7 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
*
|
||||
* A warning will be written to the log if any mismatch is detected.
|
||||
*/
|
||||
private fun validateStatesAgainstContract() = allStates.forEach(::validateStateAgainstContract)
|
||||
private fun validateStatesAgainstContract(internalTx: LedgerTransaction) = internalTx.allStates.forEach { validateStateAgainstContract(it) }
|
||||
|
||||
private fun validateStateAgainstContract(state: TransactionState<ContractState>) {
|
||||
state.data.requiredContractClassName?.let { requiredContractClassName ->
|
||||
@ -150,25 +195,25 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
* * Constraints should be one of the valid supported ones.
|
||||
* * Constraints should propagate correctly if not marked otherwise.
|
||||
*/
|
||||
private fun verifyConstraintsValidity(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
private fun verifyConstraintsValidity(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>, transactionClassLoader: ClassLoader) {
|
||||
// First check that the constraints are valid.
|
||||
for (state in allStates) {
|
||||
for (state in internalTx.allStates) {
|
||||
checkConstraintValidity(state)
|
||||
}
|
||||
|
||||
// Group the inputs and outputs by contract, and for each contract verify the constraints propagation logic.
|
||||
// This is not required for reference states as there is nothing to propagate.
|
||||
val inputContractGroups = inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = outputs.groupBy { it.contract }
|
||||
val inputContractGroups = internalTx.inputs.groupBy { it.state.contract }
|
||||
val outputContractGroups = internalTx.outputs.groupBy { it.contract }
|
||||
|
||||
for (contractClassName in (inputContractGroups.keys + outputContractGroups.keys)) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation()) {
|
||||
if (contractClassName.contractHasAutomaticConstraintPropagation(transactionClassLoader)) {
|
||||
// Verify that the constraints of output states have at least the same level of restriction as the constraints of the corresponding input states.
|
||||
val inputConstraints = inputContractGroups[contractClassName]?.map { it.state.constraint }?.toSet()
|
||||
val outputConstraints = outputContractGroups[contractClassName]?.map { it.constraint }?.toSet()
|
||||
outputConstraints?.forEach { outputConstraint ->
|
||||
inputConstraints?.forEach { inputConstraint ->
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!! ))) {
|
||||
if (!(outputConstraint.canBeTransitionedFrom(inputConstraint, contractAttachmentsByContract[contractClassName]!!))) {
|
||||
throw TransactionVerificationException.ConstraintPropagationRejection(id, contractClassName, inputConstraint, outputConstraint)
|
||||
}
|
||||
}
|
||||
@ -186,8 +231,8 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
*
|
||||
* @throws TransactionVerificationException if the constraints fail to verify
|
||||
*/
|
||||
private fun verifyConstraints(contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
for (state in allStates) {
|
||||
private fun verifyConstraints(internalTx: LedgerTransaction, contractAttachmentsByContract: Map<ContractClassName, ContractAttachment>) {
|
||||
for (state in internalTx.allStates) {
|
||||
val contractAttachment = contractAttachmentsByContract[state.contract]
|
||||
?: throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract)
|
||||
|
||||
@ -226,38 +271,64 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader): Class<out Contract> = try {
|
||||
classLoader.loadClass(className).asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, className, e)
|
||||
}
|
||||
|
||||
private fun createInternalLedgerTransaction(): LedgerTransaction {
|
||||
return if (resolvedInputBytes != null && resolvedReferenceBytes != null && componentGroups != null) {
|
||||
|
||||
// Deserialize all relevant classes in the transaction classloader.
|
||||
val resolvedDeserializedInputs = resolvedInputBytes.map { StateAndRef(it.serializedState.deserialize(), it.ref) }
|
||||
val resolvedDeserializedReferences = resolvedReferenceBytes.map { StateAndRef(it.serializedState.deserialize(), it.ref) }
|
||||
val deserializedOutputs = deserialiseComponentGroup(componentGroups, TransactionState::class, ComponentGroupEnum.OUTPUTS_GROUP, forceDeserialize = true)
|
||||
val deserializedCommands = deserialiseCommands(this.componentGroups, forceDeserialize = true)
|
||||
val authenticatedArgs = deserializedCommands.map { cmd ->
|
||||
val parties = commands.find { it.value.javaClass.name == cmd.value.javaClass.name }!!.signingParties
|
||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||
}
|
||||
|
||||
val ledgerTransactionToVerify = this.copy(
|
||||
inputs = resolvedDeserializedInputs,
|
||||
outputs = deserializedOutputs,
|
||||
commands = authenticatedArgs,
|
||||
references = resolvedDeserializedReferences)
|
||||
|
||||
ledgerTransactionToVerify
|
||||
} else {
|
||||
// This branch is only present for backwards compatibility.
|
||||
// TODO - it should be removed once the constructor of LedgerTransaction is no longer public api.
|
||||
logger.warn("The LedgerTransaction should not be instantiated directly from client code. Please use WireTransaction.toLedgerTransaction. The result of the verify method might not be accurate.")
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
private fun verifyContracts() = inputAndOutputStates.forEach { ts ->
|
||||
val contractClass = getContractClass(ts)
|
||||
val contract = createContractInstance(contractClass)
|
||||
private fun verifyContracts(internalTx: LedgerTransaction) {
|
||||
val contractClasses = (internalTx.inputs.map { it.state } + internalTx.outputs).toSet()
|
||||
.map { it.contract to contractClassFor(it.contract, it.data.javaClass.classLoader) }
|
||||
|
||||
try {
|
||||
contract.verify(this)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(id, contract, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain the contract class from the class name, wrapping any exception as a [ContractCreationError]
|
||||
private fun getContractClass(ts: TransactionState<ContractState>): Class<out Contract> =
|
||||
try {
|
||||
(ts.data::class.java.classLoader ?: this::class.java.classLoader)
|
||||
.loadClass(ts.contract)
|
||||
.asSubclass(Contract::class.java)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, ts.contract, e)
|
||||
}
|
||||
|
||||
// Obtain an instance of the contract class, wrapping any exception as a [ContractCreationError]
|
||||
private fun createContractInstance(contractClass: Class<out Contract>): Contract =
|
||||
val contractInstances = contractClasses.map { (contractClassName, contractClass) ->
|
||||
try {
|
||||
contractClass.newInstance()
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e)
|
||||
throw TransactionVerificationException.ContractCreationError(id, contractClassName, e)
|
||||
}
|
||||
}
|
||||
|
||||
contractInstances.forEach { contract ->
|
||||
try {
|
||||
contract.verify(internalTx)
|
||||
} catch (e: Exception) {
|
||||
throw TransactionVerificationException.ContractRejection(id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
@ -286,7 +357,8 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
// b) the number of outputs can contain the encumbrance
|
||||
// c) the bi-directionality (full cycle) property is satisfied
|
||||
// d) encumbered output states are assigned to the same notary.
|
||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }
|
||||
.map { Pair(it.index, it.value.encumbrance!!) }
|
||||
if (!statesAndEncumbrance.isEmpty()) {
|
||||
checkBidirectionalOutputEncumbrances(statesAndEncumbrance)
|
||||
checkNotariesOutputEncumbrance(statesAndEncumbrance)
|
||||
|
@ -6,14 +6,14 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.LazyMappedList
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.internal.deserialiseCommands
|
||||
import net.corda.core.internal.deserialiseComponentGroup
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.lazyMapped
|
||||
import java.security.PublicKey
|
||||
import java.util.function.Predicate
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Implemented by [WireTransaction] and [FilteredTransaction]. A TraversableTransaction allows you to iterate
|
||||
@ -23,27 +23,27 @@ import kotlin.reflect.KClass
|
||||
*/
|
||||
abstract class TraversableTransaction(open val componentGroups: List<ComponentGroup>) : CoreTransaction() {
|
||||
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
|
||||
val attachments: List<SecureHash> = deserialiseComponentGroup(SecureHash::class, ATTACHMENTS_GROUP)
|
||||
val attachments: List<SecureHash> = deserialiseComponentGroup(componentGroups, SecureHash::class, ATTACHMENTS_GROUP)
|
||||
|
||||
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
||||
override val inputs: List<StateRef> = deserialiseComponentGroup(StateRef::class, INPUTS_GROUP)
|
||||
override val inputs: List<StateRef> = deserialiseComponentGroup(componentGroups, StateRef::class, INPUTS_GROUP)
|
||||
|
||||
/** Pointers to reference states, identified by (tx identity hash, output index). */
|
||||
override val references: List<StateRef> = deserialiseComponentGroup(StateRef::class, REFERENCES_GROUP)
|
||||
override val references: List<StateRef> = deserialiseComponentGroup(componentGroups, StateRef::class, REFERENCES_GROUP)
|
||||
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(TransactionState::class, OUTPUTS_GROUP, attachmentsContext = true)
|
||||
override val outputs: List<TransactionState<ContractState>> = deserialiseComponentGroup(componentGroups, TransactionState::class, OUTPUTS_GROUP)
|
||||
|
||||
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
|
||||
val commands: List<Command<*>> = deserialiseCommands()
|
||||
val commands: List<Command<*>> = deserialiseCommands(componentGroups)
|
||||
|
||||
override val notary: Party? = let {
|
||||
val notaries: List<Party> = deserialiseComponentGroup(Party::class, NOTARY_GROUP)
|
||||
val notaries: List<Party> = deserialiseComponentGroup(componentGroups, Party::class, NOTARY_GROUP)
|
||||
check(notaries.size <= 1) { "Invalid Transaction. More than 1 notary party detected." }
|
||||
notaries.firstOrNull()
|
||||
}
|
||||
|
||||
val timeWindow: TimeWindow? = let {
|
||||
val timeWindows: List<TimeWindow> = deserialiseComponentGroup(TimeWindow::class, TIMEWINDOW_GROUP)
|
||||
val timeWindows: List<TimeWindow> = deserialiseComponentGroup(componentGroups, TimeWindow::class, TIMEWINDOW_GROUP)
|
||||
check(timeWindows.size <= 1) { "Invalid Transaction. More than 1 time-window detected." }
|
||||
timeWindows.firstOrNull()
|
||||
}
|
||||
@ -66,65 +66,6 @@ abstract class TraversableTransaction(open val componentGroups: List<ComponentGr
|
||||
timeWindow?.let { result += listOf(it) }
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper function to return a meaningful exception if deserialisation of a component fails.
|
||||
private fun <T : Any> deserialiseComponentGroup(clazz: KClass<T>,
|
||||
groupEnum: ComponentGroupEnum,
|
||||
attachmentsContext: Boolean = false): List<T> {
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == groupEnum.ordinal }
|
||||
|
||||
if (group == null || group.components.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// If the componentGroup is a [LazyMappedList] it means that the original deserialized version is already available.
|
||||
val components = group.components
|
||||
if (components is LazyMappedList<*, OpaqueBytes>) {
|
||||
return components.originalList as List<T>
|
||||
}
|
||||
|
||||
val factory = SerializationFactory.defaultFactory
|
||||
val context = factory.defaultContext.let { if (attachmentsContext) it.withAttachmentsClassLoader(attachments) else it }
|
||||
|
||||
return components.lazyMapped { component, internalIndex ->
|
||||
try {
|
||||
factory.deserialize(component, clazz.java , context)
|
||||
} catch (e: MissingAttachmentsException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Malformed transaction, $groupEnum at index $internalIndex cannot be deserialised", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to deserialise Commands from its two groups:
|
||||
// COMMANDS_GROUP which contains the CommandData part
|
||||
// and SIGNERS_GROUP which contains the Signers part.
|
||||
private fun deserialiseCommands(): List<Command<*>> {
|
||||
// TODO: we could avoid deserialising unrelated signers.
|
||||
// However, current approach ensures the transaction is not malformed
|
||||
// and it will throw if any of the signers objects is not List of public keys).
|
||||
val signersList: List<List<PublicKey>> = uncheckedCast(deserialiseComponentGroup(List::class, SIGNERS_GROUP))
|
||||
val commandDataList: List<CommandData> = deserialiseComponentGroup(CommandData::class, COMMANDS_GROUP, attachmentsContext = true)
|
||||
val group = componentGroups.firstOrNull { it.groupIndex == COMMANDS_GROUP.ordinal }
|
||||
return if (group is FilteredComponentGroup) {
|
||||
check(commandDataList.size <= signersList.size) {
|
||||
"Invalid Transaction. Less Signers (${signersList.size}) than CommandData (${commandDataList.size}) objects"
|
||||
}
|
||||
val componentHashes = group.components.mapIndexed { index, component -> componentHash(group.nonces[index], component) }
|
||||
val leafIndices = componentHashes.map { group.partialMerkleTree.leafIndex(it) }
|
||||
if (leafIndices.isNotEmpty())
|
||||
check(leafIndices.max()!! < signersList.size) { "Invalid Transaction. A command with no corresponding signer detected" }
|
||||
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[leafIndices[index]]) }
|
||||
} else {
|
||||
// It is a WireTransaction
|
||||
// or a FilteredTransaction with no Commands (in which case group is null).
|
||||
check(commandDataList.size == signersList.size) {
|
||||
"Invalid Transaction. Sizes of CommandData (${commandDataList.size}) and Signers (${signersList.size}) do not match"
|
||||
}
|
||||
commandDataList.lazyMapped { commandData, index -> Command(commandData, signersList[index]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
@ -10,6 +11,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction.Component.*
|
||||
@ -75,6 +77,20 @@ data class NotaryChangeWireTransaction(
|
||||
@DeleteForDJVM
|
||||
fun resolve(services: ServiceHub, sigs: List<TransactionSignature>) = resolve(services as ServicesForResolution, sigs)
|
||||
|
||||
/**
|
||||
* This should return a serialized virtual output state, that will be used to verify spending transactions.
|
||||
* The binary output should not depend on the classpath of the node that is verifying the transaction.
|
||||
*
|
||||
* Ideally the serialization engine would support partial deserialization so that only the Notary ( and the encumbrance can be replaced from the binary input state)
|
||||
*
|
||||
*
|
||||
* TODO - currently this uses the main classloader.
|
||||
*/
|
||||
@CordaInternal
|
||||
internal fun resolveOutputComponent(services: ServicesForResolution, stateRef: StateRef): SerializedBytes<TransactionState<ContractState>> {
|
||||
return services.loadState(stateRef).serialize()
|
||||
}
|
||||
|
||||
enum class Component {
|
||||
INPUTS, NOTARY, NEW_NOTARY
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ open class TransactionBuilder @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
// The final step is to resolve AutomaticPlaceholderConstraint.
|
||||
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(serializationContext?.deserializationClassLoader)
|
||||
val automaticConstraintPropagation = contractClassName.contractHasAutomaticConstraintPropagation(inputsAndOutputs.first().data::class.java.classLoader)
|
||||
|
||||
// When automaticConstraintPropagation is disabled for a contract, output states must an explicit Constraint.
|
||||
require(automaticConstraintPropagation) { "Contract $contractClassName was marked with @NoConstraintPropagation, which means the constraint of the output states has to be set explicitly." }
|
||||
|
@ -7,11 +7,15 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.ComponentGroupEnum.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.SerializedStateAndRef
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.lazyMapped
|
||||
@ -99,7 +103,7 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
return toLedgerTransactionInternal(
|
||||
resolveIdentity = { services.identityService.partyFromKey(it) },
|
||||
resolveAttachment = { services.attachments.openAttachment(it) },
|
||||
resolveStateRef = { services.loadState(it) },
|
||||
resolveStateRefComponent = { resolveStateRefBinaryComponent(it, services) },
|
||||
networkParameters = services.networkParameters
|
||||
)
|
||||
}
|
||||
@ -119,13 +123,14 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
@Suppress("UNUSED_PARAMETER") resolveContractAttachment: (TransactionState<ContractState>) -> AttachmentId?
|
||||
): LedgerTransaction {
|
||||
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, null)
|
||||
// This reverts to serializing the resolved transaction state.
|
||||
return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, { stateRef -> resolveStateRef(stateRef)?.serialize() }, null)
|
||||
}
|
||||
|
||||
private fun toLedgerTransactionInternal(
|
||||
resolveIdentity: (PublicKey) -> Party?,
|
||||
resolveAttachment: (SecureHash) -> Attachment?,
|
||||
resolveStateRef: (StateRef) -> TransactionState<*>?,
|
||||
resolveStateRefComponent: (StateRef) -> SerializedBytes<TransactionState<ContractState>>?,
|
||||
networkParameters: NetworkParameters?
|
||||
): LedgerTransaction {
|
||||
// Look up public keys to authenticated identities.
|
||||
@ -133,20 +138,38 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
val parties = cmd.signers.mapNotNull { pk -> resolveIdentity(pk) }
|
||||
CommandWithParties(cmd.signers, parties, cmd.value)
|
||||
}
|
||||
val resolvedInputs = inputs.lazyMapped { ref, _ ->
|
||||
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
|
||||
|
||||
val resolvedInputBytes = inputs.map { ref ->
|
||||
SerializedStateAndRef(resolveStateRefComponent(ref)
|
||||
?: throw TransactionResolutionException(ref.txhash), ref)
|
||||
}
|
||||
val resolvedReferences = references.lazyMapped { ref, _ ->
|
||||
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
|
||||
val resolvedInputs = resolvedInputBytes.lazyMapped { (serialized, ref), _ ->
|
||||
StateAndRef(serialized.deserialize(), ref)
|
||||
}
|
||||
|
||||
val resolvedReferenceBytes = references.map { ref ->
|
||||
SerializedStateAndRef(resolveStateRefComponent(ref)
|
||||
?: throw TransactionResolutionException(ref.txhash), ref)
|
||||
}
|
||||
val resolvedReferences = resolvedReferenceBytes.lazyMapped { (serialized, ref), _ ->
|
||||
StateAndRef(serialized.deserialize(), ref)
|
||||
}
|
||||
|
||||
val attachments = attachments.lazyMapped { att, _ ->
|
||||
resolveAttachment(att) ?: throw AttachmentResolutionException(att)
|
||||
}
|
||||
val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences)
|
||||
checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: 10485760)
|
||||
|
||||
val ltx = LedgerTransaction.makeLedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences, componentGroups, resolvedInputBytes, resolvedReferenceBytes)
|
||||
|
||||
checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: DEFAULT_MAX_TX_SIZE)
|
||||
|
||||
return ltx
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic function that checks if the transaction is below the maximum allowed size.
|
||||
* It uses the binary representation of transactions.
|
||||
*/
|
||||
private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) {
|
||||
var remainingTransactionSize = maxTransactionSize
|
||||
|
||||
@ -164,9 +187,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
// it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id.
|
||||
ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) }
|
||||
|
||||
// TODO - these can be optimized by creating a LazyStateAndRef class, that just stores (a pointer) the serialized output componentGroup from the previous transaction.
|
||||
minus(ltx.references.serialize().size)
|
||||
minus(ltx.inputs.serialize().size)
|
||||
minus(ltx.resolvedInputBytes!!.sumBy { it.serializedState.size })
|
||||
minus(ltx.resolvedReferenceBytes!!.sumBy { it.serializedState.size })
|
||||
|
||||
// For Commands and outputs we can use the component groups as they are already serialized.
|
||||
minus(componentGroupSize(COMMANDS_GROUP))
|
||||
@ -253,6 +275,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_MAX_TX_SIZE = 10485760
|
||||
|
||||
/**
|
||||
* Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required
|
||||
* for backwards compatibility purposes.
|
||||
@ -281,6 +305,28 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
|
||||
if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(SIGNERS_GROUP.ordinal, commands.map { it.signers }.lazyMapped(serialize)))
|
||||
return componentGroupMap
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main logic that knows how to retrieve the binary representation of [StateRef]s.
|
||||
*
|
||||
* For [ContractUpgradeWireTransaction] or [NotaryChangeWireTransaction] it knows how to recreate the output state in the correct classloader independent of the node's classpath.
|
||||
*/
|
||||
@CordaInternal
|
||||
fun resolveStateRefBinaryComponent(stateRef: StateRef, services: ServicesForResolution): SerializedBytes<TransactionState<ContractState>>? {
|
||||
return if (services is ServiceHub) {
|
||||
val coreTransaction = services.validatedTransactions.getTransaction(stateRef.txhash)?.coreTransaction
|
||||
?: throw TransactionResolutionException(stateRef.txhash)
|
||||
when (coreTransaction) {
|
||||
is WireTransaction -> coreTransaction.componentGroups.firstOrNull { it.groupIndex == ComponentGroupEnum.OUTPUTS_GROUP.ordinal }?.components?.get(stateRef.index) as SerializedBytes<TransactionState<ContractState>>?
|
||||
is ContractUpgradeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef)
|
||||
is NotaryChangeWireTransaction -> coreTransaction.resolveOutputComponent(services, stateRef)
|
||||
else -> throw UnsupportedOperationException("Attempting to resolve input ${stateRef.index} of a ${coreTransaction.javaClass} transaction. This is not supported.")
|
||||
}
|
||||
} else {
|
||||
// For backwards compatibility revert to using the node classloader.
|
||||
services.loadState(stateRef).serialize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteForDJVM
|
||||
|
@ -68,8 +68,8 @@ sealed class ByteSequence(private val _bytes: ByteArray, val offset: Int, val si
|
||||
* This method cannot be used to get bytes before [offset] or after [offset]+[size], and never makes a new array.
|
||||
*/
|
||||
fun slice(start: Int = 0, end: Int = size): ByteBuffer {
|
||||
require(start >= 0)
|
||||
require(end >= 0)
|
||||
require(start >= 0) { "Starting index must be greater than or equal to 0" }
|
||||
require(end >= 0){"End index must be greater or equal to 0"}
|
||||
val clampedStart = min(start, size)
|
||||
val clampedEnd = min(end, size)
|
||||
return ByteBuffer.wrap(_bytes, offset + clampedStart, max(0, clampedEnd - clampedStart)).asReadOnlyBuffer()
|
||||
@ -155,7 +155,7 @@ open class OpaqueBytes(bytes: ByteArray) : ByteSequence(bytes, 0, bytes.size) {
|
||||
}
|
||||
|
||||
init {
|
||||
require(bytes.isNotEmpty())
|
||||
require(bytes.isNotEmpty()) { "Byte Array must not be empty" }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,7 +193,7 @@ fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
@KeepForDJVM
|
||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, offset: Int, size: Int) : ByteSequence(bytes, offset, size) {
|
||||
init {
|
||||
require(offset >= 0 && offset < bytes.size)
|
||||
require(size >= 0 && offset + size <= bytes.size)
|
||||
require(offset >= 0 && offset < bytes.size) { "Offset must be greater than or equal to 0, and less than the size of the backing array" }
|
||||
require(size >= 0 && offset + size <= bytes.size) { "Sub-sequence size must be greater than or equal to 0, and less than the size of the backing array" }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.internal.LazyMappedList
|
||||
import net.corda.core.internal.concurrent.get
|
||||
import net.corda.core.internal.createSimpleCache
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
@ -149,9 +150,7 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
|
||||
fun <T, U> List<T>.lazyMapped(transform: (T, Int) -> U): List<U> = LazyMappedList(this, transform)
|
||||
|
||||
private const val MAX_SIZE = 100
|
||||
private val warnings = Collections.newSetFromMap(object : LinkedHashMap<String, Boolean>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?) = size > MAX_SIZE
|
||||
})
|
||||
private val warnings = Collections.newSetFromMap(createSimpleCache<String, Boolean>(MAX_SIZE))
|
||||
|
||||
/**
|
||||
* Utility to help log a warning message only once.
|
||||
@ -163,4 +162,4 @@ fun Logger.warnOnce(warning: String) {
|
||||
warnings.add(warning)
|
||||
this.warn(warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,18 +14,13 @@ import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.makeUnique
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
|
||||
class AttachmentTests : WithMockNet {
|
||||
companion object {
|
||||
@ -46,7 +41,7 @@ class AttachmentTests : WithMockNet {
|
||||
@Test
|
||||
fun `download and store`() {
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val id = aliceNode.importAttachment(fakeAttachment())
|
||||
val id = aliceNode.importAttachment(fakeAttachment("file1.txt", "Some useful content"))
|
||||
|
||||
// Get node one to run a flow to fetch it and insert it.
|
||||
assert.that(
|
||||
@ -87,7 +82,7 @@ class AttachmentTests : WithMockNet {
|
||||
val badAlice = badAliceNode.info.singleIdentity()
|
||||
|
||||
// Insert an attachment into node zero's store directly.
|
||||
val attachment = fakeAttachment()
|
||||
val attachment = fakeAttachment("file1.txt", "Some useful content")
|
||||
val id = badAliceNode.importAttachment(attachment)
|
||||
|
||||
// Corrupt its store.
|
||||
@ -134,18 +129,6 @@ class AttachmentTests : WithMockNet {
|
||||
}
|
||||
}).apply { registerInitiatedFlow(FetchAttachmentsResponse::class.java) }
|
||||
|
||||
private fun fakeAttachment(): ByteArray =
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
JarOutputStream(baos).use { jos ->
|
||||
jos.putNextEntry(ZipEntry("file1.txt"))
|
||||
jos.writer().apply {
|
||||
append("Some useful content")
|
||||
flush()
|
||||
}
|
||||
jos.closeEntry()
|
||||
}
|
||||
baos.toByteArray()
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Operations
|
||||
|
@ -0,0 +1,98 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoaderBuilder
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.DummyContractBackdoor
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
import java.net.URL
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentsClassLoaderSerializationTests {
|
||||
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderSerializationTests::class.java.getResource("isolated.jar")
|
||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
@Test
|
||||
fun `Can serialize and deserialize with an attachment classloader`() {
|
||||
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
||||
|
||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
val serialisedState = AttachmentsClassLoaderBuilder.withAttachmentsClassloaderContext(arrayOf(isolatedId, att1, att2).map { storage.openAttachment(it)!! }) { classLoader ->
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classLoader)
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||
|
||||
val txt = IOUtils.toString(classLoader.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||
assertEquals("some data", txt)
|
||||
|
||||
val state = (contract as DummyContractBackdoor).generateInitial(MEGA_CORP.ref(1), 1, DUMMY_NOTARY).outputStates().first()
|
||||
val serialisedState = state.serialize()
|
||||
|
||||
val state1 = serialisedState.deserialize()
|
||||
assertEquals(state, state1)
|
||||
serialisedState
|
||||
}
|
||||
|
||||
assertFailsWith<NotSerializableException> {
|
||||
serialisedState.deserialize()
|
||||
}
|
||||
}
|
||||
|
||||
// These tests are not Attachment specific. Should they be removed?
|
||||
@Test
|
||||
fun `test serialization of SecureHash`() {
|
||||
val secureHash = SecureHash.randomSHA256()
|
||||
val bytes = secureHash.serialize()
|
||||
val copiedSecuredHash = bytes.deserialize()
|
||||
|
||||
assertEquals(secureHash, copiedSecuredHash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test serialization of OpaqueBytes`() {
|
||||
val opaqueBytes = OpaqueBytes("0123456789".toByteArray())
|
||||
val bytes = opaqueBytes.serialize()
|
||||
val copiedOpaqueBytes = bytes.deserialize()
|
||||
|
||||
assertEquals(opaqueBytes, copiedOpaqueBytes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test serialization of sub-sequence OpaqueBytes`() {
|
||||
val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2)
|
||||
val bytes = bytesSequence.serialize()
|
||||
val copiedBytesSequence = bytes.deserialize()
|
||||
|
||||
assertEquals(bytesSequence, copiedBytesSequence)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,102 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URL
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentsClassLoaderTests {
|
||||
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH: URL = AttachmentsClassLoaderTests::class.java.getResource("isolated.jar")
|
||||
private const val ISOLATED_CONTRACT_CLASS_NAME = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
|
||||
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
|
||||
ByteArrayOutputStream().use {
|
||||
attachment.extractFile(filepath, it)
|
||||
return it.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
@Test
|
||||
fun `Loading AnotherDummyContract without using the AttachmentsClassLoader fails`() {
|
||||
assertFailsWith<ClassNotFoundException> {
|
||||
Class.forName(ISOLATED_CONTRACT_CLASS_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dynamically load AnotherDummyContract from isolated contracts jar using the AttachmentsClassLoader`() {
|
||||
val isolatedId = storage.importAttachment(ISOLATED_CONTRACTS_JAR_PATH.openStream(), "app", "isolated.jar")
|
||||
|
||||
val classloader = AttachmentsClassLoader(listOf(storage.openAttachment(isolatedId)!!))
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, classloader)
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
assertEquals("helloworld", contract.declaredField<Any?>("magicString").value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Load text resources from AttachmentsClassLoader`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"), Charsets.UTF_8.name())
|
||||
assertEquals("some data", txt)
|
||||
|
||||
val txt1 = IOUtils.toString(cl.getResourceAsStream("file2.txt"), Charsets.UTF_8.name())
|
||||
assertEquals("some other data", txt1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test overlapping file exception`() {
|
||||
val att1 = storage.importAttachment(fakeAttachment("file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("file1.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
assertFailsWith(AttachmentsClassLoader.Companion.OverlappingAttachments::class) {
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `No overlapping exception thrown on certain META-INF files`() {
|
||||
listOf("meta-inf/manifest.mf", "meta-inf/license", "meta-inf/test.dsa", "meta-inf/test.sf").forEach { path ->
|
||||
val att1 = storage.importAttachment(fakeAttachment(path, "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment(path, "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
AttachmentsClassLoader(arrayOf(att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check platform independent path handling in attachment jars`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att1 = storage.importAttachment(fakeAttachment("/folder1/foldera/file1.txt", "some data").inputStream(), "app", "file1.jar")
|
||||
val att2 = storage.importAttachment(fakeAttachment("\\folder1\\folderb\\file2.txt", "some other data").inputStream(), "app", "file2.jar")
|
||||
|
||||
val data1a = readAttachment(storage.openAttachment(att1)!!, "/folder1/foldera/file1.txt")
|
||||
assertArrayEquals("some data".toByteArray(), data1a)
|
||||
|
||||
val data1b = readAttachment(storage.openAttachment(att1)!!, "\\folder1\\foldera\\file1.txt")
|
||||
assertArrayEquals("some data".toByteArray(), data1b)
|
||||
|
||||
val data2a = readAttachment(storage.openAttachment(att2)!!, "\\folder1\\folderb\\file2.txt")
|
||||
assertArrayEquals("some other data".toByteArray(), data2a)
|
||||
|
||||
val data2b = readAttachment(storage.openAttachment(att2)!!, "/folder1/folderb/file2.txt")
|
||||
assertArrayEquals("some other data".toByteArray(), data2b)
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.createWireTransaction
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -118,7 +119,8 @@ class TransactionTests {
|
||||
val commands = emptyList<CommandWithParties<CommandData>>()
|
||||
val attachments = listOf<Attachment>(ContractAttachment(rigorousMock<Attachment>().also {
|
||||
doReturn(SecureHash.zeroHash).whenever(it).id
|
||||
}, DummyContract.PROGRAM_ID))
|
||||
doReturn(fakeAttachment("nothing", "nada").inputStream()).whenever(it).open()
|
||||
}, DummyContract.PROGRAM_ID, uploader = "app"))
|
||||
val id = SecureHash.randomSHA256()
|
||||
val timeWindow: TimeWindow? = null
|
||||
val privacySalt = PrivacySalt()
|
||||
|
BIN
core/src/test/resources/net/corda/core/transactions/isolated.jar
Normal file
BIN
core/src/test/resources/net/corda/core/transactions/isolated.jar
Normal file
Binary file not shown.
@ -7,6 +7,11 @@ release, see :doc:`upgrade-notes`.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
* Deprecated `SerializationContext.withAttachmentsClassLoader`. This functionality has always been disabled by flags
|
||||
and there is no reason for a CorDapp developer to use it. It is just an internal implementation detail of Corda.
|
||||
|
||||
* Deprecated the `LedgerTransaction` constructor. No client code should call it directly. LedgerTransactions can be created from WireTransactions if required.
|
||||
|
||||
* Introduced new optional network bootstrapper command line options (--register-package-owner, --unregister-package-owner)
|
||||
to register/unregister a java package namespace with an associated owner in the network parameter packageOwnership whitelist.
|
||||
|
||||
|
@ -48,8 +48,10 @@ Here are the contents of the ``reference.conf`` file for Corda Enterprise:
|
||||
|
||||
Fields
|
||||
------
|
||||
The available config fields are listed below. ``baseDirectory`` is available as a substitution value and contains the
|
||||
absolute path to the node's base directory.
|
||||
|
||||
.. note:: All fields can be used with placeholders for environment variables. For example: ``${NODE_TRUST_STORE_PASSWORD}`` would be replaced by the contents of environment variable ``NODE_TRUST_STORE_PASSWORD``. See: `Hiding Sensitive Data`_
|
||||
|
||||
The available config fields are listed below.
|
||||
|
||||
:myLegalName: The legal identity of the node. This acts as a human-readable alias to the node's public key and can be used with
|
||||
the network map to look up the node's info. This is the name that is used in the node's certificates (either when requesting them
|
||||
@ -395,3 +397,47 @@ Together with the above configuration `tlsCertCrlIssuer` option needs to be set
|
||||
|
||||
This set-up ensures that the TLS-level certificates are embedded with the CRL distribution point referencing the CRL issued by R3.
|
||||
In cases where a proprietary CRL infrastructure is provided those values need to be changed accordingly.
|
||||
|
||||
Hiding Sensitive Data
|
||||
---------------------
|
||||
A frequent requirement is that configuration files must not expose passwords to unauthorised readers. By leveraging environment variables, it is possible to hide passwords and other similar fields.
|
||||
|
||||
Take a simple node config that wishes to protect the node cryptographic stores:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
myLegalName : "O=PasswordProtectedNode,OU=corda,L=London,C=GB"
|
||||
keyStorePassword : ${KEY_PASS}
|
||||
trustStorePassword : ${TRUST_PASS}
|
||||
p2pAddress : "localhost:12345"
|
||||
devMode : false
|
||||
compatibilityZoneURL : "https://cz.corda.net"
|
||||
|
||||
By delegating to a password store, and using `command substitution` it is possible to ensure that sensitive passwords never appear in plain text.
|
||||
The below examples are of loading Corda with the KEY_PASS and TRUST_PASS variables read from a program named ``corporatePasswordStore``.
|
||||
|
||||
|
||||
Bash
|
||||
~~~~
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
KEY_PASS=$(corporatePasswordStore --cordaKeyStorePassword) TRUST_PASS=$(corporatePasswordStore --cordaTrustStorePassword) java -jar corda.jar
|
||||
|
||||
Windows PowerShell
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
$env:KEY_PASS=$(corporatePasswordStore --cordaKeyStorePassword); $env:TRUST_PASS=$(corporatePasswordStore --cordaTrustStorePassword); java -jar corda.jar
|
||||
|
||||
|
||||
For launching on Windows without PowerShell, it is not possible to perform command substitution, and so the variables must be specified manually, for example:
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
SET KEY_PASS=mypassword & SET TRUST_PASS=mypassword & java -jar corda.jar
|
||||
|
||||
.. warning:: If this approach is taken, the passwords will appear in the windows command prompt history.
|
||||
|
||||
|
||||
|
@ -46,9 +46,13 @@ Several ``ext`` variables are used in a CorDapp's ``build.gradle`` file to defin
|
||||
|
||||
``corda_gradle_plugins_versions`` are given in the form ``major.minor.patch``. You should use the same ``major`` and
|
||||
``minor`` versions as the Corda version you are using, and the latest ``patch`` version. A list of all the available
|
||||
versions can be found here: https://bintray.com/r3/corda/cordapp. If in doubt, you should base yourself on the version numbers used in the ``build.gradle`` file of the `Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the `Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
versions can be found here: https://bintray.com/r3/corda/cordapp. If in doubt, you should base yourself on the version
|
||||
numbers used in the ``build.gradle`` file of the
|
||||
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ and the
|
||||
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
|
||||
For example, to use version 3.0 of Corda, version 3.0.8 of the Corda gradle plugins, version 0.7.9 of Quasar, and version 1.1.60 of Kotlin, you'd write:
|
||||
For example, to use version 3.0 of Corda, version 3.0.8 of the Corda gradle plugins, version 0.7.9 of Quasar, and
|
||||
version 1.1.60 of Kotlin, you'd write:
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
@ -70,19 +74,55 @@ The ``cordformation`` plugin adds two new gradle configurations:
|
||||
configurations should be used for any Corda dependency (e.g. ``corda-core``, ``corda-node``) in order to prevent a
|
||||
dependency from being included twice (once in the CorDapp JAR and once in the Corda JARs).
|
||||
|
||||
To build against Corda, you must add the following to your ``build.gradle`` file:
|
||||
Here are some guidelines for Corda dependencies:
|
||||
|
||||
* ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
|
||||
* Each Corda compile dependency (eg ``net.corda:corda-core:$corda_release_version``) as a ``cordaCompile`` dependency
|
||||
* When building a CorDapp, you should always include ``net.corda:corda-core:$corda_release_version`` as a
|
||||
``cordaCompile`` dependency, and ``net.corda:corda:$corda_release_version`` as a ``cordaRuntime`` dependency
|
||||
|
||||
You may also want to add:
|
||||
* When building an RPC client that communicates with a node (e.g. a webserver), you should include
|
||||
``net.corda:corda-rpc:$corda_release_version`` as a ``cordaCompile`` dependency
|
||||
|
||||
* ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile`` dependency, in order to use Corda's test
|
||||
* When you need to use the network bootstrapper to bootstrap a local network (e.g. when using ``Cordformation``), you
|
||||
should include ``net.corda:corda-node-api:$corda_release_version`` as a ``cordaCompile`` dependency
|
||||
|
||||
* To use Corda's test frameworks, add ``net.corda:corda-test-utils:$corda_release_version`` as a ``testCompile``
|
||||
dependency. Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency
|
||||
|
||||
* Any other Corda dependencies you need should be included as ``cordaCompile`` dependencies
|
||||
|
||||
Here is an overview of the various Corda dependencies:
|
||||
|
||||
* ``corda`` - The Corda fat JAR. Do not use as a compile dependency. Required as a ``cordaRuntime`` dependency when
|
||||
using ``Cordformation``
|
||||
* ``corda-confidential-identities`` - A part of the core Corda libraries. Automatically pulled in by other libraries
|
||||
* ``corda-core`` - Usually automatically included by another dependency, contains core Corda utilities, model, and
|
||||
functionality. Include manually if the utilities are useful or you are writing a library for Corda
|
||||
* ``corda-core-deterministic`` - Used by the Corda node for deterministic contracts. Not likely to be used externally
|
||||
* ``corda-djvm`` - Used by the Corda node for deterministic contracts. Not likely to be used externally
|
||||
* ``corda-finance`` - The Corda finance CorDapp. Only include as a ``cordaCompile`` dependency if using as a dependent
|
||||
Cordapp or if you need access to the Corda finance types. Use as a ``cordapp`` dependency if using as a CorDapp
|
||||
dependency (see below)
|
||||
* ``corda-jackson`` - Corda Jackson support. Use if you plan to serialise Corda objects to and/or from JSON
|
||||
* ``corda-jfx`` - JavaFX utilities with some Corda-specific models and utilities. Only use with JavaFX apps
|
||||
* ``corda-mock`` - A small library of useful mocks. Use if the classes are useful to you
|
||||
* ``corda-node`` - The Corda node. Do not depend on. Used only by the Corda fat JAR and indirectly in testing
|
||||
frameworks
|
||||
* ``net.corda:corda-webserver:$corda_release_version`` as a ``cordaRuntime`` dependency, in order to use Corda's
|
||||
built-in development webserver
|
||||
|
||||
.. warning:: Never include ``corda-test-utils`` as a ``compile`` or ``cordaCompile`` dependency.
|
||||
* ``corda-node-api`` - The node API. Required to bootstrap a local network
|
||||
* ``corda-node-driver`` - Testing utility for programmatically starting nodes from JVM languages. Use for tests
|
||||
* ``corda-notary-bft-smart`` - A Corda notary implementation
|
||||
* ``corda-notary-raft`` - A Corda notary implementation
|
||||
* ``corda-rpc`` - The Corda RPC client library. Used when writing an RPC client
|
||||
* ``corda-serialization`` - The Corda core serialization library. Automatically included by other dependencies
|
||||
* ``corda-serialization-deterministic`` - The Corda core serialization library. Automatically included by other
|
||||
dependencies
|
||||
* ``corda-shell`` - Used by the Corda node. Never depend on directly
|
||||
* ``corda-test-common`` - A common test library. Automatically included by other test libraries
|
||||
* ``corda-test-utils`` - Used when writing tests against Corda/Cordapps
|
||||
* ``corda-tools-explorer`` - The Node Explorer tool. Do not depend on
|
||||
* ``corda-tools-network-bootstrapper`` - The Network Builder tool. Useful in build scripts
|
||||
* ``corda-tools-shell-cli`` - The Shell CLI tool. Useful in build scripts
|
||||
* ``corda-webserver-impl`` - The Corda webserver fat JAR. Deprecated. Usually only used by build scripts
|
||||
* ``corda-websever`` - The Corda webserver library. Deprecated. Use a standard webserver library such as Spring instead
|
||||
|
||||
Dependencies on other CorDapps
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -157,19 +197,19 @@ The example ``cordapp`` plugin with plugin ``signing`` configuration:
|
||||
}
|
||||
//...
|
||||
|
||||
CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp
|
||||
without need to create a keystore and configure the ``cordapp`` plugin.
|
||||
For production deployment ensure to sign the CorDapp using your own certificate e.g. by setting system properties to point to an external keystore
|
||||
or by disabling signing in ``cordapp`` plugin and signing the CordDapp JAR downstream in your build pipeline.
|
||||
CorDapp signed by Corda development certificate is accepted by Corda node only when running in the development mode.
|
||||
In case CordDapp signed by the (default) development key is run on node in the production mode (e.g. for testing),
|
||||
the node may be set to accept the development key by adding the ``cordappSignerKeyFingerprintBlacklist = []`` property set to empty list
|
||||
(see :ref:`Configuring a node <corda_configuration_file_signer_blacklist>`).
|
||||
CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp without need to create a
|
||||
keystore and configure the ``cordapp`` plugin. For production deployment ensure to sign the CorDapp using your own
|
||||
certificate e.g. by setting system properties to point to an external keystore or by disabling signing in ``cordapp``
|
||||
plugin and signing the CordDapp JAR downstream in your build pipeline. CorDapp signed by Corda development certificate
|
||||
is accepted by Corda node only when running in the development mode. In case CordDapp signed by the (default)
|
||||
development key is run on node in the production mode (e.g. for testing), the node may be set to accept the development
|
||||
key by adding the ``cordappSignerKeyFingerprintBlacklist = []`` property set to empty list (see
|
||||
:ref:`Configuring a node <corda_configuration_file_signer_blacklist>`).
|
||||
|
||||
Signing options can be contextually overwritten by the relevant system properties as described above.
|
||||
This allows the single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore)
|
||||
and for a production build (using an external keystore).
|
||||
The example system properties setup for the build process which overrides signing options:
|
||||
Signing options can be contextually overwritten by the relevant system properties as described above. This allows the
|
||||
single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore) and for
|
||||
a production build (using an external keystore). The example system properties setup for the build process which
|
||||
overrides signing options:
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
@ -187,8 +227,9 @@ CorDapp signing can be disabled for a build:
|
||||
|
||||
./gradlew -Dsigning.enabled=false
|
||||
|
||||
Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin configuration.
|
||||
For example the below configuration sets the specific signing algorithm when a system property is available otherwise defaults to an empty string:
|
||||
Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin
|
||||
configuration. For example the below configuration sets the specific signing algorithm when a system property is
|
||||
available otherwise defaults to an empty string:
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
@ -200,7 +241,8 @@ For example the below configuration sets the specific signing algorithm when a s
|
||||
}
|
||||
//...
|
||||
|
||||
Then the build process can set the value for *custom.sigalg* system property and other system properties recognized by ``cordapp`` plugin:
|
||||
Then the build process can set the value for *custom.sigalg* system property and other system properties recognized by
|
||||
``cordapp`` plugin:
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
@ -216,8 +258,8 @@ Cordformation plugin can also sign CorDapps JARs, when deploying set of nodes, s
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should
|
||||
base yourself on the ``build.gradle`` file of the
|
||||
Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you
|
||||
should base yourself on the ``build.gradle`` file of the
|
||||
`Kotlin CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_ or the
|
||||
`Java CorDapp Template <https://github.com/corda/cordapp-template-kotlin>`_.
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 133 KiB |
@ -16,7 +16,7 @@ The example CorDapp allows nodes to agree IOUs with each other, as long as they
|
||||
|
||||
We will deploy and run the CorDapp on four test nodes:
|
||||
|
||||
* **Notary**, which hosts a validating notary service
|
||||
* **Notary**, which runs a notary service
|
||||
* **PartyA**
|
||||
* **PartyB**
|
||||
* **PartyC**
|
||||
@ -30,10 +30,9 @@ Start by downloading the example CorDapp from GitHub:
|
||||
|
||||
* Set up your machine by following the :doc:`quickstart guide <getting-set-up>`
|
||||
|
||||
* Clone the example CorDapp from the `cordapp-example repository <https://github.com/corda/cordapp-example>`_ using
|
||||
the following command: ``git clone https://github.com/corda/cordapp-example``
|
||||
* Clone the samples repository from using the following command: ``git clone https://github.com/corda/samples``
|
||||
|
||||
* Change directories to the freshly cloned repo: ``cd cordapp-example``
|
||||
* Change directories to the ``cordapp-example`` folder: ``cd samples/cordapp-example``
|
||||
|
||||
Opening the example CorDapp in IntelliJ
|
||||
---------------------------------------
|
||||
@ -41,7 +40,7 @@ Let's open the example CorDapp in IntelliJ IDEA:
|
||||
|
||||
* Open IntelliJ
|
||||
|
||||
* A splash screen will appear. Click ``open``, select the cloned ``cordapp-example`` folder, and click ``OK``
|
||||
* A splash screen will appear. Click ``open``, navigate to and select the ``cordapp-example`` folder, and click ``OK``
|
||||
|
||||
* Once the project is open, click ``File``, then ``Project Structure``. Under ``Project SDK:``, set the project SDK by
|
||||
clicking ``New...``, clicking ``JDK``, and navigating to ``C:\Program Files\Java\jdk1.8.0_XXX`` on Windows or ``Library/Java/JavaVirtualMachines/jdk1.8.XXX`` on MacOSX (where ``XXX`` is the
|
||||
@ -65,61 +64,59 @@ The example CorDapp has the following structure:
|
||||
│ │ └── log4j2.xml
|
||||
│ └── test
|
||||
│ └── log4j2.xml
|
||||
├── doc
|
||||
│ └── example_flow.plantuml
|
||||
├── gradle
|
||||
│ └── wrapper
|
||||
│ ├── gradle-wrapper.jar
|
||||
│ └── gradle-wrapper.properties
|
||||
├── lib
|
||||
│ ├── README.txt
|
||||
│ └── quasar.jar
|
||||
├── java-source
|
||||
│ └── ...
|
||||
├── kotlin-source
|
||||
│ ├── build.gradle
|
||||
│ └── src
|
||||
│ ├── integrationTest
|
||||
│ │ └── java
|
||||
│ │ └── com
|
||||
│ │ └── example
|
||||
│ │ └── DriverBasedTests.java
|
||||
│ ├── main
|
||||
│ │ ├── kotlin
|
||||
│ │ ├── java
|
||||
│ │ │ └── com
|
||||
│ │ │ └── example
|
||||
│ │ │ ├── api
|
||||
│ │ │ │ └── ExampleApi.kt
|
||||
│ │ │ │ └── ExampleApi.java
|
||||
│ │ │ ├── client
|
||||
│ │ │ │ └── ExampleClientRPC.kt
|
||||
│ │ │ │ └── ExampleClientRPC.java
|
||||
│ │ │ ├── contract
|
||||
│ │ │ │ └── IOUContract.kt
|
||||
│ │ │ │ └── IOUContract.java
|
||||
│ │ │ ├── flow
|
||||
│ │ │ │ └── ExampleFlow.kt
|
||||
│ │ │ ├── model
|
||||
│ │ │ │ └── IOU.kt
|
||||
│ │ │ │ └── ExampleFlow.java
|
||||
│ │ │ ├── plugin
|
||||
│ │ │ │ └── ExamplePlugin.kt
|
||||
│ │ │ │ └── ExamplePlugin.java
|
||||
│ │ │ ├── schema
|
||||
│ │ │ │ └── IOUSchema.kt
|
||||
│ │ │ │ ├── IOUSchema.java
|
||||
│ │ │ │ └── IOUSchemaV1.java
|
||||
│ │ │ └── state
|
||||
│ │ │ └── IOUState.kt
|
||||
│ │ │ └── IOUState.java
|
||||
│ │ └── resources
|
||||
│ │ ├── META-INF
|
||||
│ │ │ └── services
|
||||
│ │ │ └── net.corda.webserver.services.WebServerPluginRegistry
|
||||
│ │ ├── certificates
|
||||
│ │ │ ├── readme.txt
|
||||
│ │ │ ├── sslkeystore.jks
|
||||
│ │ │ └── truststore.jks
|
||||
│ │ └── exampleWeb
|
||||
│ │ ├── index.html
|
||||
│ │ └── js
|
||||
│ │ └── angular-module.js
|
||||
│ └── test
|
||||
│ └── kotlin
|
||||
│ └── java
|
||||
│ └── com
|
||||
│ └── example
|
||||
│ ├── Main.kt
|
||||
│ ├── NodeDriver.java
|
||||
│ ├── contract
|
||||
│ │ └── IOUContractTests.kt
|
||||
│ │ └── IOUContractTests.java
|
||||
│ └── flow
|
||||
│ └── IOUFlowTests.kt
|
||||
│ └── IOUFlowTests.java
|
||||
├── kotlin-source
|
||||
│ ├── ...
|
||||
├── lib
|
||||
│ ├── README.txt
|
||||
│ └── quasar.jar
|
||||
├── .gitignore
|
||||
├── LICENCE
|
||||
├── README.md
|
||||
@ -137,16 +134,15 @@ The key files and directories are as follows:
|
||||
* **gradle** contains the gradle wrapper, which allows the use of Gradle without installing it yourself and worrying
|
||||
about which version is required
|
||||
* **lib** contains the Quasar jar which rewrites our CorDapp's flows to be checkpointable
|
||||
* **kotlin-source** contains the source code for the example CorDapp written in Kotlin
|
||||
* **java-source** contains the source code for the example CorDapp written in Java
|
||||
|
||||
* **kotlin-source/src/main/kotlin** contains the source code for the example CorDapp
|
||||
* **kotlin-source/src/main/resources** contains the certificate store, some static web content to be served by the
|
||||
* **java-source/src/main/java** contains the source code for the example CorDapp
|
||||
* **java-source/src/main/resources** contains the certificate store, some static web content to be served by the
|
||||
nodes and the WebServerPluginRegistry file
|
||||
* **kotlin-source/src/test/kotlin** contains unit tests for the contracts and flows, and the driver to run the nodes
|
||||
* **java-source/src/test/java** contains unit tests for the contracts and flows, and the driver to run the nodes
|
||||
via IntelliJ
|
||||
|
||||
* **java-source** contains the same source code, but written in Java. CorDapps can be developed in any language
|
||||
targeting the JVM
|
||||
* **kotlin-source** contains the same source code, but written in Kotlin. CorDapps can be developed in either Java and Kotlin
|
||||
|
||||
Running the example CorDapp
|
||||
---------------------------
|
||||
|
@ -32,14 +32,14 @@ data class PrintedTransitionGraph(val stateClassName: String, val printedPUML: S
|
||||
* Shorthand for defining transitions directly from the command class
|
||||
*/
|
||||
fun <S, R> CommandData.txDef(signer: R? = null, from: S?, to: List<S?>):
|
||||
TransitionDef<S, R> = TransitionDef(this::class.java, signer, from, to)
|
||||
TransitionDef<S, R> = TransitionDef(this::class.java, signer, from, to)
|
||||
|
||||
/**
|
||||
* For a given [stateClass] that tracks a status, it holds all possible transitions in [ts].
|
||||
* This can be used for generic [verify] in contract code as well as for visualizing the state transition graph in PUML ([printGraph]).
|
||||
*/
|
||||
class StatusTransitions<out S, in R, T : StatusTrackingContractState<S, R>>(private val stateClass: KClass<T>,
|
||||
private vararg val ts: TransitionDef<S, R>) {
|
||||
private vararg val ts: TransitionDef<S, R>) {
|
||||
|
||||
private val allowedCmds = ts.map { it.cmd }.toSet()
|
||||
|
||||
@ -67,11 +67,11 @@ class StatusTransitions<out S, in R, T : StatusTrackingContractState<S, R>>(priv
|
||||
// for each combination of in x out which should normally be at most 1...
|
||||
inputStates.forEach { inp ->
|
||||
outputStates.forEach { outp ->
|
||||
require(inp != null || outp != null)
|
||||
require(inp != null || outp != null) { "Input and output states cannot be both left unspecified" }
|
||||
val options = matchingTransitions(inp?.status, outp?.status, cmd.value)
|
||||
|
||||
val signerGroup = options.groupBy { it.signer }.entries.singleOrNull()
|
||||
?: throw IllegalStateException("Cannot have different signers in StatusTransitions for the same command.")
|
||||
?: throw IllegalStateException("Cannot have different signers in StatusTransitions for the same command.")
|
||||
val signer = signerGroup.key
|
||||
if (signer != null) {
|
||||
// which state determines who is the signer? by default the input, unless it's the initial transition
|
||||
|
@ -132,7 +132,8 @@ class FlowWorkerServiceHub(override val configuration: NodeConfiguration,
|
||||
identityService::wellKnownPartyFromX500Name,
|
||||
identityService::wellKnownPartyFromAnonymous,
|
||||
schemaService,
|
||||
cacheFactory
|
||||
cacheFactory,
|
||||
cordappLoader.appClassLoader
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -92,7 +92,8 @@ class RpcWorkerServiceHub(override val configuration: NodeConfiguration,
|
||||
identityService::wellKnownPartyFromX500Name,
|
||||
identityService::wellKnownPartyFromAnonymous,
|
||||
schemaService,
|
||||
cacheFactory
|
||||
cacheFactory,
|
||||
cordappLoader.appClassLoader
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -203,7 +203,7 @@ class UniversalContract : Contract {
|
||||
val rest = extractRemainder(arr, action)
|
||||
|
||||
// for now - let's assume not
|
||||
require(rest is Zero)
|
||||
require(rest is Zero) { "Remainder must be zero" }
|
||||
|
||||
requireThat {
|
||||
"action must have a time-window" using (tx.timeWindow != null)
|
||||
|
@ -321,7 +321,7 @@ open class BusinessCalendar(val holidayDates: List<LocalDate>) {
|
||||
* TODO: Make more efficient if necessary
|
||||
*/
|
||||
fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate {
|
||||
require(i >= 0)
|
||||
require(i >= 0){"Days to add/subtract must be positive"}
|
||||
if (i == 0) return date
|
||||
var retDate = date
|
||||
var ctr = 0
|
||||
|
@ -39,8 +39,8 @@ private fun rowsToAmount(currency: Currency, rows: Vault.Page<FungibleAsset<*>>)
|
||||
return if (rows.otherResults.isEmpty()) {
|
||||
Amount(0L, currency)
|
||||
} else {
|
||||
require(rows.otherResults.size == 2)
|
||||
require(rows.otherResults[1] == currency.currencyCode)
|
||||
require(rows.otherResults.size == 2){"Invalid number of rows returned by query"}
|
||||
require(rows.otherResults[1] == currency.currencyCode){"Currency on rows returned by query does not match expected"}
|
||||
val quantity = rows.otherResults[0] as Long
|
||||
Amount(quantity, currency)
|
||||
}
|
||||
|
@ -298,7 +298,7 @@ abstract class OnLedgerAsset<T : Any, out C : CommandData, S : FungibleAsset<T>>
|
||||
issueCommand: CommandData): Set<PublicKey> {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
|
||||
require(transactionState.data.amount.quantity > 0)
|
||||
require(transactionState.data.amount.quantity > 0){"Amount to issue must be greater than zero"}
|
||||
val at = transactionState.data.amount.token.issuer
|
||||
val commandSigner = at.party.owningKey
|
||||
tx.addOutputState(transactionState)
|
||||
|
@ -140,8 +140,8 @@ object TwoPartyDealFlow {
|
||||
// Verify the transaction identities represent the correct parties
|
||||
val wellKnownOtherParty = serviceHub.identityService.wellKnownPartyFromAnonymous(it.primaryIdentity)
|
||||
val wellKnownMe = serviceHub.identityService.wellKnownPartyFromAnonymous(it.secondaryIdentity)
|
||||
require(wellKnownOtherParty == otherSideSession.counterparty)
|
||||
require(wellKnownMe == ourIdentity)
|
||||
require(wellKnownOtherParty == otherSideSession.counterparty){"Well known party for handshake identity ${it.primaryIdentity} does not match counterparty"}
|
||||
require(wellKnownMe == ourIdentity){"Well known party for handshake identity ${it.secondaryIdentity} does not match ourIdentity"}
|
||||
validateHandshake(it)
|
||||
}
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ object TwoPartyTradeFlow {
|
||||
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
|
||||
// prove the owner is a confidential identity of the counterparty.
|
||||
val assetForSaleIdentity = serviceHub.identityService.wellKnownPartyFromAnonymous(asset.owner)
|
||||
require(assetForSaleIdentity == sellerSession.counterparty)
|
||||
require(assetForSaleIdentity == sellerSession.counterparty){"Well known identity lookup returned identity that does not match counterparty"}
|
||||
|
||||
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
|
||||
// identity, so that anonymity is enforced.
|
||||
|
@ -9,8 +9,8 @@ data class ScreenCoordinate(val screenX: Double, val screenY: Double)
|
||||
@CordaSerializable
|
||||
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
||||
init {
|
||||
require(latitude in -90..90)
|
||||
require(longitude in -180..180)
|
||||
require(latitude in -90..90){"Latitude must be between -90 and +90"}
|
||||
require(longitude in -180..180){"Longitude must be between -180 and +180"}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -24,8 +24,8 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
||||
@Suppress("unused") // Used from the visualiser GUI.
|
||||
fun project(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double,
|
||||
leftLongitude: Double, rightLongitude: Double): ScreenCoordinate {
|
||||
require(latitude in bottomLatitude..topLatitude)
|
||||
require(longitude in leftLongitude..rightLongitude)
|
||||
require(latitude in bottomLatitude..topLatitude){"Latitude must be between $bottomLatitude and $topLatitude"}
|
||||
require(longitude in leftLongitude..rightLongitude){"Longitude must be between $leftLongitude and $rightLongitude"}
|
||||
|
||||
fun deg2rad(deg: Double) = deg * Math.PI / 180.0
|
||||
val leftLngRad = deg2rad(leftLongitude)
|
||||
|
@ -25,6 +25,7 @@ import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.dsl.*
|
||||
import net.corda.testing.internal.TEST_TX_TIME
|
||||
import net.corda.testing.internal.fakeAttachment
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.vault.CommodityState
|
||||
import net.corda.testing.node.MockServices
|
||||
@ -565,7 +566,7 @@ class ObligationTests {
|
||||
|
||||
@Test
|
||||
fun `commodity settlement`() {
|
||||
val commodityContractBytes = "https://www.big-book-of-banking-law.gov/commodity-claims.html".toByteArray()
|
||||
val commodityContractBytes = fakeAttachment("file1.txt", "https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||
val defaultFcoj = Issued(defaultIssuer, Commodity.getInstance("FCOJ")!!)
|
||||
val oneUnitFcoj = Amount(1, defaultFcoj)
|
||||
val obligationDef = Obligation.Terms(NonEmptySet.of(commodityContractBytes.sha256() as SecureHash), NonEmptySet.of(defaultFcoj), TEST_TX_TIME)
|
||||
@ -957,7 +958,7 @@ class ObligationTests {
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
private val cashContractBytes = "https://www.big-book-of-banking-law.gov/cash-claims.html".toByteArray()
|
||||
private val cashContractBytes = fakeAttachment("file1.txt", "https://www.big-book-of-banking-law.gov/cash-claims.html")
|
||||
private val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency>
|
||||
get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME)
|
||||
private val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
|
||||
|
@ -47,7 +47,7 @@ object DevIdentityGenerator {
|
||||
|
||||
/** Generates a CFT notary identity, where the entire cluster shares a key pair. */
|
||||
fun generateDistributedNotarySingularIdentity(dirs: List<Path>, notaryName: CordaX500Name): Party {
|
||||
require(dirs.isNotEmpty())
|
||||
require(dirs.isNotEmpty()){"At least one directory to generate identity for must be specified"}
|
||||
|
||||
log.trace { "Generating singular identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||
|
||||
@ -63,7 +63,7 @@ object DevIdentityGenerator {
|
||||
|
||||
/** Generates a BFT notary identity: individual key pairs for each cluster member, and a shared composite key. */
|
||||
fun generateDistributedNotaryCompositeIdentity(dirs: List<Path>, notaryName: CordaX500Name, threshold: Int = 1): Party {
|
||||
require(dirs.isNotEmpty())
|
||||
require(dirs.isNotEmpty()){"At least one directory to generate identity for must be specified"}
|
||||
|
||||
log.trace { "Generating composite identity \"$notaryName\" for nodes: ${dirs.joinToString()}" }
|
||||
|
||||
|
@ -254,7 +254,7 @@ object X509Utilities {
|
||||
crlIssuer: X500Name? = null): X509Certificate {
|
||||
val builder = createPartialCertificate(certificateType, issuer, issuerPublicKey, subject, subjectPublicKey, validityWindow, nameConstraints, crlDistPoint, crlIssuer)
|
||||
return builder.build(issuerSigner).run {
|
||||
require(isValidOn(Date()))
|
||||
require(isValidOn(Date())){"Certificate is not valid at instant now"}
|
||||
toJca()
|
||||
}
|
||||
}
|
||||
@ -292,8 +292,8 @@ object X509Utilities {
|
||||
crlDistPoint,
|
||||
crlIssuer)
|
||||
return builder.build(signer).run {
|
||||
require(isValidOn(Date()))
|
||||
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public)))
|
||||
require(isValidOn(Date())){"Certificate is not valid at instant now"}
|
||||
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))){"Invalid signature"}
|
||||
toJca()
|
||||
}
|
||||
}
|
||||
|
@ -431,10 +431,10 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
|
||||
private fun NodeInfo.notaryIdentity(): Party {
|
||||
return when (legalIdentities.size) {
|
||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||
// Single node notaries have just one identity like all other nodes. This identity is the notary identity
|
||||
1 -> legalIdentities[0]
|
||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||
// cluster and is shared by all the other members. This is the notary identity.
|
||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||
// cluster and is shared by all the other members. This is the notary identity.
|
||||
2 -> legalIdentities[1]
|
||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import rx.Observable
|
||||
import rx.Scheduler
|
||||
import rx.Subscription
|
||||
|
@ -64,7 +64,8 @@ class CordaPersistence(
|
||||
databaseConfig: DatabaseConfig,
|
||||
schemas: Set<MappedSchema>,
|
||||
cacheFactory: NamedCacheFactory,
|
||||
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet()
|
||||
attributeConverters: Collection<AttributeConverter<*, *>> = emptySet(),
|
||||
customClassLoader: ClassLoader? = null
|
||||
) : Closeable {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
@ -73,7 +74,7 @@ class CordaPersistence(
|
||||
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||
val hibernateConfig: HibernateConfiguration by lazy {
|
||||
transaction {
|
||||
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory)
|
||||
HibernateConfiguration(schemas, databaseConfig, attributeConverters, jdbcUrl, cacheFactory, customClassLoader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ class HibernateConfiguration(
|
||||
private val attributeConverters: Collection<AttributeConverter<*, *>>,
|
||||
private val jdbcUrl: String,
|
||||
cacheFactory: NamedCacheFactory,
|
||||
val cordappClassLoader: ClassLoader? = null
|
||||
val customClassLoader: ClassLoader? = null
|
||||
) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
@ -104,7 +104,7 @@ class HibernateConfiguration(
|
||||
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
|
||||
}
|
||||
|
||||
val sessionFactory = buildSessionFactory(config, metadataSources, cordappClassLoader)
|
||||
val sessionFactory = buildSessionFactory(config, metadataSources, customClassLoader)
|
||||
logger.info("Created session factory for schemas: $schemas")
|
||||
|
||||
// export Hibernate JMX statistics
|
||||
@ -130,13 +130,13 @@ class HibernateConfiguration(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, cordappClassLoader: ClassLoader?): SessionFactory {
|
||||
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, customClassLoader: ClassLoader?): SessionFactory {
|
||||
config.standardServiceRegistryBuilder.applySettings(config.properties)
|
||||
|
||||
if (cordappClassLoader != null) {
|
||||
if (customClassLoader != null) {
|
||||
config.standardServiceRegistryBuilder.addService(
|
||||
ClassLoaderService::class.java,
|
||||
ClassLoaderServiceImpl(cordappClassLoader))
|
||||
ClassLoaderServiceImpl(customClassLoader))
|
||||
}
|
||||
|
||||
val metadataBuilder = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build())
|
||||
|
@ -11,7 +11,7 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.config.toConfig
|
||||
|
@ -4,7 +4,7 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.write
|
||||
import net.corda.nodeapi.eventually
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
@ -5,10 +5,11 @@ import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.ParametersUpdateInfo
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.SignedNetworkParameters
|
||||
@ -24,14 +25,13 @@ import net.corda.testing.node.internal.*
|
||||
import net.corda.testing.node.internal.network.NetworkMapServer
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.junit.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.time.Instant
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
@ -247,7 +247,9 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
||||
// Make sure the nodes aren't getting the node infos from their additional directories
|
||||
val nodeInfosDir = baseDirectory / NODE_INFO_DIRECTORY
|
||||
if (nodeInfosDir.exists()) {
|
||||
assertThat(nodeInfosDir.list()).isEmpty()
|
||||
Assert.assertThat(nodeInfosDir.list().size, `is`(1))
|
||||
Assert.assertThat(Files.readAllBytes(nodeInfosDir.list().single()).deserialize<SignedNodeInfo>().verified().legalIdentities.first(), `is`( this.nodeInfo.legalIdentities.first()))
|
||||
|
||||
}
|
||||
assertThat(rpc.networkMapSnapshot()).containsOnly(*nodes)
|
||||
}
|
||||
|
@ -77,10 +77,10 @@ class LargeTransactionsTest : IntegrationTest() {
|
||||
fun checkCanSendLargeTransactions() {
|
||||
// These 4 attachments yield a transaction that's got >10mb attached, so it'd push us over the Artemis
|
||||
// max message size.
|
||||
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 0)
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 1)
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 2)
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 3)
|
||||
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 0, "a")
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 1, "b")
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 2, "c")
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 3, "d")
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),
|
||||
|
@ -128,7 +128,6 @@ public class CordaCaplet extends Capsule {
|
||||
}
|
||||
// Add additional directories of JARs to the classpath (at the end), e.g., for JDBC drivers.
|
||||
augmentClasspath(cp, new File(baseDir, "drivers"));
|
||||
augmentClasspath(cp, cordappsDir);
|
||||
try {
|
||||
List<String> jarDirs = nodeConfig.getStringList("jarDirs");
|
||||
log(LOG_VERBOSE, "Configured JAR directories = " + jarDirs);
|
||||
|
@ -75,7 +75,7 @@ open class SharedNodeCmdLineOptions {
|
||||
errors.forEach { error ->
|
||||
when (error) {
|
||||
is ConfigException.IO -> logger.error(configFileNotFoundMessage(configFile))
|
||||
else -> logger.error(error.message, error)
|
||||
else -> logger.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +154,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
identityService::wellKnownPartyFromX500Name,
|
||||
identityService::wellKnownPartyFromAnonymous,
|
||||
schemaService,
|
||||
cacheFactory)
|
||||
cacheFactory,
|
||||
this.cordappLoader.appClassLoader)
|
||||
|
||||
init {
|
||||
// TODO Break cyclic dependency
|
||||
@ -466,6 +467,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
// Write the node-info file even if nothing's changed, just in case the file has been deleted.
|
||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, nodeInfoAndSigned)
|
||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory / NODE_INFO_DIRECTORY, nodeInfoAndSigned)
|
||||
|
||||
// Always republish on startup, it's treated by network map server as a heartbeat.
|
||||
if (publish && networkMapClient != null) {
|
||||
@ -774,7 +776,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
|
||||
val isH2Database = isH2Database(props.getProperty("dataSource.url", ""))
|
||||
val schemas = if (isH2Database) schemaService.internalSchemas() else schemaService.schemaOptions.keys
|
||||
database.startHikariPool(props, configuration.database, schemas, metricRegistry)
|
||||
database.startHikariPool(props, configuration.database, schemas, metricRegistry, this.cordappLoader.appClassLoader)
|
||||
// Now log the vendor string as this will also cause a connection to be tested eagerly.
|
||||
logVendorString(database, log)
|
||||
}
|
||||
@ -1093,21 +1095,22 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
|
||||
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
|
||||
schemaService: SchemaService,
|
||||
cacheFactory: NamedCacheFactory): CordaPersistence {
|
||||
cacheFactory: NamedCacheFactory,
|
||||
customClassLoader: ClassLoader?): CordaPersistence {
|
||||
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
|
||||
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
|
||||
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
|
||||
// either Hibernate can be convinced to stop warning, use the descriptor by default, or something else.
|
||||
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, cacheFactory, attributeConverters)
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, cacheFactory, attributeConverters, customClassLoader)
|
||||
}
|
||||
|
||||
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null) {
|
||||
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null, classloader: ClassLoader = Thread.currentThread().contextClassLoader) {
|
||||
try {
|
||||
val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
|
||||
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
|
||||
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig)
|
||||
val schemaMigration = SchemaMigration(schemas, dataSource, databaseConfig, classloader)
|
||||
schemaMigration.nodeStartup(dataSource.connection.use { DBCheckpointStorage().getCheckpointCount(it) != 0L }, isH2Database(jdbcUrl))
|
||||
start(dataSource, jdbcUrl)
|
||||
} catch (ex: Exception) {
|
||||
|
@ -39,7 +39,7 @@ private class MultiplexingReactiveArtemisConsumer(private val queueNames: Set<St
|
||||
override fun start() {
|
||||
|
||||
synchronized(this) {
|
||||
require(!startedFlag)
|
||||
require(!startedFlag) { "Must not be started" }
|
||||
connect()
|
||||
startedFlag = true
|
||||
}
|
||||
@ -59,7 +59,7 @@ private class MultiplexingReactiveArtemisConsumer(private val queueNames: Set<St
|
||||
override fun connect() {
|
||||
|
||||
synchronized(this) {
|
||||
require(!connected)
|
||||
require(!connected) { "Must not be connected" }
|
||||
queueNames.forEach { queue ->
|
||||
createSession().apply {
|
||||
start()
|
||||
|
@ -11,7 +11,7 @@ import net.corda.core.internal.writer
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.serialization.internal.MutableClassWhitelist
|
||||
import net.corda.serialization.internal.TransientClassWhiteList
|
||||
import net.corda.serialization.internal.amqp.hasCordaSerializable
|
||||
|
@ -77,7 +77,9 @@ class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>()
|
||||
// Verify that this class is immutable (all properties are final).
|
||||
// We disable this check inside SGX as the reflection blows up.
|
||||
if (!SgxSupport.isInsideEnclave) {
|
||||
require(props.none { it is KMutableProperty<*> })
|
||||
props.forEach {
|
||||
require(it !is KMutableProperty<*>) { "$it mutable property of class: ${klass} is unsupported" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,8 +284,8 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
|
||||
val users: List<User>? = null) {
|
||||
init {
|
||||
when (type) {
|
||||
AuthDataSourceType.INMEMORY -> require(users != null && connection == null)
|
||||
AuthDataSourceType.DB -> require(users == null && connection != null)
|
||||
AuthDataSourceType.INMEMORY -> require(users != null && connection == null) { "In-memory authentication must specify a user list, and must not configure a database" }
|
||||
AuthDataSourceType.DB -> require(users == null && connection != null) { "Database-backed authentication must not specify a user list, and must configure a database" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -673,7 +673,7 @@ private class P2PMessagingConsumer(
|
||||
override fun start() {
|
||||
|
||||
synchronized(this) {
|
||||
require(!startedFlag)
|
||||
require(!startedFlag){"Must not already be started"}
|
||||
drainingModeWasChangedEvents.filter { change -> change.switchedOn() }.doOnNext { initialAndExistingConsumer.switchTo(existingOnlyConsumer) }.subscribe()
|
||||
drainingModeWasChangedEvents.filter { change -> change.switchedOff() }.doOnNext { existingOnlyConsumer.switchTo(initialAndExistingConsumer) }.subscribe()
|
||||
subscriptions += existingOnlyConsumer.messages.doOnNext(messages::onNext).subscribe()
|
||||
|
@ -289,7 +289,7 @@ class RPCServer<OPS : RPCOps>(
|
||||
private fun bindingRemovalArtemisMessageHandler(artemisMessage: ClientMessage) {
|
||||
lifeCycle.requireState(State.STARTED)
|
||||
val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE)
|
||||
require(notificationType == CoreNotificationType.BINDING_REMOVED.name)
|
||||
require(notificationType == CoreNotificationType.BINDING_REMOVED.name){"Message contained notification type of $notificationType instead of expected ${CoreNotificationType.BINDING_REMOVED.name}"}
|
||||
val clientAddress = artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME)
|
||||
log.warn("Detected RPC client disconnect on address $clientAddress, scheduling for reaping")
|
||||
invalidateClient(SimpleString(clientAddress))
|
||||
@ -299,7 +299,7 @@ class RPCServer<OPS : RPCOps>(
|
||||
private fun bindingAdditionArtemisMessageHandler(artemisMessage: ClientMessage) {
|
||||
lifeCycle.requireState(State.STARTED)
|
||||
val notificationType = artemisMessage.getStringProperty(ManagementHelper.HDR_NOTIFICATION_TYPE)
|
||||
require(notificationType == CoreNotificationType.BINDING_ADDED.name)
|
||||
require(notificationType == CoreNotificationType.BINDING_ADDED.name){"Message contained notification type of $notificationType instead of expected ${CoreNotificationType.BINDING_ADDED.name}"}
|
||||
val clientAddress = SimpleString(artemisMessage.getStringProperty(ManagementHelper.HDR_ROUTING_NAME))
|
||||
log.debug("RPC client queue created on address $clientAddress")
|
||||
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||
import rx.Observable
|
||||
|
@ -229,7 +229,7 @@ class NodeAttachmentService(
|
||||
val attachmentImpl = AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad).let {
|
||||
val contracts = attachment.contractClassNames
|
||||
if (contracts != null && contracts.isNotEmpty()) {
|
||||
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers
|
||||
ContractAttachment(it, contracts.first(), contracts.drop(1).toSet(), attachment.uploader, attachment.signers?.toList()
|
||||
?: emptyList())
|
||||
} else {
|
||||
it
|
||||
@ -300,7 +300,7 @@ class NodeAttachmentService(
|
||||
private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId {
|
||||
return database.transaction {
|
||||
withContractsInJar(jar) { contractClassNames, inputStream ->
|
||||
require(inputStream !is JarInputStream)
|
||||
require(inputStream !is JarInputStream){"Input stream must not be a JarInputStream"}
|
||||
|
||||
// Read the file into RAM and then calculate its hash. The attachment must fit into memory.
|
||||
// TODO: Switch to a two-phase insert so we can handle attachments larger than RAM.
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services.statemachine
|
||||
|
||||
import co.paralleluniverse.strands.concurrent.AbstractQueuedSynchronizer
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.concurrent.AbstractQueuedSynchronizer
|
||||
|
||||
/**
|
||||
* Quasar-compatible latch that may be incremented.
|
||||
@ -56,7 +56,7 @@ class CountUpDownLatch(initialValue: Int) {
|
||||
}
|
||||
|
||||
fun countDown(number: Int = 1) {
|
||||
require(number > 0)
|
||||
require(number > 0){"Number to count down by must be greater than 0"}
|
||||
sync.releaseShared(number)
|
||||
}
|
||||
|
||||
|
@ -197,7 +197,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
"Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation."
|
||||
}
|
||||
} else {
|
||||
require(contextTransactionOrNull == null)
|
||||
require(contextTransactionOrNull == null){"Transaction is marked as not present, but is not null"}
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,7 +392,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
isDbTransactionOpenOnEntry = true,
|
||||
isDbTransactionOpenOnExit = false
|
||||
)
|
||||
require(continuation == FlowContinuation.ProcessEvents)
|
||||
require(continuation == FlowContinuation.ProcessEvents){"Expected a continuation of type ${FlowContinuation.ProcessEvents}, found $continuation "}
|
||||
unpark(SERIALIZER_BLOCKER)
|
||||
}
|
||||
return uncheckedCast(processEventsUntilFlowIsResumed(
|
||||
|
@ -172,7 +172,7 @@ class SingleThreadedStateMachineManager(
|
||||
* @param allowedUnsuspendedFiberCount Optional parameter is used in some tests.
|
||||
*/
|
||||
override fun stop(allowedUnsuspendedFiberCount: Int) {
|
||||
require(allowedUnsuspendedFiberCount >= 0)
|
||||
require(allowedUnsuspendedFiberCount >= 0){"allowedUnsuspendedFiberCount must be greater than or equal to zero"}
|
||||
mutex.locked {
|
||||
if (stopping) throw IllegalStateException("Already stopping!")
|
||||
stopping = true
|
||||
@ -775,10 +775,10 @@ class SingleThreadedStateMachineManager(
|
||||
) {
|
||||
drainFlowEventQueue(flow)
|
||||
// final sanity checks
|
||||
require(lastState.pendingDeduplicationHandlers.isEmpty())
|
||||
require(lastState.isRemoved)
|
||||
require(lastState.checkpoint.subFlowStack.size == 1)
|
||||
require(flow.fiber.id !in sessionToFlow.values)
|
||||
require(lastState.pendingDeduplicationHandlers.isEmpty()) { "Flow cannot be removed until all pending deduplications have completed" }
|
||||
require(lastState.isRemoved) { "Flow must be in removable state before removal" }
|
||||
require(lastState.checkpoint.subFlowStack.size == 1) { "Checkpointed stack must be empty" }
|
||||
require(flow.fiber.id !in sessionToFlow.values) { "Flow fibre must not be needed by an existing session" }
|
||||
flow.resultFuture.set(removalReason.flowReturnValue)
|
||||
lastState.flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
changesPublisher.onNext(StateMachineManager.Change.Removed(lastState.flowLogic, Try.Success(removalReason.flowReturnValue)))
|
||||
|
@ -2,17 +2,11 @@ package net.corda.node.services.statemachine.interceptors
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.checkpointDeserialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.statemachine.ActionExecutor
|
||||
import net.corda.node.services.statemachine.Event
|
||||
import net.corda.node.services.statemachine.FlowFiber
|
||||
import net.corda.node.services.statemachine.FlowState
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.statemachine.StateMachineState
|
||||
import net.corda.node.services.statemachine.TransitionExecutor
|
||||
import net.corda.node.services.statemachine.*
|
||||
import net.corda.node.services.statemachine.transitions.FlowContinuation
|
||||
import net.corda.node.services.statemachine.transitions.TransitionResult
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
@ -69,7 +63,7 @@ class FiberDeserializationChecker {
|
||||
private var foundUnrestorableFibers: Boolean = false
|
||||
|
||||
fun start(checkpointSerializationContext: CheckpointSerializationContext) {
|
||||
require(checkerThread == null)
|
||||
require(checkerThread == null){"Checking thread must not already be started"}
|
||||
checkerThread = thread(name = "FiberDeserializationChecker") {
|
||||
while (true) {
|
||||
val job = jobQueue.take()
|
||||
|
@ -372,7 +372,7 @@ class NodeRegistrationHelper(
|
||||
private class FixedPeriodLimitedRetrialStrategy(times: Int, private val period: Duration) : (Duration?) -> Duration? {
|
||||
|
||||
init {
|
||||
require(times > 0)
|
||||
require(times > 0){"Retry attempts must be larger than zero"}
|
||||
}
|
||||
|
||||
private var counter = times
|
||||
|
@ -16,7 +16,7 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||
|
@ -6,7 +6,7 @@ import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.size
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.nodeapi.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
|
@ -31,7 +31,7 @@ class SchemaMigrationTest {
|
||||
private fun configureDatabase(hikariProperties: Properties,
|
||||
databaseConfig: DatabaseConfig,
|
||||
schemaService: NodeSchemaService = NodeSchemaService()): CordaPersistence =
|
||||
createCordaPersistence(databaseConfig, { null }, { null }, schemaService, TestingNamedCacheFactory())
|
||||
createCordaPersistence(databaseConfig, { null }, { null }, schemaService, TestingNamedCacheFactory(), null)
|
||||
.apply { startHikariPool(hikariProperties, databaseConfig, schemaService.schemaOptions.keys) }
|
||||
|
||||
@Test
|
||||
|
@ -54,10 +54,10 @@ class MaxTransactionSizeTests {
|
||||
@Test
|
||||
fun `check transaction will fail when exceed max transaction size limit`() {
|
||||
// These 4 attachments yield a transaction that's got ~ 4mb, which will exceed the 3mb max transaction size limit
|
||||
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0)
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1)
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2)
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3)
|
||||
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0, "a")
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1, "b")
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2, "c")
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3, "d")
|
||||
val flow = aliceNode.transaction {
|
||||
val hash1 = aliceNode.importAttachment(bigFile1.inputStream)
|
||||
val hash2 = aliceNode.importAttachment(bigFile2.inputStream)
|
||||
@ -77,10 +77,10 @@ class MaxTransactionSizeTests {
|
||||
@Test
|
||||
fun `check transaction will be rejected by counterparty when exceed max transaction size limit`() {
|
||||
// These 4 attachments yield a transaction that's got ~ 4mb, which will exceed the 3mb max transaction size limit
|
||||
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0)
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1)
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2)
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3)
|
||||
val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 0, "a")
|
||||
val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 1, "b")
|
||||
val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 2, "c")
|
||||
val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024, 3, "c")
|
||||
val flow = aliceNode.transaction {
|
||||
val hash1 = aliceNode.importAttachment(bigFile1.inputStream)
|
||||
val hash2 = aliceNode.importAttachment(bigFile2.inputStream)
|
||||
|
@ -94,7 +94,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.
|
||||
val id = rpc.uploadAttachment(it)
|
||||
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
||||
}
|
||||
require(rpc.attachmentExists(hash))
|
||||
require(rpc.attachmentExists(hash)){"Attachment matching hash: $hash does not exist"}
|
||||
}
|
||||
|
||||
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSideFuture.get(), notaryFuture.get(), hash)
|
||||
@ -159,7 +159,7 @@ fun recipient(rpc: CordaRPCOps, webPort: Int) {
|
||||
if (wtx.attachments.isNotEmpty()) {
|
||||
if (wtx.outputs.isNotEmpty()) {
|
||||
val state = wtx.outputsOfType<AttachmentContract.State>().single()
|
||||
require(rpc.attachmentExists(state.hash))
|
||||
require(rpc.attachmentExists(state.hash)) {"attachment matching hash: ${state.hash} does not exist"}
|
||||
|
||||
// Download the attachment via the Web endpoint.
|
||||
val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
|
||||
@ -207,7 +207,7 @@ class AttachmentContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val state = tx.outputsOfType<AttachmentContract.State>().single()
|
||||
// we check that at least one has the matching hash, the other will be the contract
|
||||
require(tx.attachments.any { it.id == state.hash })
|
||||
require(tx.attachments.any { it.id == state.hash }) {"At least one attachment in transaction must match hash ${state.hash}"}
|
||||
}
|
||||
|
||||
object Command : TypeOnlyCommandData()
|
||||
|
@ -40,7 +40,7 @@ data class PortfolioState(val portfolio: List<StateRef>,
|
||||
}
|
||||
|
||||
override fun generateRevision(notary: Party, oldState: StateAndRef<*>, updatedValue: Update): TransactionBuilder {
|
||||
require(oldState.state.data == this)
|
||||
require(oldState.state.data == this){"Old state data does not match current state data"}
|
||||
val portfolio = updatedValue.portfolio ?: portfolio
|
||||
val valuation = updatedValue.valuation ?: valuation
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
package net.corda.serialization.internal
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
|
||||
/**
|
||||
* Drop-in replacement for [AttachmentsClassLoaderBuilder] in the serialization module.
|
||||
* This version is not strongly-coupled to [net.corda.core.node.ServiceHub].
|
||||
*/
|
||||
@Suppress("UNUSED", "UNUSED_PARAMETER")
|
||||
internal class AttachmentsClassLoaderBuilder() {
|
||||
fun build(attachmentHashes: List<SecureHash>, properties: Map<Any, Any>, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? = null
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package net.corda.serialization.internal
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
import java.net.URLStreamHandler
|
||||
import java.security.CodeSigner
|
||||
import java.security.CodeSource
|
||||
import java.security.SecureClassLoader
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
|
||||
* need to provide JAR streams, and so could be fetched from a database, local disk, etc. Constructing an
|
||||
* AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping
|
||||
* file paths.
|
||||
*/
|
||||
@KeepForDJVM
|
||||
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) : SecureClassLoader(parent) {
|
||||
private val pathsToAttachments = HashMap<String, Attachment>()
|
||||
private val idsToAttachments = HashMap<SecureHash, Attachment>()
|
||||
|
||||
@CordaSerializable
|
||||
class OverlappingAttachments(val path: String) : Exception() {
|
||||
override fun toString() = "Multiple attachments define a file at path $path"
|
||||
}
|
||||
|
||||
init {
|
||||
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
|
||||
"Attempting to load Contract Attachments downloaded from the network"
|
||||
}
|
||||
|
||||
for (attachment in attachments) {
|
||||
attachment.openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
val entry = jar.nextJarEntry ?: break
|
||||
|
||||
// We already verified that paths are not strange/game playing when we inserted the attachment
|
||||
// into the storage service. So we don't need to repeat it here.
|
||||
//
|
||||
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
|
||||
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
|
||||
//
|
||||
// Also convert to Unix path separators as all resource/class lookups will expect this.
|
||||
val path = entry.name.toLowerCase().replace('\\', '/')
|
||||
if (path in pathsToAttachments)
|
||||
throw OverlappingAttachments(path)
|
||||
pathsToAttachments[path] = attachment
|
||||
}
|
||||
}
|
||||
idsToAttachments[attachment.id] = attachment
|
||||
}
|
||||
}
|
||||
|
||||
// Example: attachment://0b4fc1327f3bbebf1bfe98330ea402ae035936c3cb6da9bd3e26eeaa9584e74d/some/file.txt
|
||||
//
|
||||
// We have to provide a fake stream handler to satisfy the URL class that the scheme is known. But it's not
|
||||
// a real scheme and we don't register it. It's just here to ensure that there aren't codepaths that could
|
||||
// lead to data loading that we don't control right here in this class (URLs can have evil security properties!)
|
||||
private val fakeStreamHandler = object : URLStreamHandler() {
|
||||
override fun openConnection(u: URL?): URLConnection? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Attachment.toURL(path: String?) = URL(null, "attachment://$id/" + (path ?: ""), fakeStreamHandler)
|
||||
|
||||
override fun findClass(name: String): Class<*> {
|
||||
val path = name.replace('.', '/').toLowerCase() + ".class"
|
||||
val attachment = pathsToAttachments[path] ?: throw ClassNotFoundException(name)
|
||||
val stream = ByteArrayOutputStream()
|
||||
try {
|
||||
attachment.extractFile(path, stream)
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw ClassNotFoundException(name)
|
||||
}
|
||||
val bytes = stream.toByteArray()
|
||||
// We don't attempt to propagate signatures from the JAR into the codesource, because our sandbox does not
|
||||
// depend on external policy files to specify what it can do, so the data wouldn't be useful.
|
||||
val codesource = CodeSource(attachment.toURL(null), emptyArray<CodeSigner>())
|
||||
// TODO: Define an empty ProtectionDomain to start enforcing the standard Java sandbox.
|
||||
// The standard Java sandbox is insufficient for our needs and a much more sophisticated sandboxing
|
||||
// ClassLoader will appear here in future, but it can't hurt to use the default one too: defence in depth!
|
||||
return defineClass(name, bytes, 0, bytes.size, codesource)
|
||||
}
|
||||
|
||||
override fun findResource(name: String): URL? {
|
||||
val attachment = pathsToAttachments[name.toLowerCase()] ?: return null
|
||||
return attachment.toURL(name)
|
||||
}
|
||||
|
||||
override fun getResourceAsStream(name: String): InputStream? {
|
||||
val url = getResource(name) ?: return null // May check parent classloaders, for example.
|
||||
if (url.protocol != "attachment") return null
|
||||
val attachment = idsToAttachments[SecureHash.parse(url.host)] ?: return null
|
||||
val path = url.path?.substring(1) ?: return null // Chop off the leading slash.
|
||||
return try {
|
||||
val stream = ByteArrayOutputStream()
|
||||
attachment.extractFile(path, stream)
|
||||
stream.toByteArray().inputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,15 +16,6 @@ data class CheckpointSerializationContextImpl @JvmOverloads constructor(
|
||||
override val objectReferencesEnabled: Boolean,
|
||||
override val encoding: SerializationEncoding?,
|
||||
override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : CheckpointSerializationContext {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Unsupported for checkpoints.
|
||||
*/
|
||||
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): CheckpointSerializationContext {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun withProperty(property: Any, value: Any): CheckpointSerializationContext {
|
||||
return copy(properties = properties + (property to value))
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.copyBytes
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.AttachmentsClassLoader
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.serialization.internal.amqp.amqpMagic
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -31,20 +32,12 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
|
||||
override val useCase: SerializationContext.UseCase,
|
||||
override val encoding: SerializationEncoding?,
|
||||
override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist,
|
||||
override val lenientCarpenterEnabled: Boolean = false,
|
||||
private val builder: AttachmentsClassLoaderBuilder = AttachmentsClassLoaderBuilder()
|
||||
) : SerializationContext {
|
||||
|
||||
|
||||
override val lenientCarpenterEnabled: Boolean = false) : SerializationContext {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context.
|
||||
*/
|
||||
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
|
||||
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this
|
||||
val classLoader = builder.build(attachmentHashes, properties, deserializationClassLoader) ?: return this
|
||||
return withClassLoader(classLoader)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun withProperty(property: Any, value: Any): SerializationContext {
|
||||
@ -72,34 +65,6 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe
|
||||
override fun withEncodingWhitelist(encodingWhitelist: EncodingWhitelist) = copy(encodingWhitelist = encodingWhitelist)
|
||||
}
|
||||
|
||||
/*
|
||||
* This class is internal rather than private so that serialization-deterministic
|
||||
* can replace it with an alternative version.
|
||||
*/
|
||||
@DeleteForDJVM
|
||||
class AttachmentsClassLoaderBuilder() {
|
||||
private val cache: Cache<Pair<List<SecureHash>, ClassLoader>, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build()
|
||||
|
||||
fun build(attachmentHashes: List<SecureHash>, properties: Map<Any, Any>, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? {
|
||||
val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContext ?: return null // Some tests don't set one.
|
||||
try {
|
||||
return cache.get(Pair(attachmentHashes, deserializationClassLoader)) {
|
||||
val missing = ArrayList<SecureHash>()
|
||||
val attachments = ArrayList<Attachment>()
|
||||
attachmentHashes.forEach { id ->
|
||||
serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it }
|
||||
?: run { missing += id }
|
||||
}
|
||||
missing.isNotEmpty() && throw MissingAttachmentsException(missing)
|
||||
AttachmentsClassLoader(attachments, parent = deserializationClassLoader)
|
||||
}!!
|
||||
} catch (e: ExecutionException) {
|
||||
// Caught from within the cache get, so unwrap.
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@KeepForDJVM
|
||||
open class SerializationFactoryImpl(
|
||||
// TODO: This is read-mostly. Probably a faster implementation to be found.
|
||||
|
@ -0,0 +1,209 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.*
|
||||
import java.io.NotSerializableException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Interprets AMQP [Schema] information to obtain [RemoteTypeInformation], caching by [TypeDescriptor].
|
||||
*/
|
||||
class AMQPRemoteTypeModel {
|
||||
|
||||
private val cache: MutableMap<TypeDescriptor, RemoteTypeInformation> = DefaultCacheProvider.createCache()
|
||||
|
||||
/**
|
||||
* Interpret a [Schema] to obtain a [Map] of all of the [RemoteTypeInformation] contained therein, indexed by
|
||||
* [TypeDescriptor].
|
||||
*
|
||||
* A [Schema] contains a set of [TypeNotation]s, which we recursively convert into [RemoteTypeInformation],
|
||||
* associating each new piece of [RemoteTypeInformation] with the [TypeDescriptor] attached to it in the schema.
|
||||
*
|
||||
* We start by building a [Map] of [TypeNotation] by [TypeIdentifier], using [AMQPTypeIdentifierParser] to convert
|
||||
* AMQP type names into [TypeIdentifier]s. This is used as a lookup for resolving notations that are referred to by
|
||||
* type name from other notations, e.g. the types of properties.
|
||||
*
|
||||
* We also build a [Map] of [TypeNotation] by [TypeDescriptor], which we then convert into [RemoteTypeInformation]
|
||||
* while merging with the cache.
|
||||
*/
|
||||
fun interpret(serializationSchemas: SerializationSchemas): Map<TypeDescriptor, RemoteTypeInformation> {
|
||||
val (schema, transforms) = serializationSchemas
|
||||
val notationLookup = schema.types.associateBy { it.name.typeIdentifier }
|
||||
val byTypeDescriptor = schema.types.associateBy { it.typeDescriptor }
|
||||
val enumTransformsLookup = transforms.types.asSequence().map { (name, transformSet) ->
|
||||
name.typeIdentifier to interpretTransformSet(transformSet)
|
||||
}.toMap()
|
||||
|
||||
val interpretationState = InterpretationState(notationLookup, enumTransformsLookup, cache, emptySet())
|
||||
|
||||
return byTypeDescriptor.mapValues { (typeDescriptor, typeNotation) ->
|
||||
cache.getOrPut(typeDescriptor) { interpretationState.run { typeNotation.name.typeIdentifier.interpretIdentifier() } }
|
||||
}
|
||||
}
|
||||
|
||||
data class InterpretationState(val notationLookup: Map<TypeIdentifier, TypeNotation>,
|
||||
val enumTransformsLookup: Map<TypeIdentifier, EnumTransforms>,
|
||||
val cache: MutableMap<TypeDescriptor, RemoteTypeInformation>,
|
||||
val seen: Set<TypeIdentifier>) {
|
||||
|
||||
private inline fun <T> forgetSeen(block: InterpretationState.() -> T): T =
|
||||
withSeen(emptySet(), block)
|
||||
|
||||
private inline fun <T> withSeen(typeIdentifier: TypeIdentifier, block: InterpretationState.() -> T): T =
|
||||
withSeen(seen + typeIdentifier, block)
|
||||
|
||||
private inline fun <T> withSeen(seen: Set<TypeIdentifier>, block: InterpretationState.() -> T): T =
|
||||
copy(seen = seen).run(block)
|
||||
|
||||
/**
|
||||
* Follow a [TypeIdentifier] to the [TypeNotation] associated with it in the lookup, and interpret that notation.
|
||||
* If there is no such notation, interpret the [TypeIdentifier] directly into [RemoteTypeInformation].
|
||||
*
|
||||
* If we have visited this [TypeIdentifier] before while traversing the graph of related [TypeNotation]s, then we
|
||||
* know we have hit a cycle and respond accordingly.
|
||||
*/
|
||||
fun TypeIdentifier.interpretIdentifier(): RemoteTypeInformation =
|
||||
if (this in seen) RemoteTypeInformation.Cycle(this) { forgetSeen { interpretIdentifier() } }
|
||||
else withSeen(this) {
|
||||
val identifier = this@interpretIdentifier
|
||||
notationLookup[identifier]?.interpretNotation(identifier) ?: interpretNoNotation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Either fetch from the cache, or interpret, cache, and return, the [RemoteTypeInformation] corresponding to this
|
||||
* [TypeNotation].
|
||||
*/
|
||||
private fun TypeNotation.interpretNotation(identifier: TypeIdentifier): RemoteTypeInformation =
|
||||
cache.getOrPut(typeDescriptor) {
|
||||
when (this) {
|
||||
is CompositeType -> interpretComposite(identifier)
|
||||
is RestrictedType -> interpretRestricted(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret the properties, interfaces and type parameters in this [TypeNotation], and return suitable
|
||||
* [RemoteTypeInformation].
|
||||
*/
|
||||
private fun CompositeType.interpretComposite(identifier: TypeIdentifier): RemoteTypeInformation {
|
||||
val properties = fields.asSequence().map { it.interpret() }.toMap()
|
||||
val typeParameters = identifier.interpretTypeParameters()
|
||||
val interfaceIdentifiers = provides.map { name -> name.typeIdentifier }
|
||||
val isInterface = identifier in interfaceIdentifiers
|
||||
val interfaces = interfaceIdentifiers.mapNotNull { interfaceIdentifier ->
|
||||
if (interfaceIdentifier == identifier) null
|
||||
else interfaceIdentifier.interpretIdentifier()
|
||||
}
|
||||
|
||||
return if (isInterface) RemoteTypeInformation.AnInterface(typeDescriptor, identifier, properties, interfaces, typeParameters)
|
||||
else RemoteTypeInformation.Composable(typeDescriptor, identifier, properties, interfaces, typeParameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type parameters are read off from the [TypeIdentifier] we translated the AMQP type name into.
|
||||
*/
|
||||
private fun TypeIdentifier.interpretTypeParameters(): List<RemoteTypeInformation> = when (this) {
|
||||
is TypeIdentifier.Parameterised -> parameters.map { it.interpretIdentifier() }
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret a [RestrictedType] into suitable [RemoteTypeInformation].
|
||||
*/
|
||||
private fun RestrictedType.interpretRestricted(identifier: TypeIdentifier): RemoteTypeInformation = when (identifier) {
|
||||
is TypeIdentifier.Parameterised ->
|
||||
RemoteTypeInformation.Parameterised(
|
||||
typeDescriptor,
|
||||
identifier,
|
||||
identifier.interpretTypeParameters())
|
||||
is TypeIdentifier.ArrayOf ->
|
||||
RemoteTypeInformation.AnArray(
|
||||
typeDescriptor,
|
||||
identifier,
|
||||
identifier.componentType.interpretIdentifier())
|
||||
is TypeIdentifier.Unparameterised ->
|
||||
if (choices.isEmpty()) {
|
||||
RemoteTypeInformation.Unparameterised(
|
||||
typeDescriptor,
|
||||
identifier)
|
||||
} else RemoteTypeInformation.AnEnum(
|
||||
typeDescriptor,
|
||||
identifier,
|
||||
choices.map { it.name },
|
||||
enumTransformsLookup[identifier] ?: EnumTransforms.empty)
|
||||
else -> throw NotSerializableException("Cannot interpret restricted type $this")
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret a [Field] into a name/[RemotePropertyInformation] pair.
|
||||
*/
|
||||
private fun Field.interpret(): Pair<String, RemotePropertyInformation> {
|
||||
val identifier = type.typeIdentifier
|
||||
|
||||
// A type of "*" is replaced with the value of the "requires" field
|
||||
val fieldTypeIdentifier = if (identifier == TypeIdentifier.TopType && !requires.isEmpty()) {
|
||||
requires[0].typeIdentifier
|
||||
} else identifier
|
||||
|
||||
// We convert Java Object types to Java primitive types if the field is mandatory.
|
||||
val fieldType = fieldTypeIdentifier.forcePrimitive(mandatory).interpretIdentifier()
|
||||
|
||||
return name to RemotePropertyInformation(
|
||||
fieldType,
|
||||
mandatory)
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is no [TypeNotation] in the [Schema] matching a given [TypeIdentifier], we interpret the [TypeIdentifier]
|
||||
* directly.
|
||||
*/
|
||||
private fun TypeIdentifier.interpretNoNotation(): RemoteTypeInformation =
|
||||
when (this) {
|
||||
is TypeIdentifier.TopType -> RemoteTypeInformation.Top
|
||||
is TypeIdentifier.UnknownType -> RemoteTypeInformation.Unknown
|
||||
is TypeIdentifier.ArrayOf ->
|
||||
RemoteTypeInformation.AnArray(
|
||||
name,
|
||||
this,
|
||||
componentType.interpretIdentifier())
|
||||
is TypeIdentifier.Parameterised ->
|
||||
RemoteTypeInformation.Parameterised(
|
||||
name,
|
||||
this,
|
||||
parameters.map { it.interpretIdentifier() })
|
||||
else -> RemoteTypeInformation.Unparameterised(name, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun interpretTransformSet(transformSet: EnumMap<TransformTypes, MutableList<Transform>>): EnumTransforms {
|
||||
val defaultTransforms = transformSet[TransformTypes.EnumDefault]?.toList() ?: emptyList()
|
||||
val defaults = defaultTransforms.associate { transform -> (transform as EnumDefaultSchemaTransform).new to transform.old }
|
||||
val renameTransforms = transformSet[TransformTypes.Rename]?.toList() ?: emptyList()
|
||||
val renames = renameTransforms.associate { transform -> (transform as RenameSchemaTransform).to to transform.from }
|
||||
|
||||
return EnumTransforms(defaults, renames)
|
||||
}
|
||||
|
||||
private val TypeNotation.typeDescriptor: String get() = descriptor.name?.toString() ?:
|
||||
throw NotSerializableException("Type notation has no type descriptor: $this")
|
||||
|
||||
private val String.typeIdentifier get(): TypeIdentifier = AMQPTypeIdentifierParser.parse(this)
|
||||
|
||||
/**
|
||||
* Force e.g. [java.lang.Integer] to `int`, if it is the type of a mandatory field.
|
||||
*/
|
||||
private fun TypeIdentifier.forcePrimitive(mandatory: Boolean) =
|
||||
if (mandatory) primitives[this] ?: this
|
||||
else this
|
||||
|
||||
private val primitives = sequenceOf(
|
||||
Boolean::class,
|
||||
Byte::class,
|
||||
Char::class,
|
||||
Int::class,
|
||||
Short::class,
|
||||
Long::class,
|
||||
Float::class,
|
||||
Double::class).associate {
|
||||
TypeIdentifier.forClass(it.javaObjectType) to TypeIdentifier.forClass(it.javaPrimitiveType!!)
|
||||
}
|
@ -172,7 +172,7 @@ abstract class AbstractAMQPSerializationScheme(
|
||||
// Not used as a simple direct import to facilitate testing
|
||||
open val publicKeySerializer: CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer
|
||||
|
||||
private fun getSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
fun getSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
val key = Pair(context.whitelist, context.deserializationClassLoader)
|
||||
// ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already.
|
||||
return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) {
|
||||
|
@ -0,0 +1,190 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.StringBuilder
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Thrown if the type string parser enters an illegal state.
|
||||
*/
|
||||
class IllegalTypeNameParserStateException(message: String): NotSerializableException(message)
|
||||
|
||||
/**
|
||||
* Provides a state machine which knows how to parse AMQP type strings into [TypeIdentifier]s.
|
||||
*/
|
||||
object AMQPTypeIdentifierParser {
|
||||
|
||||
internal const val MAX_TYPE_PARAM_DEPTH = 32
|
||||
private const val MAX_ARRAY_DEPTH = 32
|
||||
|
||||
/**
|
||||
* Given a string representing a serialized AMQP type, construct a TypeIdentifier for that string.
|
||||
*
|
||||
* @param typeString The AMQP type string to parse
|
||||
* @return A [TypeIdentifier] representing the type represented by the input string.
|
||||
*/
|
||||
fun parse(typeString: String): TypeIdentifier {
|
||||
validate(typeString)
|
||||
return typeString.fold<ParseState>(ParseState.ParsingRawType(null)) { state, c ->
|
||||
state.accept(c)
|
||||
}.getTypeIdentifier()
|
||||
}
|
||||
|
||||
// Make sure our inputs aren't designed to blow things up.
|
||||
private fun validate(typeString: String) {
|
||||
var maxTypeParamDepth = 0
|
||||
var typeParamdepth = 0
|
||||
|
||||
var maxArrayDepth = 0
|
||||
var wasArray = false
|
||||
var arrayDepth = 0
|
||||
|
||||
for (c in typeString) {
|
||||
if (c.isWhitespace() || c.isJavaIdentifierPart() || c.isJavaIdentifierStart() ||
|
||||
c == '.' || c == ',' || c == '?' || c == '*') continue
|
||||
|
||||
when(c) {
|
||||
'<' -> maxTypeParamDepth = Math.max(++typeParamdepth, typeParamdepth)
|
||||
'>' -> typeParamdepth--
|
||||
'[' -> {
|
||||
arrayDepth = if (wasArray) arrayDepth + 2 else 1
|
||||
maxArrayDepth = Math.max(maxArrayDepth,arrayDepth)
|
||||
}
|
||||
']' -> arrayDepth--
|
||||
else -> throw IllegalTypeNameParserStateException("Type name '$typeString' contains illegal character '$c'")
|
||||
}
|
||||
wasArray = c == ']'
|
||||
}
|
||||
if (maxTypeParamDepth >= MAX_TYPE_PARAM_DEPTH)
|
||||
throw IllegalTypeNameParserStateException("Nested depth of type parameters exceeds maximum of $MAX_TYPE_PARAM_DEPTH")
|
||||
|
||||
if (maxArrayDepth >= MAX_ARRAY_DEPTH)
|
||||
throw IllegalTypeNameParserStateException("Nested depth of arrays exceeds maximum of $MAX_ARRAY_DEPTH")
|
||||
}
|
||||
|
||||
private sealed class ParseState {
|
||||
abstract val parent: ParseState.ParsingParameterList?
|
||||
abstract fun accept(c: Char): ParseState
|
||||
abstract fun getTypeIdentifier(): TypeIdentifier
|
||||
|
||||
fun unexpected(c: Char): ParseState = throw IllegalTypeNameParserStateException("Unexpected character: '$c'")
|
||||
fun notInParameterList(c: Char): ParseState =
|
||||
throw IllegalTypeNameParserStateException("'$c' encountered, but not parsing type parameter list")
|
||||
|
||||
/**
|
||||
* We are parsing a raw type name, either at the top level or as part of a list of type parameters.
|
||||
*/
|
||||
data class ParsingRawType(override val parent: ParseState.ParsingParameterList?, val buffer: StringBuilder = StringBuilder()) : ParseState() {
|
||||
override fun accept(c: Char) = when (c) {
|
||||
',' ->
|
||||
if (parent == null) notInParameterList(c)
|
||||
else ParsingRawType(parent.addParameter(getTypeIdentifier()))
|
||||
'[' -> ParsingArray(getTypeIdentifier(), parent)
|
||||
']' -> unexpected(c)
|
||||
'<' -> ParsingRawType(ParsingParameterList(getTypeName(), parent))
|
||||
'>' -> parent?.addParameter(getTypeIdentifier())?.accept(c) ?: notInParameterList(c)
|
||||
else -> apply { buffer.append(c) }
|
||||
}
|
||||
|
||||
private fun getTypeName(): String {
|
||||
val typeName = buffer.toString().trim()
|
||||
if (typeName.contains(' '))
|
||||
throw IllegalTypeNameParserStateException("Illegal whitespace in type name $typeName")
|
||||
return typeName
|
||||
}
|
||||
|
||||
override fun getTypeIdentifier(): TypeIdentifier {
|
||||
val typeName = getTypeName()
|
||||
return when (typeName) {
|
||||
"*" -> TypeIdentifier.TopType
|
||||
"?" -> TypeIdentifier.UnknownType
|
||||
in simplified -> simplified[typeName]!!
|
||||
else -> TypeIdentifier.Unparameterised(typeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We are parsing a parameter list, and expect either to start a new parameter, add array-ness to the last
|
||||
* parameter we have, or end the list.
|
||||
*/
|
||||
data class ParsingParameterList(val typeName: String, override val parent: ParsingParameterList?, val parameters: List<TypeIdentifier> = emptyList()) : ParseState() {
|
||||
override fun accept(c: Char) = when (c) {
|
||||
' ' -> this
|
||||
',' -> ParsingRawType(this)
|
||||
'[' ->
|
||||
if (parameters.isEmpty()) unexpected(c)
|
||||
else ParsingArray(
|
||||
// Start adding array-ness to the last parameter we have.
|
||||
parameters[parameters.lastIndex],
|
||||
// Take a copy of this state, dropping the last parameter which will be added back on
|
||||
// when array parsing completes.
|
||||
copy(parameters = parameters.subList(0, parameters.lastIndex)))
|
||||
'>' -> parent?.addParameter(getTypeIdentifier()) ?: Complete(getTypeIdentifier())
|
||||
else -> unexpected(c)
|
||||
}
|
||||
|
||||
fun addParameter(parameter: TypeIdentifier) = copy(parameters = parameters + parameter)
|
||||
|
||||
override fun getTypeIdentifier() = TypeIdentifier.Parameterised(typeName, null, parameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* We are adding array-ness to some type identifier.
|
||||
*/
|
||||
data class ParsingArray(val componentType: TypeIdentifier, override val parent: ParseState.ParsingParameterList?) : ParseState() {
|
||||
override fun accept(c: Char) = when (c) {
|
||||
' ' -> this
|
||||
'p' -> ParsingArray(forcePrimitive(componentType), parent)
|
||||
']' -> parent?.addParameter(getTypeIdentifier()) ?: Complete(getTypeIdentifier())
|
||||
else -> unexpected(c)
|
||||
}
|
||||
|
||||
override fun getTypeIdentifier() = TypeIdentifier.ArrayOf(componentType)
|
||||
|
||||
private fun forcePrimitive(componentType: TypeIdentifier): TypeIdentifier =
|
||||
TypeIdentifier.forClass(Primitives.unwrap(componentType.getLocalType().asClass()))
|
||||
}
|
||||
|
||||
/**
|
||||
* We have a complete type identifier, and all we can do to it is add array-ness.
|
||||
*/
|
||||
data class Complete(val identifier: TypeIdentifier) : ParseState() {
|
||||
override val parent: ParseState.ParsingParameterList? get() = null
|
||||
override fun accept(c: Char): ParseState = when (c) {
|
||||
' ' -> this
|
||||
'[' -> ParsingArray(identifier, null)
|
||||
else -> unexpected(c)
|
||||
}
|
||||
|
||||
override fun getTypeIdentifier() = identifier
|
||||
}
|
||||
}
|
||||
|
||||
private val simplified = mapOf(
|
||||
"string" to String::class,
|
||||
"boolean" to Boolean::class,
|
||||
"byte" to Byte::class,
|
||||
"char" to Char::class,
|
||||
"int" to Int::class,
|
||||
"short" to Short::class,
|
||||
"long" to Long::class,
|
||||
"double" to Double::class,
|
||||
"float" to Float::class,
|
||||
"ubyte" to UnsignedByte::class,
|
||||
"uint" to UnsignedInteger::class,
|
||||
"ushort" to UnsignedShort::class,
|
||||
"ulong" to UnsignedLong::class,
|
||||
"decimal32" to Decimal32::class,
|
||||
"decimal64" to Decimal64::class,
|
||||
"decimal128" to Decimal128::class,
|
||||
"binary" to ByteArray::class,
|
||||
"timestamp" to Date::class,
|
||||
"uuid" to UUID::class,
|
||||
"symbol" to Symbol::class).mapValues { (_, v) ->
|
||||
TypeIdentifier.forClass(v.javaObjectType)
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
object AMQPTypeIdentifiers {
|
||||
fun isPrimitive(type: Type): Boolean = isPrimitive(TypeIdentifier.forGenericType(type))
|
||||
fun isPrimitive(typeIdentifier: TypeIdentifier) = typeIdentifier in primitiveTypeNamesByName
|
||||
|
||||
fun primitiveTypeName(type: Type): String? =
|
||||
primitiveTypeNamesByName[TypeIdentifier.forGenericType(type)]
|
||||
|
||||
private val primitiveTypeNamesByName = sequenceOf(
|
||||
Character::class to "char",
|
||||
Char::class to "char",
|
||||
Boolean::class to "boolean",
|
||||
Byte::class to "byte",
|
||||
UnsignedByte::class to "ubyte",
|
||||
Short::class to "short",
|
||||
UnsignedShort::class to "ushort",
|
||||
Int::class to "int",
|
||||
UnsignedInteger::class to "uint",
|
||||
Long::class to "long",
|
||||
UnsignedLong::class to "ulong",
|
||||
Float::class to "float",
|
||||
Double::class to "double",
|
||||
Decimal32::class to "decimal32",
|
||||
Decimal64::class to "decimal62",
|
||||
Decimal128::class to "decimal128",
|
||||
Date::class to "timestamp",
|
||||
UUID::class to "uuid",
|
||||
ByteArray::class to "binary",
|
||||
String::class to "string",
|
||||
Symbol::class to "symbol")
|
||||
.flatMap { (klass, name) ->
|
||||
val typeIdentifier = TypeIdentifier.forClass(klass.javaObjectType)
|
||||
val primitiveTypeIdentifier = klass.javaPrimitiveType?.let { TypeIdentifier.forClass(it) }
|
||||
if (primitiveTypeIdentifier == null) sequenceOf(typeIdentifier to name)
|
||||
else sequenceOf(typeIdentifier to name, primitiveTypeIdentifier to name)
|
||||
}.toMap()
|
||||
|
||||
fun nameForType(typeIdentifier: TypeIdentifier): String = when(typeIdentifier) {
|
||||
is TypeIdentifier.Erased -> typeIdentifier.name
|
||||
is TypeIdentifier.Unparameterised -> primitiveTypeNamesByName[typeIdentifier] ?: typeIdentifier.name
|
||||
is TypeIdentifier.UnknownType,
|
||||
is TypeIdentifier.TopType -> "?"
|
||||
is TypeIdentifier.ArrayOf ->
|
||||
if (typeIdentifier == primitiveByteArrayType) "binary"
|
||||
else nameForType(typeIdentifier.componentType) +
|
||||
if (typeIdentifier.componentType is TypeIdentifier.Unparameterised &&
|
||||
typeIdentifier.componentType.isPrimitive) "[p]"
|
||||
else "[]"
|
||||
is TypeIdentifier.Parameterised -> typeIdentifier.name + typeIdentifier.parameters.joinToString(", ", "<", ">") {
|
||||
nameForType(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val primitiveByteArrayType = TypeIdentifier.ArrayOf(TypeIdentifier.forClass(Byte::class.javaPrimitiveType!!))
|
||||
|
||||
fun nameForType(type: Type): String = nameForType(TypeIdentifier.forGenericType(type))
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.model.DefaultCacheProvider
|
||||
import net.corda.serialization.internal.model.TypeIdentifier
|
||||
import java.lang.reflect.Type
|
||||
|
||||
interface CustomSerializerRegistry {
|
||||
/**
|
||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||
* that expects to find getters and a constructor with a parameter for each property.
|
||||
*/
|
||||
fun register(customSerializer: CustomSerializer<out Any>)
|
||||
fun registerExternal(customSerializer: CorDappCustomSerializer)
|
||||
|
||||
fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>?
|
||||
}
|
||||
|
||||
class CachingCustomSerializerRegistry(
|
||||
private val descriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry)
|
||||
: CustomSerializerRegistry {
|
||||
|
||||
companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
private data class CustomSerializerIdentifier(val actualTypeIdentifier: TypeIdentifier, val declaredTypeIdentifier: TypeIdentifier)
|
||||
|
||||
private val customSerializersCache: MutableMap<CustomSerializerIdentifier, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
private var customSerializers: List<SerializerFor> = emptyList()
|
||||
|
||||
/**
|
||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||
* that expects to find getters and a constructor with a parameter for each property.
|
||||
*/
|
||||
override fun register(customSerializer: CustomSerializer<out Any>) {
|
||||
logger.trace("action=\"Registering custom serializer\", class=\"${customSerializer.type}\"")
|
||||
|
||||
descriptorBasedSerializerRegistry.getOrBuild(customSerializer.typeDescriptor.toString()) {
|
||||
customSerializers += customSerializer
|
||||
for (additional in customSerializer.additionalSerializers) {
|
||||
register(additional)
|
||||
}
|
||||
customSerializer
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
||||
logger.trace("action=\"Registering external serializer\", class=\"${customSerializer.type}\"")
|
||||
|
||||
descriptorBasedSerializerRegistry.getOrBuild(customSerializer.typeDescriptor.toString()) {
|
||||
customSerializers += customSerializer
|
||||
customSerializer
|
||||
}
|
||||
}
|
||||
|
||||
override fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
|
||||
val typeIdentifier = CustomSerializerIdentifier(
|
||||
TypeIdentifier.forClass(clazz),
|
||||
TypeIdentifier.forGenericType(declaredType))
|
||||
|
||||
return customSerializersCache[typeIdentifier]
|
||||
?: doFindCustomSerializer(clazz, declaredType)?.also { serializer ->
|
||||
customSerializersCache.putIfAbsent(typeIdentifier, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doFindCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
|
||||
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
|
||||
// AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
|
||||
// super type. Could be done, but do we need it?
|
||||
for (customSerializer in customSerializers) {
|
||||
if (customSerializer.isSerializerFor(clazz)) {
|
||||
val declaredSuperClass = declaredType.asClass().superclass
|
||||
|
||||
return if (declaredSuperClass == null
|
||||
|| !customSerializer.isSerializerFor(declaredSuperClass)
|
||||
|| !customSerializer.revealSubclassesInSchema
|
||||
) {
|
||||
logger.debug("action=\"Using custom serializer\", class=${clazz.typeName}, " +
|
||||
"declaredType=${declaredType.typeName}")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
customSerializer as? AMQPSerializer<Any>
|
||||
} else {
|
||||
// Make a subclass serializer for the subclass and return that...
|
||||
CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import net.corda.serialization.internal.model.DefaultCacheProvider
|
||||
|
||||
/**
|
||||
* The quickest way to find a serializer, if one has already been generated, is to look it up by type descriptor.
|
||||
*
|
||||
* This registry gets shared around between various participants that might want to use it as a lookup, or register
|
||||
* serialisers that they have created with it.
|
||||
*/
|
||||
interface DescriptorBasedSerializerRegistry {
|
||||
operator fun get(descriptor: String): AMQPSerializer<Any>?
|
||||
operator fun set(descriptor: String, serializer: AMQPSerializer<Any>)
|
||||
fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>): AMQPSerializer<Any>
|
||||
}
|
||||
|
||||
class DefaultDescriptorBasedSerializerRegistry: DescriptorBasedSerializerRegistry {
|
||||
|
||||
private val registry: MutableMap<String, AMQPSerializer<Any>> = DefaultCacheProvider.createCache()
|
||||
|
||||
override fun get(descriptor: String): AMQPSerializer<Any>? = registry[descriptor]
|
||||
|
||||
override fun set(descriptor: String, serializer: AMQPSerializer<Any>) {
|
||||
registry.putIfAbsent(descriptor, serializer)
|
||||
}
|
||||
|
||||
override fun getOrBuild(descriptor: String, builder: () -> AMQPSerializer<Any>) =
|
||||
get(descriptor) ?: builder().also { newSerializer -> this[descriptor] = newSerializer }
|
||||
}
|
@ -52,6 +52,8 @@ class PublicPropertyReader(private val readMethod: Method) : PropertyReader() {
|
||||
}
|
||||
|
||||
override fun isNullable(): Boolean = readMethod.returnsNullable()
|
||||
|
||||
val genericReturnType get() = readMethod.genericReturnType
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,7 +118,7 @@ interface SerializerFactory {
|
||||
Float::class.java to "float",
|
||||
Double::class.java to "double",
|
||||
Decimal32::class.java to "decimal32",
|
||||
Decimal64::class.java to "decimal62",
|
||||
Decimal64::class.java to "decimal64",
|
||||
Decimal128::class.java to "decimal128",
|
||||
Date::class.java to "timestamp",
|
||||
UUID::class.java to "uuid",
|
||||
|
@ -0,0 +1,45 @@
|
||||
package net.corda.serialization.internal.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.serialization.internal.model.LocalTypeModelConfiguration
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [LocalTypeModelConfiguration] based on a [ClassWhitelist]
|
||||
*/
|
||||
class WhitelistBasedTypeModelConfiguration(
|
||||
private val whitelist: ClassWhitelist,
|
||||
private val customSerializerRegistry: CustomSerializerRegistry)
|
||||
: LocalTypeModelConfiguration {
|
||||
override fun isExcluded(type: Type): Boolean = whitelist.isNotWhitelisted(type.asClass())
|
||||
override fun isOpaque(type: Type): Boolean = Primitives.unwrap(type.asClass()) in opaqueTypes ||
|
||||
customSerializerRegistry.findCustomSerializer(type.asClass(), type) != null
|
||||
}
|
||||
|
||||
// Copied from SerializerFactory so that we can have equivalent behaviour, for now.
|
||||
private val opaqueTypes = setOf(
|
||||
Character::class.java,
|
||||
Char::class.java,
|
||||
Boolean::class.java,
|
||||
Byte::class.java,
|
||||
UnsignedByte::class.java,
|
||||
Short::class.java,
|
||||
UnsignedShort::class.java,
|
||||
Int::class.java,
|
||||
UnsignedInteger::class.java,
|
||||
Long::class.java,
|
||||
UnsignedLong::class.java,
|
||||
Float::class.java,
|
||||
Double::class.java,
|
||||
Decimal32::class.java,
|
||||
Decimal64::class.java,
|
||||
Decimal128::class.java,
|
||||
Date::class.java,
|
||||
UUID::class.java,
|
||||
ByteArray::class.java,
|
||||
String::class.java,
|
||||
Symbol::class.java
|
||||
)
|
@ -148,7 +148,9 @@ class ClassCarpenterImpl @JvmOverloads constructor (override val whitelist: Clas
|
||||
}
|
||||
}
|
||||
|
||||
require(schema.name in _loaded)
|
||||
if (schema.name !in _loaded){
|
||||
throw ClassNotFoundException(schema.name)
|
||||
}
|
||||
|
||||
return _loaded[schema.name]!!
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: Class
|
||||
// carpented class existing and remove it from their dependency list, If that
|
||||
// list is now empty we have no impediment to carpenting that class up
|
||||
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
|
||||
|
||||
require(newObject.name in schemas.dependencies[dependent]!!.second)
|
||||
|
||||
schemas.dependencies[dependent]?.second?.remove(newObject.name)
|
||||
|
@ -72,8 +72,7 @@ open class NonNullableField(field: Class<out Any?>) : ClassField(field) {
|
||||
}
|
||||
|
||||
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
||||
require(name != unsetName)
|
||||
|
||||
check(name != unsetName) {"Property this.name cannot be $unsetName"}
|
||||
if (!field.isPrimitive) {
|
||||
with(mv) {
|
||||
visitVarInsn(ALOAD, 0) // load this
|
||||
@ -109,7 +108,7 @@ class NullableField(field: Class<out Any?>) : ClassField(field) {
|
||||
}
|
||||
|
||||
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
||||
require(name != unsetName)
|
||||
require(name != unsetName){"Property this.name cannot be $unsetName"}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,124 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Once we have the complete graph of types requiring carpentry to hand, we can use it to sort those types in reverse-
|
||||
* dependency order, i.e. beginning with those types that have no dependencies on other types, then the types that
|
||||
* depended on those types, and so on. This means we can feed types directly to the [RemoteTypeCarpenter], and don't
|
||||
* have to use the [CarpenterMetaSchema].
|
||||
*
|
||||
* @param typesRequiringCarpentry The set of [RemoteTypeInformation] for types that are not reachable by the current
|
||||
* classloader.
|
||||
*/
|
||||
class CarpentryDependencyGraph private constructor(private val typesRequiringCarpentry: Set<RemoteTypeInformation>) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Sort the [typesRequiringCarpentry] into reverse-dependency order, then pass them to the provided
|
||||
* [Type]-builder, collating the results into a [Map] of [Type] by [TypeIdentifier]
|
||||
*/
|
||||
fun buildInReverseDependencyOrder(
|
||||
typesRequiringCarpentry: Set<RemoteTypeInformation>,
|
||||
getOrBuild: (RemoteTypeInformation) -> Type): Map<TypeIdentifier, Type> =
|
||||
CarpentryDependencyGraph(typesRequiringCarpentry).buildInOrder(getOrBuild)
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of inbound edges by node.
|
||||
*
|
||||
* A [RemoteTypeInformation] map key is a type that requires other types to have been constructed before it can be
|
||||
* constructed.
|
||||
*
|
||||
* Each [RemoteTypeInformation] in the corresponding [Set] map value is one of the types that the key-type depends on.
|
||||
*
|
||||
* No key ever maps to an empty set: types with no dependencies are not included in this map.
|
||||
*/
|
||||
private val dependencies = mutableMapOf<RemoteTypeInformation, MutableSet<RemoteTypeInformation>>()
|
||||
|
||||
/**
|
||||
* If it is in [typesRequiringCarpentry], then add an edge from [dependee] to this type to the [dependencies] graph.
|
||||
*/
|
||||
private fun RemoteTypeInformation.dependsOn(dependee: RemoteTypeInformation) = dependsOn(listOf(dependee))
|
||||
|
||||
/**
|
||||
* Add an edge from each of these [dependees] that are in [typesRequiringCarpentry] to this type to the
|
||||
* [dependencies] graph.
|
||||
*/
|
||||
private fun RemoteTypeInformation.dependsOn(dependees: Collection<RemoteTypeInformation>) {
|
||||
val dependeesInTypesRequiringCarpentry = dependees.filter { it in typesRequiringCarpentry }
|
||||
if (dependeesInTypesRequiringCarpentry.isEmpty()) return // we don't want to put empty sets into the map.
|
||||
dependencies.compute(this) { _, dependees ->
|
||||
dependees?.apply { addAll(dependeesInTypesRequiringCarpentry) } ?:
|
||||
dependeesInTypesRequiringCarpentry.toMutableSet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses each of the [typesRequiringCarpentry], building (or obtaining from a cache) the corresponding [Type]
|
||||
* and populating them into a [Map] of [Type] by [TypeIdentifier].
|
||||
*/
|
||||
private fun buildInOrder(getOrBuild: (RemoteTypeInformation) -> Type): Map<TypeIdentifier, Type> {
|
||||
typesRequiringCarpentry.forEach { it.recordDependencies() }
|
||||
|
||||
return topologicalSort(typesRequiringCarpentry).associate { information ->
|
||||
information.typeIdentifier to getOrBuild(information)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record appropriate dependencies for each type of [RemoteTypeInformation]
|
||||
*/
|
||||
private fun RemoteTypeInformation.recordDependencies() = when (this) {
|
||||
is RemoteTypeInformation.Composable -> {
|
||||
dependsOn(typeParameters)
|
||||
dependsOn(interfaces)
|
||||
dependsOn(properties.values.map { it.type })
|
||||
}
|
||||
is RemoteTypeInformation.AnInterface -> {
|
||||
dependsOn(typeParameters)
|
||||
dependsOn(interfaces)
|
||||
dependsOn(properties.values.map { it.type })
|
||||
}
|
||||
is RemoteTypeInformation.AnArray -> dependsOn(componentType)
|
||||
is RemoteTypeInformation.Parameterised -> dependsOn(typeParameters)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate out those [types] which have [noDependencies] from those which still have dependencies.
|
||||
*
|
||||
* Remove the types with no dependencies from the graph, identifying which types are left with no inbound dependees
|
||||
* as a result, then return the types with no dependencies concatenated with the [topologicalSort] of the remaining
|
||||
* types, minus the newly-independent types.
|
||||
*/
|
||||
private fun topologicalSort(
|
||||
types: Set<RemoteTypeInformation>,
|
||||
noDependencies: Set<RemoteTypeInformation> = types - dependencies.keys): Sequence<RemoteTypeInformation> {
|
||||
// Types which still have dependencies.
|
||||
val remaining = dependencies.keys.toSet()
|
||||
|
||||
// Remove the types which have no dependencies from the dependencies of the remaining types, and identify
|
||||
// those types which have no dependencies left after we've done this.
|
||||
val newlyIndependent = dependencies.asSequence().mapNotNull { (dependent, dependees) ->
|
||||
dependees.removeAll(noDependencies)
|
||||
if (dependees.isEmpty()) dependent else null
|
||||
}.toSet()
|
||||
|
||||
// If there are still types with dependencies, and we have no dependencies we can remove, then we can't continue.
|
||||
if (newlyIndependent.isEmpty() && dependencies.isNotEmpty()) {
|
||||
throw NotSerializableException(
|
||||
"Cannot build dependencies for " +
|
||||
dependencies.keys.map { it.typeIdentifier.prettyPrint(false) })
|
||||
}
|
||||
|
||||
// Remove the types which have no dependencies remaining, maintaining the invariant that no key maps to an
|
||||
// empty set.
|
||||
dependencies.keys.removeAll(newlyIndependent)
|
||||
|
||||
// Return the types that had no dependencies, then recurse to process the remainder.
|
||||
return noDependencies.asSequence() +
|
||||
if (dependencies.isEmpty()) newlyIndependent.asSequence() else topologicalSort(remaining, newlyIndependent)
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Represents the information we have about a property of a type.
|
||||
*/
|
||||
sealed class LocalPropertyInformation(val isCalculated: Boolean) {
|
||||
|
||||
/**
|
||||
* [LocalTypeInformation] for the type of the property.
|
||||
*/
|
||||
abstract val type: LocalTypeInformation
|
||||
|
||||
/**
|
||||
* True if the property is a primitive type or is flagged as non-nullable, false otherwise.
|
||||
*/
|
||||
abstract val isMandatory: Boolean
|
||||
|
||||
/**
|
||||
* A property of an interface, for which we have only a getter method.
|
||||
*
|
||||
* @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type.
|
||||
*/
|
||||
data class ReadOnlyProperty(val observedGetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||
|
||||
/**
|
||||
* A property for which we have both a getter, and a matching slot in an array of constructor parameters.
|
||||
*
|
||||
* @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type.
|
||||
* @param constructorSlot The [ConstructorSlot] to which the property corresponds, used to populate an array of
|
||||
* constructor arguments when creating instances of its owning type.
|
||||
*/
|
||||
data class ConstructorPairedProperty(val observedGetter: Method, val constructorSlot: ConstructorSlot, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||
|
||||
/**
|
||||
* A property for which we have no getter, but for which there is a backing field a matching slot in an array of
|
||||
* constructor parameters.
|
||||
*
|
||||
* @param observedField The field which can be used to obtain the value of this property from an instance of its owning type.
|
||||
* @param constructorSlot The [ConstructorSlot] to which the property corresponds, used to populate an array of
|
||||
* constructor arguments when creating instances of its owning type.
|
||||
*/
|
||||
data class PrivateConstructorPairedProperty(val observedField: Field, val constructorSlot: ConstructorSlot, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||
|
||||
/**
|
||||
* A property for which we have both getter and setter methods (usually belonging to a POJO which is initialised
|
||||
* with the default no-argument constructor and then configured via setters).
|
||||
*
|
||||
* @param observedGetter The method which can be used to obtain the value of this property from an instance of its owning type.
|
||||
* @param observedSetter The method which can be used to set the value of this property on an instance of its owning type.
|
||||
*/
|
||||
data class GetterSetterProperty(val observedGetter: Method, val observedSetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(false)
|
||||
|
||||
/**
|
||||
* A property for which we have only a getter method, which is annotated with [SerializableCalculatedProperty].
|
||||
*/
|
||||
data class CalculatedProperty(val observedGetter: Method, override val type: LocalTypeInformation, override val isMandatory: Boolean) : LocalPropertyInformation(true)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* References a slot in an array of constructor parameters.
|
||||
*/
|
||||
data class ConstructorSlot(val parameterIndex: Int, val constructorInformation: LocalConstructorInformation) {
|
||||
val parameterInformation get() = constructorInformation.parameters.getOrNull(parameterIndex) ?:
|
||||
throw IllegalStateException("Constructor slot refers to parameter #$parameterIndex " +
|
||||
"of constructor $constructorInformation, " +
|
||||
"but constructor has only ${constructorInformation.parameters.size} parameters")
|
||||
}
|
@ -0,0 +1,374 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import java.lang.reflect.*
|
||||
import kotlin.reflect.KFunction
|
||||
import java.util.*
|
||||
|
||||
typealias PropertyName = String
|
||||
|
||||
/**
|
||||
* The [LocalTypeInformation] captured for a [Type] gathers together everything that can be ascertained about the type
|
||||
* through runtime reflection, in the form of a directed acyclic graph (DAG) of types and relationships between types.
|
||||
*
|
||||
* Types can be related in the following ways:
|
||||
*
|
||||
* * Type A is the type of a _property_ of type B.
|
||||
* * Type A is the type of an _interface_ of type B.
|
||||
* * Type A is the type of the _superclass_ of type B.
|
||||
* * Type A is the type of a _type parameter_ of type B.
|
||||
* * Type A is an _array type_, of which type B is the _component type_.
|
||||
*
|
||||
* All of these relationships are represented by references and collections held by the objects representing the nodes
|
||||
* themselves.
|
||||
*
|
||||
* A type is [Composable] if it is isomorphic to a dictionary of its property values, i.e. if we can obtain an instance
|
||||
* of the type from a dictionary containing typed key/value pairs corresponding to its properties, and a dictionary from
|
||||
* an instance of the type, and can round-trip (in both directions) between these representations without losing
|
||||
* information. This is the basis for compositional serialization, i.e. building a serializer for a type out of the
|
||||
* serializers we have for its property types.
|
||||
*
|
||||
* A type is [Atomic] if it cannot be decomposed or recomposed in this fashion (usually because it is the type of a
|
||||
* scalar value of some sort, such as [Int]), and [Opaque] if we have chosen not to investigate its composability,
|
||||
* typically because it is handled by a custom serializer.
|
||||
*
|
||||
* Abstract types are represented by [AnInterface] and [Abstract], the difference between them being that an [Abstract]
|
||||
* type may have a superclass.
|
||||
*
|
||||
* If a concrete type does not have a unique deserialization constructor, it is represented by [NonComposable], meaning
|
||||
* that we know how to take it apart but do not know how to put it back together again.
|
||||
*
|
||||
* An array of any type is represented by [ArrayOf]. Enums are represented by [AnEnum].
|
||||
*
|
||||
* The type of [Any]/[java.lang.Object] is represented by [Top]. Unbounded wildcards, or wildcards whose upper bound is
|
||||
* [Top], are represented by [Unknown]. Bounded wildcards are always resolved to their upper bounds, e.g.
|
||||
* `List<? extends String>` becomes `List<String>`.
|
||||
*
|
||||
* If we encounter a cycle while traversing the DAG, the type on which traversal detected the cycle is represented by
|
||||
* [Cycle], and no further traversal is attempted from that type. Kotlin objects are represented by [Singleton].
|
||||
*/
|
||||
sealed class LocalTypeInformation {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Using the provided [LocalTypeLookup] to record and locate already-visited nodes, traverse the DAG of related
|
||||
* types beginning the with provided [Type] and construct a complete set of [LocalTypeInformation] for that type.
|
||||
*
|
||||
* @param type The [Type] to obtain [LocalTypeInformation] for.
|
||||
* @param lookup The [LocalTypeLookup] to use to find previously-constructed [LocalTypeInformation].
|
||||
*/
|
||||
fun forType(type: Type, lookup: LocalTypeLookup): LocalTypeInformation =
|
||||
LocalTypeInformationBuilder(lookup).build(type, TypeIdentifier.forGenericType(type))
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual type which was observed when constructing this type information.
|
||||
*/
|
||||
abstract val observedType: Type
|
||||
|
||||
/**
|
||||
* The [TypeIdentifier] for the type represented by this type information, used to cross-reference with
|
||||
* [RemoteTypeInformation].
|
||||
*/
|
||||
abstract val typeIdentifier: TypeIdentifier
|
||||
|
||||
/**
|
||||
* Obtain a multi-line, recursively-indented representation of this type information.
|
||||
*
|
||||
* @param simplifyClassNames By default, class names are printed as their "simple" class names, i.e. "String" instead
|
||||
* of "java.lang.String". If this is set to `false`, then the full class name will be printed instead.
|
||||
*/
|
||||
fun prettyPrint(simplifyClassNames: Boolean = true): String =
|
||||
LocalTypeInformationPrettyPrinter(simplifyClassNames).prettyPrint(this)
|
||||
|
||||
/**
|
||||
* The [LocalTypeInformation] corresponding to an unbounded wildcard ([TypeIdentifier.UnknownType])
|
||||
*/
|
||||
object Unknown : LocalTypeInformation() {
|
||||
override val observedType get() = TypeIdentifier.UnknownType.getLocalType()
|
||||
override val typeIdentifier get() = TypeIdentifier.UnknownType
|
||||
}
|
||||
|
||||
/**
|
||||
* The [LocalTypeInformation] corresponding to [java.lang.Object] / [Any] ([TypeIdentifier.TopType])
|
||||
*/
|
||||
object Top : LocalTypeInformation() {
|
||||
override val observedType get() = TypeIdentifier.TopType.getLocalType()
|
||||
override val typeIdentifier get() = TypeIdentifier.TopType
|
||||
}
|
||||
|
||||
/**
|
||||
* The [LocalTypeInformation] emitted if we hit a cycle while traversing the graph of related types.
|
||||
*/
|
||||
data class Cycle(
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
private val _follow: () -> LocalTypeInformation) : LocalTypeInformation() {
|
||||
val follow: LocalTypeInformation get() = _follow()
|
||||
|
||||
// Custom equals / hashcode because otherwise the "follow" lambda makes equality harder to reason about.
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is Cycle &&
|
||||
other.observedType == observedType &&
|
||||
other.typeIdentifier == typeIdentifier
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier)
|
||||
|
||||
override fun toString(): String = "Cycle($observedType, $typeIdentifier)"
|
||||
}
|
||||
|
||||
/**
|
||||
* May in fact be a more complex class, but is treated as if atomic, i.e. we don't further expand its properties.
|
||||
*/
|
||||
data class Opaque(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier,
|
||||
private val _expand: () -> LocalTypeInformation) : LocalTypeInformation() {
|
||||
val expand: LocalTypeInformation get() = _expand()
|
||||
|
||||
// Custom equals / hashcode because otherwise the "expand" lambda makes equality harder to reason about.
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is Cycle &&
|
||||
other.observedType == observedType &&
|
||||
other.typeIdentifier == typeIdentifier
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(observedType, typeIdentifier)
|
||||
|
||||
override fun toString(): String = "Opaque($observedType, $typeIdentifier)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a scalar type such as [Int].
|
||||
*/
|
||||
data class Atomic(override val observedType: Class<*>, override val typeIdentifier: TypeIdentifier) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents an array of some other type.
|
||||
*
|
||||
* @param componentType The [LocalTypeInformation] for the component type of the array (e.g. [Int], if the type is [IntArray])
|
||||
*/
|
||||
data class AnArray(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val componentType: LocalTypeInformation) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents an `enum`
|
||||
*
|
||||
* @param members The string names of the members of the enum.
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the type (as enums can inherit from other types).
|
||||
* @param interfaces [LocalTypeInformation] for each interface implemented by the type.
|
||||
*/
|
||||
data class AnEnum(
|
||||
override val observedType: Class<*>,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
val members: List<String>,
|
||||
val interfaces: List<LocalTypeInformation>): LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents a type whose underlying class is an interface.
|
||||
*
|
||||
* @param properties [LocalPropertyInformation] for the read-only properties of the interface, i.e. its "getter" methods.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||
*/
|
||||
data class AnInterface(
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents a type whose underlying class is abstract.
|
||||
*
|
||||
* @param properties [LocalPropertyInformation] for the read-only properties of the interface, i.e. its "getter" methods.
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||
*/
|
||||
data class Abstract(
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents a type which has only a single instantiation, e.g. a Kotlin `object`.
|
||||
*
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
*/
|
||||
data class Singleton(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val superclass: LocalTypeInformation, val interfaces: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents a type whose instances can be reversibly decomposed into dictionaries of typed values.
|
||||
*
|
||||
* @param constructor [LocalConstructorInformation] for the constructor used when building instances of this type
|
||||
* out of dictionaries of typed values.
|
||||
* @param properties [LocalPropertyInformation] for the properties of the interface.
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||
*/
|
||||
data class Composable(
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
val constructor: LocalConstructorInformation,
|
||||
val evolverConstructors: List<EvolverConstructorInformation>,
|
||||
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents a type whose instances may have observable properties (represented by "getter" methods), but for which
|
||||
* we do not possess a method (such as a unique "deserialization constructor" satisfied by these properties) for
|
||||
* creating a new instance from a dictionary of property values.
|
||||
*
|
||||
* @param constructor [LocalConstructorInformation] for the constructor of this type, if there is one.
|
||||
* @param properties [LocalPropertyInformation] for the properties of the interface.
|
||||
* @param superclass [LocalTypeInformation] for the superclass of the underlying class of this type.
|
||||
* @param interfaces [LocalTypeInformation] for the interfaces extended by this interface.
|
||||
* @param typeParameters [LocalTypeInformation] for the resolved type parameters of the type.
|
||||
*/
|
||||
data class NonComposable(
|
||||
override val observedType: Type,
|
||||
override val typeIdentifier: TypeIdentifier,
|
||||
val constructor: LocalConstructorInformation?,
|
||||
val properties: Map<PropertyName, LocalPropertyInformation>,
|
||||
val superclass: LocalTypeInformation,
|
||||
val interfaces: List<LocalTypeInformation>,
|
||||
val typeParameters: List<LocalTypeInformation>) : LocalTypeInformation()
|
||||
|
||||
/**
|
||||
* Represents a type whose underlying class is a collection class such as [List] with a single type parameter.
|
||||
*
|
||||
* @param elementType [LocalTypeInformation] for the resolved type parameter of the type, i.e. the type of its
|
||||
* elements. [Unknown] if the type is erased.
|
||||
*/
|
||||
data class ACollection(override val observedType: Type, override val typeIdentifier: TypeIdentifier, val elementType: LocalTypeInformation) : LocalTypeInformation() {
|
||||
val isErased: Boolean get() = typeIdentifier is TypeIdentifier.Erased
|
||||
|
||||
fun withElementType(parameter: LocalTypeInformation): ACollection = when(typeIdentifier) {
|
||||
is TypeIdentifier.Erased -> {
|
||||
val unerasedType = typeIdentifier.toParameterized(listOf(parameter.typeIdentifier))
|
||||
ACollection(
|
||||
unerasedType.getLocalType(this::class.java.classLoader),
|
||||
unerasedType,
|
||||
parameter)
|
||||
}
|
||||
is TypeIdentifier.Parameterised -> {
|
||||
val reparameterizedType = typeIdentifier.copy(parameters = listOf(parameter.typeIdentifier))
|
||||
ACollection(
|
||||
reparameterizedType.getLocalType(this::class.java.classLoader),
|
||||
reparameterizedType,
|
||||
parameter
|
||||
)
|
||||
}
|
||||
else -> throw IllegalStateException("Cannot parameterise $this")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a type whose underlying class is a map class such as [Map] with two type parameters.
|
||||
*
|
||||
* @param keyType [LocalTypeInformation] for the first resolved type parameter of the type, i.e. the type of its
|
||||
* keys. [Unknown] if the type is erased.
|
||||
* @param valueType [LocalTypeInformation] for the second resolved type parameter of the type, i.e. the type of its
|
||||
* values. [Unknown] if the type is erased.
|
||||
*/
|
||||
data class AMap(override val observedType: Type, override val typeIdentifier: TypeIdentifier,
|
||||
val keyType: LocalTypeInformation, val valueType: LocalTypeInformation) : LocalTypeInformation() {
|
||||
val isErased: Boolean get() = typeIdentifier is TypeIdentifier.Erased
|
||||
|
||||
fun withParameters(keyType: LocalTypeInformation, valueType: LocalTypeInformation): AMap = when(typeIdentifier) {
|
||||
is TypeIdentifier.Erased -> {
|
||||
val unerasedType = typeIdentifier.toParameterized(listOf(keyType.typeIdentifier, valueType.typeIdentifier))
|
||||
AMap(
|
||||
unerasedType.getLocalType(this::class.java.classLoader),
|
||||
unerasedType,
|
||||
keyType, valueType)
|
||||
}
|
||||
is TypeIdentifier.Parameterised -> {
|
||||
val reparameterizedType = typeIdentifier.copy(parameters = listOf(keyType.typeIdentifier, valueType.typeIdentifier))
|
||||
AMap(
|
||||
reparameterizedType.getLocalType(this::class.java.classLoader),
|
||||
reparameterizedType,
|
||||
keyType, valueType
|
||||
)
|
||||
}
|
||||
else -> throw IllegalStateException("Cannot parameterise $this")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents information about a constructor.
|
||||
*/
|
||||
data class LocalConstructorInformation(
|
||||
val observedMethod: KFunction<Any>,
|
||||
val parameters: List<LocalConstructorParameterInformation>) {
|
||||
val hasParameters: Boolean get() = parameters.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents information about a constructor that is specifically to be used for evolution, and is potentially matched
|
||||
* with a different set of properties to the regular constructor.
|
||||
*/
|
||||
data class EvolverConstructorInformation(
|
||||
val constructor: LocalConstructorInformation,
|
||||
val properties: Map<String, LocalPropertyInformation>)
|
||||
|
||||
/**
|
||||
* Represents information about a constructor parameter
|
||||
*/
|
||||
data class LocalConstructorParameterInformation(
|
||||
val name: String,
|
||||
val type: LocalTypeInformation,
|
||||
val isMandatory: Boolean)
|
||||
|
||||
private data class LocalTypeInformationPrettyPrinter(private val simplifyClassNames: Boolean, private val indent: Int = 0) {
|
||||
|
||||
fun prettyPrint(typeInformation: LocalTypeInformation): String =
|
||||
with(typeInformation) {
|
||||
when (this) {
|
||||
is LocalTypeInformation.Abstract ->
|
||||
typeIdentifier.prettyPrint() +
|
||||
printInheritsFrom(interfaces, superclass) +
|
||||
indentAnd { printProperties(properties) }
|
||||
is LocalTypeInformation.AnInterface ->
|
||||
typeIdentifier.prettyPrint() + printInheritsFrom(interfaces)
|
||||
is LocalTypeInformation.Composable -> typeIdentifier.prettyPrint() +
|
||||
printConstructor(constructor) +
|
||||
printInheritsFrom(interfaces, superclass) +
|
||||
indentAnd { printProperties(properties) }
|
||||
else -> typeIdentifier.prettyPrint()
|
||||
}
|
||||
}
|
||||
|
||||
private fun printConstructor(constructor: LocalConstructorInformation) =
|
||||
constructor.parameters.joinToString(", ", "(", ")") {
|
||||
it.name +
|
||||
": " + it.type.typeIdentifier.prettyPrint(simplifyClassNames) +
|
||||
(if (!it.isMandatory) "?" else "")
|
||||
}
|
||||
|
||||
private fun printInheritsFrom(interfaces: List<LocalTypeInformation>, superclass: LocalTypeInformation? = null): String {
|
||||
val parents = if (superclass == null || superclass == LocalTypeInformation.Top) interfaces.asSequence()
|
||||
else sequenceOf(superclass) + interfaces.asSequence()
|
||||
return if (!parents.iterator().hasNext()) ""
|
||||
else parents.joinToString(", ", ": ", "") { it.typeIdentifier.prettyPrint(simplifyClassNames) }
|
||||
}
|
||||
|
||||
private fun printProperties(properties: Map<String, LocalPropertyInformation>) =
|
||||
properties.entries.asSequence().sortedBy { it.key }.joinToString("\n", "\n", "") {
|
||||
it.prettyPrint()
|
||||
}
|
||||
|
||||
private fun Map.Entry<String, LocalPropertyInformation>.prettyPrint(): String =
|
||||
" ".repeat(indent) + key +
|
||||
(if(!value.isMandatory) " (optional)" else "") +
|
||||
(if (value.isCalculated) " (calculated)" else "") +
|
||||
": " + value.type.prettyPrint(simplifyClassNames)
|
||||
|
||||
private inline fun indentAnd(block: LocalTypeInformationPrettyPrinter.() -> String) =
|
||||
copy(indent = indent + 1).block()
|
||||
}
|
||||
|
@ -0,0 +1,410 @@
|
||||
package net.corda.serialization.internal.model
|
||||
|
||||
import net.corda.core.internal.isAbstractClass
|
||||
import net.corda.core.internal.isConcreteClass
|
||||
import net.corda.core.internal.kotlinObjectInstance
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.serialization.internal.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashMap
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
/**
|
||||
* Provides the logic for building instances of [LocalTypeInformation] by reflecting over local [Type]s.
|
||||
*
|
||||
* @param lookup The [LocalTypeLookup] to use to locate and register constructed [LocalTypeInformation].
|
||||
* @param resolutionContext The [Type] to use when attempting to resolve type variables.
|
||||
* @param visited The [Set] of [TypeIdentifier]s already visited while building information for a given [Type]. Note that
|
||||
* this is not a [MutableSet], as we want to be able to backtrack while traversing through the graph of related types, and
|
||||
* will find it useful to revert to earlier states of knowledge about which types have been visited on a given branch.
|
||||
*/
|
||||
internal data class LocalTypeInformationBuilder(val lookup: LocalTypeLookup, val resolutionContext: Type? = null, val visited: Set<TypeIdentifier> = emptySet()) {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively build [LocalTypeInformation] for the given [Type] and [TypeIdentifier]
|
||||
*/
|
||||
fun build(type: Type, typeIdentifier: TypeIdentifier): LocalTypeInformation =
|
||||
if (typeIdentifier in visited) LocalTypeInformation.Cycle(type, typeIdentifier) {
|
||||
LocalTypeInformationBuilder(lookup, resolutionContext).build(type, typeIdentifier)
|
||||
}
|
||||
else lookup.findOrBuild(type, typeIdentifier) { isOpaque ->
|
||||
copy(visited = visited + typeIdentifier).buildIfNotFound(type, typeIdentifier, isOpaque)
|
||||
}
|
||||
|
||||
private fun resolveAndBuild(type: Type): LocalTypeInformation {
|
||||
val resolved = type.resolveAgainstContext()
|
||||
return build(resolved, TypeIdentifier.forGenericType(resolved, resolutionContext
|
||||
?: type))
|
||||
}
|
||||
|
||||
private fun Type.resolveAgainstContext(): Type =
|
||||
if (resolutionContext == null) this else resolveAgainst(resolutionContext)
|
||||
|
||||
private fun buildIfNotFound(type: Type, typeIdentifier: TypeIdentifier, isOpaque: Boolean): LocalTypeInformation {
|
||||
val rawType = type.asClass()
|
||||
return when (typeIdentifier) {
|
||||
is TypeIdentifier.TopType -> LocalTypeInformation.Top
|
||||
is TypeIdentifier.UnknownType -> LocalTypeInformation.Unknown
|
||||
is TypeIdentifier.Unparameterised,
|
||||
is TypeIdentifier.Erased -> buildForClass(rawType, typeIdentifier, isOpaque)
|
||||
is TypeIdentifier.ArrayOf -> {
|
||||
LocalTypeInformation.AnArray(
|
||||
type,
|
||||
typeIdentifier,
|
||||
resolveAndBuild(type.componentType()))
|
||||
}
|
||||
is TypeIdentifier.Parameterised -> buildForParameterised(rawType, type as ParameterizedType, typeIdentifier, isOpaque)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildForClass(type: Class<*>, typeIdentifier: TypeIdentifier, isOpaque: Boolean): LocalTypeInformation = withContext(type) {
|
||||
when {
|
||||
Collection::class.java.isAssignableFrom(type) &&
|
||||
!EnumSet::class.java.isAssignableFrom(type) -> LocalTypeInformation.ACollection(type, typeIdentifier, LocalTypeInformation.Unknown)
|
||||
Map::class.java.isAssignableFrom(type) -> LocalTypeInformation.AMap(type, typeIdentifier, LocalTypeInformation.Unknown, LocalTypeInformation.Unknown)
|
||||
type.kotlin.javaPrimitiveType != null -> LocalTypeInformation.Atomic(type.kotlin.javaPrimitiveType!!, typeIdentifier)
|
||||
type.isEnum -> LocalTypeInformation.AnEnum(
|
||||
type,
|
||||
typeIdentifier,
|
||||
type.enumConstants.map { it.toString() },
|
||||
buildInterfaceInformation(type))
|
||||
type.kotlinObjectInstance != null -> LocalTypeInformation.Singleton(
|
||||
type,
|
||||
typeIdentifier,
|
||||
buildSuperclassInformation(type),
|
||||
buildInterfaceInformation(type))
|
||||
type.isInterface -> buildInterface(type, typeIdentifier, emptyList())
|
||||
type.isAbstractClass -> buildAbstract(type, typeIdentifier, emptyList())
|
||||
else -> when {
|
||||
isOpaque -> LocalTypeInformation.Opaque(type, typeIdentifier) {
|
||||
buildNonAtomic(type, type, typeIdentifier, emptyList())
|
||||
}
|
||||
else -> buildNonAtomic(type, type, typeIdentifier, emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildForParameterised(
|
||||
rawType: Class<*>,
|
||||
type: ParameterizedType,
|
||||
typeIdentifier: TypeIdentifier.Parameterised,
|
||||
isOpaque: Boolean): LocalTypeInformation = withContext(type) {
|
||||
when {
|
||||
Collection::class.java.isAssignableFrom(rawType) &&
|
||||
!EnumSet::class.java.isAssignableFrom(rawType) ->
|
||||
LocalTypeInformation.ACollection(type, typeIdentifier, buildTypeParameterInformation(type)[0])
|
||||
Map::class.java.isAssignableFrom(rawType) -> {
|
||||
val (keyType, valueType) = buildTypeParameterInformation(type)
|
||||
LocalTypeInformation.AMap(type, typeIdentifier, keyType, valueType)
|
||||
}
|
||||
rawType.isInterface -> buildInterface(type, typeIdentifier, buildTypeParameterInformation(type))
|
||||
rawType.isAbstractClass -> buildAbstract(type, typeIdentifier, buildTypeParameterInformation(type))
|
||||
else -> when {
|
||||
isOpaque -> LocalTypeInformation.Opaque(rawType, typeIdentifier) {
|
||||
buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type))
|
||||
}
|
||||
else -> buildNonAtomic(rawType, type, typeIdentifier, buildTypeParameterInformation(type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAbstract(type: Type, typeIdentifier: TypeIdentifier,
|
||||
typeParameters: List<LocalTypeInformation>): LocalTypeInformation.Abstract =
|
||||
LocalTypeInformation.Abstract(
|
||||
type,
|
||||
typeIdentifier,
|
||||
buildReadOnlyProperties(type.asClass()),
|
||||
buildSuperclassInformation(type),
|
||||
buildInterfaceInformation(type),
|
||||
typeParameters)
|
||||
|
||||
private fun buildInterface(type: Type, typeIdentifier: TypeIdentifier,
|
||||
typeParameters: List<LocalTypeInformation>): LocalTypeInformation.AnInterface =
|
||||
LocalTypeInformation.AnInterface(
|
||||
type,
|
||||
typeIdentifier,
|
||||
buildReadOnlyProperties(type.asClass()),
|
||||
buildInterfaceInformation(type),
|
||||
typeParameters)
|
||||
|
||||
private inline fun <T> withContext(newContext: Type, block: LocalTypeInformationBuilder.() -> T): T =
|
||||
copy(resolutionContext = newContext).run(block)
|
||||
|
||||
/**
|
||||
* Build a non-atomic type, which is either [Composable] or [NonComposable].
|
||||
*
|
||||
* Composability is a transitive property: a type is [Composable] iff it has a unique deserialization constructor _and_
|
||||
* all of its property types are also [Composable]. If not, the type is [NonComposable], meaning we cannot deserialize
|
||||
* it without a custom serializer (in which case it should normally have been flagged as [Opaque]).
|
||||
*
|
||||
* Rather than throwing an exception if a type is [NonComposable], we capture its type information so that it can
|
||||
* still be used to _serialize_ values, or as the basis for deciding on an evolution strategy.
|
||||
*/
|
||||
private fun buildNonAtomic(rawType: Class<*>, type: Type, typeIdentifier: TypeIdentifier, typeParameterInformation: List<LocalTypeInformation>): LocalTypeInformation {
|
||||
val superclassInformation = buildSuperclassInformation(type)
|
||||
val interfaceInformation = buildInterfaceInformation(type)
|
||||
val observedConstructor = constructorForDeserialization(type)
|
||||
|
||||
if (observedConstructor == null) {
|
||||
logger.warn("No unique deserialisation constructor found for class $rawType, type is marked as non-composable")
|
||||
return LocalTypeInformation.NonComposable(type, typeIdentifier, null, buildReadOnlyProperties(rawType),
|
||||
superclassInformation, interfaceInformation, typeParameterInformation)
|
||||
}
|
||||
|
||||
val constructorInformation = buildConstructorInformation(type, observedConstructor)
|
||||
val properties = buildObjectProperties(rawType, constructorInformation)
|
||||
|
||||
val hasNonComposableProperties = properties.values.any { it.type is LocalTypeInformation.NonComposable }
|
||||
|
||||
if (!propertiesSatisfyConstructor(constructorInformation, properties) || hasNonComposableProperties) {
|
||||
if (hasNonComposableProperties) {
|
||||
logger.warn("Type ${type.typeName} has non-composable properties and has been marked as non-composable")
|
||||
} else {
|
||||
logger.warn("Properties of type ${type.typeName} do not satisfy its constructor, type has been marked as non-composable")
|
||||
}
|
||||
return LocalTypeInformation.NonComposable(type, typeIdentifier, constructorInformation, properties, superclassInformation,
|
||||
interfaceInformation, typeParameterInformation)
|
||||
}
|
||||
|
||||
val evolverConstructors = evolverConstructors(type).map { ctor ->
|
||||
val constructorInformation = buildConstructorInformation(type, ctor)
|
||||
val evolverProperties = buildObjectProperties(rawType, constructorInformation)
|
||||
EvolverConstructorInformation(constructorInformation, evolverProperties)
|
||||
}
|
||||
|
||||
return LocalTypeInformation.Composable(type, typeIdentifier, constructorInformation, evolverConstructors, properties,
|
||||
superclassInformation, interfaceInformation, typeParameterInformation)
|
||||
}
|
||||
|
||||
// Can we supply all of the mandatory constructor parameters using values addressed by readable properties?
|
||||
private fun propertiesSatisfyConstructor(constructorInformation: LocalConstructorInformation, properties: Map<PropertyName, LocalPropertyInformation>): Boolean {
|
||||
if (!constructorInformation.hasParameters) return true
|
||||
|
||||
val indicesAddressedByProperties = properties.values.asSequence().mapNotNull {
|
||||
when (it) {
|
||||
is LocalPropertyInformation.ConstructorPairedProperty -> it.constructorSlot.parameterIndex
|
||||
is LocalPropertyInformation.PrivateConstructorPairedProperty -> it.constructorSlot.parameterIndex
|
||||
else -> null
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
return (0 until constructorInformation.parameters.size).none { index ->
|
||||
constructorInformation.parameters[index].isMandatory && index !in indicesAddressedByProperties
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSuperclassInformation(type: Type): LocalTypeInformation =
|
||||
resolveAndBuild(type.asClass().genericSuperclass)
|
||||
|
||||
private fun buildInterfaceInformation(type: Type) =
|
||||
type.allInterfaces.asSequence().mapNotNull {
|
||||
if (it == type) return@mapNotNull null
|
||||
resolveAndBuild(it)
|
||||
}.toList()
|
||||
|
||||
private val Type.allInterfaces: Set<Type> get() = exploreType(this)
|
||||
|
||||
private fun exploreType(type: Type, interfaces: MutableSet<Type> = LinkedHashSet()): MutableSet<Type> {
|
||||
val clazz = type.asClass()
|
||||
|
||||
if (clazz.isInterface) {
|
||||
// Ignore classes we've already seen, and stop exploring once we reach an excluded type.
|
||||
if (clazz in interfaces || lookup.isExcluded(clazz)) return interfaces
|
||||
else interfaces += type
|
||||
}
|
||||
|
||||
clazz.genericInterfaces.forEach { exploreType(it.resolveAgainstContext(), interfaces) }
|
||||
if (clazz.genericSuperclass != null) exploreType(clazz.genericSuperclass.resolveAgainstContext(), interfaces)
|
||||
|
||||
return interfaces
|
||||
}
|
||||
|
||||
private fun buildReadOnlyProperties(rawType: Class<*>): Map<PropertyName, LocalPropertyInformation> =
|
||||
rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) ->
|
||||
if (descriptor.field == null || descriptor.getter == null) null
|
||||
else {
|
||||
val paramType = (descriptor.getter.genericReturnType).resolveAgainstContext()
|
||||
val paramTypeInformation = build(paramType, TypeIdentifier.forGenericType(paramType, resolutionContext
|
||||
?: rawType))
|
||||
val isMandatory = paramType.asClass().isPrimitive || !descriptor.getter.returnsNullable()
|
||||
name to LocalPropertyInformation.ReadOnlyProperty(descriptor.getter, paramTypeInformation, isMandatory)
|
||||
}
|
||||
}.sortedBy { (name, _) -> name }.toMap(LinkedHashMap())
|
||||
|
||||
private fun buildObjectProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Map<PropertyName, LocalPropertyInformation> =
|
||||
(calculatedProperties(rawType) + nonCalculatedProperties(rawType, constructorInformation))
|
||||
.sortedBy { (name, _) -> name }
|
||||
.toMap(LinkedHashMap())
|
||||
|
||||
private fun nonCalculatedProperties(rawType: Class<*>, constructorInformation: LocalConstructorInformation): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||
if (constructorInformation.hasParameters) getConstructorPairedProperties(constructorInformation, rawType)
|
||||
else getterSetterProperties(rawType)
|
||||
|
||||
private fun getConstructorPairedProperties(constructorInformation: LocalConstructorInformation, rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> {
|
||||
val constructorParameterIndices = constructorInformation.parameters.asSequence().mapIndexed { index, parameter ->
|
||||
parameter.name to index
|
||||
}.toMap()
|
||||
|
||||
return rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) ->
|
||||
val property = makeConstructorPairedProperty(constructorParameterIndices, name, descriptor, constructorInformation)
|
||||
if (property == null) null else name to property
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeConstructorPairedProperty(constructorParameterIndices: Map<String, Int>,
|
||||
name: String,
|
||||
descriptor: PropertyDescriptor,
|
||||
constructorInformation: LocalConstructorInformation): LocalPropertyInformation? {
|
||||
val constructorIndex = constructorParameterIndices[name] ?:
|
||||
// In some very rare cases we have a constructor parameter matched by a getter with no backing field,
|
||||
// and cannot infer whether the property name should be capitalised or not.
|
||||
constructorParameterIndices[name.decapitalize()] ?: return null
|
||||
|
||||
if (descriptor.getter == null) {
|
||||
if (descriptor.field == null) return null
|
||||
val paramType = descriptor.field.genericType
|
||||
val paramTypeInformation = resolveAndBuild(paramType)
|
||||
|
||||
return LocalPropertyInformation.PrivateConstructorPairedProperty(
|
||||
descriptor.field,
|
||||
ConstructorSlot(constructorIndex, constructorInformation),
|
||||
paramTypeInformation,
|
||||
constructorInformation.parameters[constructorIndex].isMandatory)
|
||||
}
|
||||
|
||||
val paramType = descriptor.getter.genericReturnType
|
||||
val paramTypeInformation = resolveAndBuild(paramType)
|
||||
|
||||
return LocalPropertyInformation.ConstructorPairedProperty(
|
||||
descriptor.getter,
|
||||
ConstructorSlot(constructorIndex, constructorInformation),
|
||||
paramTypeInformation,
|
||||
descriptor.getter.returnType.isPrimitive ||
|
||||
!descriptor.getter.returnsNullable())
|
||||
}
|
||||
|
||||
private fun getterSetterProperties(rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||
rawType.propertyDescriptors().asSequence().mapNotNull { (name, descriptor) ->
|
||||
if (descriptor.getter == null || descriptor.setter == null || descriptor.field == null) null
|
||||
else {
|
||||
val paramType = descriptor.getter.genericReturnType
|
||||
val paramTypeInformation = resolveAndBuild(paramType)
|
||||
val isMandatory = paramType.asClass().isPrimitive || !descriptor.getter.returnsNullable()
|
||||
|
||||
name to LocalPropertyInformation.GetterSetterProperty(
|
||||
descriptor.getter,
|
||||
descriptor.setter,
|
||||
paramTypeInformation,
|
||||
isMandatory)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatedProperties(rawType: Class<*>): Sequence<Pair<String, LocalPropertyInformation>> =
|
||||
rawType.calculatedPropertyDescriptors().asSequence().map { (name, v) ->
|
||||
val paramType = v.getter!!.genericReturnType
|
||||
val paramTypeInformation = resolveAndBuild(paramType)
|
||||
val isMandatory = paramType.asClass().isPrimitive || !v.getter.returnsNullable()
|
||||
|
||||
name to LocalPropertyInformation.CalculatedProperty(v.getter, paramTypeInformation, isMandatory)
|
||||
}
|
||||
|
||||
private fun buildTypeParameterInformation(type: ParameterizedType): List<LocalTypeInformation> =
|
||||
type.actualTypeArguments.map {
|
||||
resolveAndBuild(it)
|
||||
}
|
||||
|
||||
private fun buildConstructorInformation(type: Type, observedConstructor: KFunction<Any>): LocalConstructorInformation {
|
||||
if (observedConstructor.javaConstructor?.parameters?.getOrNull(0)?.name == "this$0")
|
||||
throw NotSerializableException("Type '${type.typeName} has synthetic fields and is likely a nested inner class.")
|
||||
|
||||
return LocalConstructorInformation(observedConstructor, observedConstructor.parameters.map {
|
||||
val parameterType = it.type.javaType
|
||||
LocalConstructorParameterInformation(
|
||||
it.name ?: throw IllegalStateException("Unnamed parameter in constructor $observedConstructor"),
|
||||
resolveAndBuild(parameterType),
|
||||
parameterType.asClass().isPrimitive || !it.type.isMarkedNullable)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean = try {
|
||||
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull {
|
||||
it.javaGetter == this
|
||||
}?.returnType?.toString() ?: "?"
|
||||
|
||||
returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||
} catch (e: KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Code for finding the unique constructor we will use for deserialization.
|
||||
*
|
||||
* If any constructor is uniquely annotated with [@ConstructorForDeserialization], then that constructor is chosen.
|
||||
* An error is reported if more than one constructor is annotated.
|
||||
*
|
||||
* Otherwise, if there is a Kotlin primary constructor, it selects that, and if not it selects either the unique
|
||||
* constructor or, if there are two and one is the default no-argument constructor, the non-default constructor.
|
||||
*/
|
||||
private fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||
val clazz = type.asClass()
|
||||
if (!clazz.isConcreteClass || clazz.isSynthetic) return null
|
||||
|
||||
val kotlinCtors = clazz.kotlin.constructors
|
||||
|
||||
val annotatedCtors = kotlinCtors.filter { it.findAnnotation<ConstructorForDeserialization>() != null }
|
||||
if (annotatedCtors.size > 1) return null
|
||||
if (annotatedCtors.size == 1) return annotatedCtors.first().apply { isAccessible = true }
|
||||
|
||||
val defaultCtor = kotlinCtors.firstOrNull { it.parameters.isEmpty() }
|
||||
val nonDefaultCtors = kotlinCtors.filter { it != defaultCtor }
|
||||
|
||||
val preferredCandidate = clazz.kotlin.primaryConstructor ?:
|
||||
when(nonDefaultCtors.size) {
|
||||
1 -> nonDefaultCtors.first()
|
||||
0 -> defaultCtor
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
||||
return try {
|
||||
preferredCandidate.apply { isAccessible = true }
|
||||
} catch (e: SecurityException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun evolverConstructors(type: Type): List<KFunction<Any>> {
|
||||
val clazz = type.asClass()
|
||||
if (!clazz.isConcreteClass || clazz.isSynthetic) return emptyList()
|
||||
|
||||
return clazz.kotlin.constructors.asSequence()
|
||||
.mapNotNull {
|
||||
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version
|
||||
if (version == null) null else version to it
|
||||
}
|
||||
.sortedBy { (version, ctor) -> version }
|
||||
.map { (version, ctor) -> ctor.apply { isAccessible = true} }
|
||||
.toList()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user