mirror of
https://github.com/corda/corda.git
synced 2025-06-20 16:10:26 +00:00
Merge commit '86fb1ed852c69121f989c9eeea92cfb4c27f9d13' into aslemmer-merge-19-Feb
This commit is contained in:
@ -7,6 +7,8 @@ import net.corda.core.node.NodeInfo
|
||||
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 java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
@ -44,3 +46,11 @@ class SignedNodeInfo(val raw: SerializedBytes<NodeInfo>, val signatures: List<Di
|
||||
return nodeInfo
|
||||
}
|
||||
}
|
||||
|
||||
inline fun NodeInfo.sign(signer: (PublicKey, SerializedBytes<NodeInfo>) -> DigitalSignature): SignedNodeInfo {
|
||||
// For now we exclude any composite identities, see [SignedNodeInfo]
|
||||
val owningKeys = legalIdentities.map { it.owningKey }.filter { it !is CompositeKey }
|
||||
val serialised = serialize()
|
||||
val signatures = owningKeys.map { signer(it, serialised) }
|
||||
return SignedNodeInfo(serialised, signatures)
|
||||
}
|
||||
|
@ -4,10 +4,7 @@ import net.corda.core.CordaOID
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.reader
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.internal.writer
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
import org.bouncycastle.asn1.*
|
||||
|
@ -5,12 +5,14 @@ import net.corda.cordform.CordformNode
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
@ -18,7 +20,7 @@ import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -167,7 +169,7 @@ class NetworkBootstrapper {
|
||||
epoch = 1
|
||||
), overwriteFile = true)
|
||||
|
||||
nodeDirs.forEach(copier::install)
|
||||
nodeDirs.forEach { copier.install(it) }
|
||||
}
|
||||
|
||||
private fun NotaryInfo.prettyPrint(): String = "${identity.name} (${if (validating) "" else "non-"}validating)"
|
||||
@ -196,8 +198,8 @@ class NetworkBootstrapper {
|
||||
}
|
||||
|
||||
private object KryoParametersSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
|
||||
return magic == kryoMagic && target == SerializationContext.UseCase.P2P
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException()
|
||||
|
@ -1,58 +1,48 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.CertRole
|
||||
import net.corda.core.internal.SignedDataWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
const val NETWORK_PARAMS_FILE_NAME = "network-parameters"
|
||||
const val NETWORK_PARAMS_UPDATE_FILE_NAME = "network-parameters-update"
|
||||
|
||||
/**
|
||||
* Data class containing hash of [NetworkParameters] and network participant's [NodeInfo] hashes.
|
||||
* Data structure representing the network map available from the HTTP network map service as a serialised blob.
|
||||
* @property nodeInfoHashes list of network participant's [NodeInfo] hashes
|
||||
* @property networkParameterHash hash of the current active [NetworkParameters]
|
||||
* @property parametersUpdate if present means that network operator has scheduled an update of the network parameters
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NetworkMap(val nodeInfoHashes: List<SecureHash>, val networkParameterHash: SecureHash)
|
||||
data class NetworkMap(
|
||||
val nodeInfoHashes: List<SecureHash>,
|
||||
val networkParameterHash: SecureHash,
|
||||
val parametersUpdate: ParametersUpdate?
|
||||
)
|
||||
|
||||
/**
|
||||
* @property minimumPlatformVersion Minimum version of Corda platform that is required for nodes in the network.
|
||||
* @property notaries List of well known and trusted notary identities with information on validation type.
|
||||
* @property maxMessageSize Maximum P2P message sent over the wire in bytes.
|
||||
* @property maxTransactionSize Maximum permitted transaction size in bytes.
|
||||
* @property modifiedTime
|
||||
* @property epoch Version number of the network parameters. Starting from 1, this will always increment on each new set
|
||||
* of parameters.
|
||||
* Data class representing scheduled network parameters update.
|
||||
* @property newParametersHash Hash of the new [NetworkParameters] which can be requested from the network map
|
||||
* @property description Short description of the update
|
||||
* @property updateDeadline deadline by which new network parameters need to be accepted, after this date network operator
|
||||
* can switch to new parameters which will result in getting nodes with old parameters out of the network
|
||||
*/
|
||||
// TODO Add eventHorizon - how many days a node can be offline before being automatically ejected from the network.
|
||||
// It needs separate design.
|
||||
// TODO Currently maxTransactionSize is not wired.
|
||||
@CordaSerializable
|
||||
data class NetworkParameters(
|
||||
val minimumPlatformVersion: Int,
|
||||
val notaries: List<NotaryInfo>,
|
||||
val maxMessageSize: Int,
|
||||
val maxTransactionSize: Int,
|
||||
val modifiedTime: Instant,
|
||||
val epoch: Int
|
||||
) {
|
||||
init {
|
||||
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
|
||||
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
|
||||
require(epoch > 0) { "epoch must be at least 1" }
|
||||
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
|
||||
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||
data class ParametersUpdate(
|
||||
val newParametersHash: SecureHash,
|
||||
val description: String,
|
||||
val updateDeadline: Instant
|
||||
)
|
||||
|
||||
fun <T : Any> SignedDataWithCert<T>.verifiedNetworkMapCert(rootCert: X509Certificate): T {
|
||||
require(CertRole.extract(sig.by) == CertRole.NETWORK_MAP) { "Incorrect cert role: ${CertRole.extract(sig.by)}" }
|
||||
X509Utilities.validateCertificateChain(rootCert, sig.by, rootCert)
|
||||
return verified()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package net.corda.nodeapi.internal.network
|
||||
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||
@ -13,7 +12,9 @@ import java.nio.file.StandardCopyOption
|
||||
class NetworkParametersCopier(
|
||||
networkParameters: NetworkParameters,
|
||||
networkMapCa: CertificateAndKeyPair = createDevNetworkMapCa(),
|
||||
overwriteFile: Boolean = false
|
||||
overwriteFile: Boolean = false,
|
||||
@VisibleForTesting
|
||||
val update: Boolean = false
|
||||
) {
|
||||
private val copyOptions = if (overwriteFile) arrayOf(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
|
||||
private val serialisedSignedNetParams = networkParameters.signWithCert(
|
||||
@ -22,8 +23,9 @@ class NetworkParametersCopier(
|
||||
).serialize()
|
||||
|
||||
fun install(nodeDir: Path) {
|
||||
val fileName = if (update) NETWORK_PARAMS_UPDATE_FILE_NAME else NETWORK_PARAMS_FILE_NAME
|
||||
try {
|
||||
serialisedSignedNetParams.open().copyTo(nodeDir / NETWORK_PARAMS_FILE_NAME, *copyOptions)
|
||||
serialisedSignedNetParams.open().copyTo(nodeDir / fileName, *copyOptions)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
// This is only thrown if the file already exists and we didn't specify to overwrite it. In that case we
|
||||
// ignore this exception as we're happy with the existing file.
|
||||
|
@ -4,8 +4,8 @@ package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
/*
|
||||
* Serialisation contexts for the client.
|
||||
@ -13,14 +13,13 @@ import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
* servers from trying to instantiate any of them.
|
||||
*/
|
||||
|
||||
val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.RPCClient)
|
||||
|
||||
val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
|
@ -6,14 +6,15 @@ import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import com.esotericsoftware.kryo.util.Util
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.nodeapi.internal.serialization.amqp.hasAnnotationInHierarchy
|
||||
import net.corda.nodeapi.internal.serialization.kryo.ThrowableSerializer
|
||||
import java.io.PrintWriter
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Modifier.isAbstract
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
@ -74,10 +75,21 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
|
||||
|
||||
override fun registerImplicit(type: Class<*>): Registration {
|
||||
val targetType = typeForSerializationOf(type)
|
||||
// Is this a Kotlin object? We use our own reflection here rather than .kotlin.objectInstance because Kotlin
|
||||
// reflection won't work for private objects, and can throw exceptions in other circumstances as well.
|
||||
val objectInstance = try {
|
||||
targetType.kotlin.objectInstance
|
||||
targetType.declaredFields.singleOrNull {
|
||||
it.name == "INSTANCE" &&
|
||||
it.type == type &&
|
||||
Modifier.isStatic(it.modifiers) &&
|
||||
Modifier.isFinal(it.modifiers) &&
|
||||
Modifier.isPublic(it.modifiers)
|
||||
}?.let {
|
||||
it.isAccessible = true
|
||||
type.cast(it.get(null)!!)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
null // objectInstance will throw if the type is something like a lambda
|
||||
null
|
||||
}
|
||||
|
||||
// We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent.
|
||||
|
@ -0,0 +1,12 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
private val bufferView = slice()
|
||||
fun consume(data: ByteSequence): ByteBuffer? {
|
||||
return if (data.slice(end = size) == bufferView) data.slice(size) else null
|
||||
}
|
||||
}
|
@ -4,9 +4,12 @@ import com.google.common.cache.Cache
|
||||
import com.google.common.cache.CacheBuilder
|
||||
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.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.NotSerializableException
|
||||
import java.util.*
|
||||
@ -15,17 +18,7 @@ import java.util.concurrent.ExecutionException
|
||||
|
||||
val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled"
|
||||
|
||||
object NotSupportedSerializationScheme : SerializationScheme {
|
||||
private fun doThrow(): Nothing = throw UnsupportedOperationException("Serialization scheme not supported.")
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean = doThrow()
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T = doThrow()
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> = doThrow()
|
||||
}
|
||||
|
||||
data class SerializationContextImpl(override val preferredSerializationVersion: VersionHeader,
|
||||
data class SerializationContextImpl(override val preferredSerializationVersion: SerializationMagic,
|
||||
override val deserializationClassLoader: ClassLoader,
|
||||
override val whitelist: ClassWhitelist,
|
||||
override val properties: Map<Any, Any>,
|
||||
@ -76,14 +69,14 @@ data class SerializationContextImpl(override val preferredSerializationVersion:
|
||||
})
|
||||
}
|
||||
|
||||
override fun withPreferredSerializationVersion(versionHeader: VersionHeader) = copy(preferredSerializationVersion = versionHeader)
|
||||
override fun withPreferredSerializationVersion(magic: SerializationMagic) = copy(preferredSerializationVersion = magic)
|
||||
}
|
||||
|
||||
private const val HEADER_SIZE: Int = 8
|
||||
|
||||
fun ByteSequence.obtainHeaderSignature(): VersionHeader = take(HEADER_SIZE).copy()
|
||||
|
||||
open class SerializationFactoryImpl : SerializationFactory() {
|
||||
companion object {
|
||||
private val magicSize = sequenceOf(kryoMagic, amqpMagic).map { it.size }.distinct().single()
|
||||
}
|
||||
|
||||
private val creator: List<StackTraceElement> = Exception().stackTrace.asList()
|
||||
|
||||
private val registeredSchemes: MutableCollection<SerializationScheme> = Collections.synchronizedCollection(mutableListOf())
|
||||
@ -91,19 +84,17 @@ open class SerializationFactoryImpl : SerializationFactory() {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
// TODO: This is read-mostly. Probably a faster implementation to be found.
|
||||
private val schemes: ConcurrentHashMap<Pair<ByteSequence, SerializationContext.UseCase>, SerializationScheme> = ConcurrentHashMap()
|
||||
private val schemes: ConcurrentHashMap<Pair<CordaSerializationMagic, SerializationContext.UseCase>, SerializationScheme> = ConcurrentHashMap()
|
||||
|
||||
private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): Pair<SerializationScheme, VersionHeader> {
|
||||
// truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays
|
||||
val lookupKey = byteSequence.obtainHeaderSignature() to target
|
||||
val scheme = schemes.computeIfAbsent(lookupKey) {
|
||||
registeredSchemes
|
||||
.filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) }
|
||||
.forEach { return@computeIfAbsent it }
|
||||
private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): Pair<SerializationScheme, CordaSerializationMagic> {
|
||||
// truncate sequence to at most magicSize, and make sure it's a copy to avoid holding onto large ByteArrays
|
||||
val magic = CordaSerializationMagic(byteSequence.slice(end = magicSize).copyBytes())
|
||||
val lookupKey = magic to target
|
||||
return schemes.computeIfAbsent(lookupKey) {
|
||||
registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single?
|
||||
logger.warn("Cannot find serialization scheme for: $lookupKey, registeredSchemes are: $registeredSchemes")
|
||||
NotSupportedSerializationScheme
|
||||
}
|
||||
return scheme to lookupKey.first
|
||||
throw UnsupportedOperationException("Serialization scheme not supported.")
|
||||
} to magic
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
@ -115,9 +106,9 @@ open class SerializationFactoryImpl : SerializationFactory() {
|
||||
override fun <T : Any> deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): ObjectWithCompatibleContext<T> {
|
||||
return asCurrent {
|
||||
withCurrentContext(context) {
|
||||
val (scheme, versionHeader) = schemeFor(byteSequence, context.useCase)
|
||||
val (scheme, magic) = schemeFor(byteSequence, context.useCase)
|
||||
val deserializedObject = scheme.deserialize(byteSequence, clazz, context)
|
||||
ObjectWithCompatibleContext(deserializedObject, context.withPreferredSerializationVersion(versionHeader))
|
||||
ObjectWithCompatibleContext(deserializedObject, context.withPreferredSerializationVersion(magic))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,9 +140,7 @@ open class SerializationFactoryImpl : SerializationFactory() {
|
||||
|
||||
|
||||
interface SerializationScheme {
|
||||
// byteSequence expected to just be the 8 bytes necessary for versioning
|
||||
fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean
|
||||
|
||||
fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T
|
||||
|
||||
|
@ -5,8 +5,8 @@ package net.corda.nodeapi.internal.serialization
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
object QuasarWhitelist : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>): Boolean = true
|
||||
@ -22,29 +22,25 @@ object QuasarWhitelist : ClassWhitelist {
|
||||
* MUST be kept separate!
|
||||
*/
|
||||
|
||||
val KRYO_RPC_SERVER_CONTEXT = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val KRYO_RPC_SERVER_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.RPCServer)
|
||||
|
||||
val KRYO_STORAGE_CONTEXT = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val KRYO_STORAGE_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
AllButBlacklisted,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Storage)
|
||||
|
||||
|
||||
val AMQP_STORAGE_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
val AMQP_STORAGE_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
AllButBlacklisted,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Storage)
|
||||
|
||||
val AMQP_RPC_SERVER_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
val AMQP_RPC_SERVER_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
|
@ -4,8 +4,8 @@ package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
/*
|
||||
* Serialisation contexts shared by the server and client.
|
||||
@ -15,21 +15,19 @@ import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
* MUST be kept separate from these ones!
|
||||
*/
|
||||
|
||||
val KRYO_P2P_CONTEXT = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val KRYO_P2P_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P)
|
||||
|
||||
val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
QuasarWhitelist,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Checkpoint)
|
||||
|
||||
val AMQP_P2P_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
val AMQP_P2P_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
|
@ -18,7 +18,7 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
if (obj is ByteArray) {
|
||||
data.putObject(Binary(obj))
|
||||
} else {
|
||||
|
@ -4,6 +4,7 @@ package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.MutableClassWhitelist
|
||||
@ -12,7 +13,7 @@ import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0
|
||||
val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == amqpMagic
|
||||
|
||||
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
|
||||
require(types.toSet().size == types.size) {
|
||||
@ -106,7 +107,7 @@ abstract class AbstractAMQPSerializationScheme(val cordappLoader: List<Cordapp>)
|
||||
return SerializationOutput(serializerFactory).serialize(obj)
|
||||
}
|
||||
|
||||
protected fun canDeserializeVersion(byteSequence: ByteSequence): Boolean = byteSequence == AmqpHeaderV1_0
|
||||
protected fun canDeserializeVersion(magic: CordaSerializationMagic) = magic == amqpMagic
|
||||
}
|
||||
|
||||
// TODO: This will eventually cover server RPC as well and move to node module, but for now this is not implemented
|
||||
@ -119,9 +120,9 @@ class AMQPServerSerializationScheme(cordapps: List<Cordapp> = emptyList()) : Abs
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return (canDeserializeVersion(byteSequence) &&
|
||||
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage))
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
|
||||
return canDeserializeVersion(magic) &&
|
||||
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage)
|
||||
}
|
||||
|
||||
}
|
||||
@ -136,9 +137,9 @@ class AMQPClientSerializationScheme(cordapps: List<Cordapp> = emptyList()) : Abs
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return (canDeserializeVersion(byteSequence) &&
|
||||
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage))
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
|
||||
return canDeserializeVersion(magic) &&
|
||||
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ interface AMQPSerializer<out T> {
|
||||
/**
|
||||
* Write the given object, with declared type, to the output.
|
||||
*/
|
||||
fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput)
|
||||
fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int = 0)
|
||||
|
||||
/**
|
||||
* Read the given object from the input. The envelope is provided in case the schema is required.
|
||||
|
@ -45,12 +45,12 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
withList {
|
||||
for (entry in obj as Array<*>) {
|
||||
output.writeObjectOrNull(entry, this, elementType)
|
||||
output.writeObjectOrNull(entry, this, elementType, debugIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,15 +109,19 @@ abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : Arr
|
||||
|
||||
class PrimIntArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(IntArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as IntArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as IntArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimCharArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(CharArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as CharArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) { (obj as CharArray).forEach {
|
||||
output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
@ -132,35 +136,45 @@ class PrimCharArraySerializer(factory: SerializerFactory) :
|
||||
|
||||
class PrimBooleanArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(BooleanArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as BooleanArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as BooleanArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimDoubleArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(DoubleArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as DoubleArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as DoubleArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimFloatArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(FloatArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as FloatArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as FloatArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimShortArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(ShortArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as ShortArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as ShortArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimLongArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(LongArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
localWriteObject(data) { (obj as LongArray).forEach { output.writeObjectOrNull(it, data, elementType) } }
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as LongArray).forEach { output.writeObjectOrNull(it, data, elementType, debugIndent+1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,18 +66,26 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
withList {
|
||||
for (entry in obj as Collection<*>) {
|
||||
output.writeObjectOrNull(entry, this, declaredType.actualTypeArguments[0])
|
||||
output.writeObjectOrNull(entry, this, declaredType.actualTypeArguments[0], debugIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
override fun readObject(
|
||||
obj: Any,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// TODO: Can we verify the entries in the list?
|
||||
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0]) })
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class CorDappCustomSerializer(
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
val proxy = uncheckedCast<SerializationCustomSerializer<*, *>,
|
||||
SerializationCustomSerializer<Any?, Any?>>(serializer).toProxy(obj)
|
||||
|
||||
|
@ -40,7 +40,7 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
*/
|
||||
override val revealSubclassesInSchema: Boolean get() = false
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
data.withDescribed(descriptor) {
|
||||
writeDescribedObject(uncheckedCast(obj), data, type, output)
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.TypeVariable
|
||||
import java.lang.reflect.WildcardType
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
data class ObjectAndEnvelope<out T>(val obj: T, val envelope: Envelope)
|
||||
|
||||
@ -51,27 +50,19 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>): T =
|
||||
deserialize(bytes, T::class.java)
|
||||
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>): T = deserialize(bytes, T::class.java)
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
inline internal fun <reified T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>): ObjectAndEnvelope<T> =
|
||||
deserializeAndReturnEnvelope(bytes, T::class.java)
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
internal fun getEnvelope(bytes: ByteSequence): Envelope {
|
||||
internal fun getEnvelope(byteSequence: ByteSequence): Envelope {
|
||||
// Check that the lead bytes match expected header
|
||||
val headerSize = AmqpHeaderV1_0.size
|
||||
if (bytes.take(headerSize) != AmqpHeaderV1_0) {
|
||||
throw NotSerializableException("Serialization header does not match.")
|
||||
}
|
||||
|
||||
val dataBytes = amqpMagic.consume(byteSequence) ?: throw NotSerializableException("Serialization header does not match.")
|
||||
val data = Data.Factory.create()
|
||||
val size = data.decode(ByteBuffer.wrap(bytes.bytes, bytes.offset + headerSize, bytes.size - headerSize))
|
||||
if (size.toInt() != bytes.size - headerSize) {
|
||||
throw NotSerializableException("Unexpected size of data")
|
||||
}
|
||||
|
||||
val expectedSize = dataBytes.remaining()
|
||||
if (data.decode(dataBytes) != expectedSize.toLong()) throw NotSerializableException("Unexpected size of data")
|
||||
return Envelope.get(data)
|
||||
}
|
||||
|
||||
@ -106,11 +97,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema), clazz)), envelope)
|
||||
}
|
||||
|
||||
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type): Any? {
|
||||
return if (obj == null) null else readObject(obj, schema, type)
|
||||
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, offset: Int = 0): Any? {
|
||||
return if (obj == null) null else readObject(obj, schema, type, offset)
|
||||
}
|
||||
|
||||
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type): Any =
|
||||
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, debugIndent: Int = 0): Any =
|
||||
if (obj is DescribedType && ReferencedObject.DESCRIPTOR == obj.descriptor) {
|
||||
// It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference.
|
||||
val objectIndex = (obj.described as UnsignedInteger).toInt()
|
||||
@ -119,8 +110,11 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
"is outside of the bounds for the list of size: ${objectHistory.size}")
|
||||
|
||||
val objectRetrieved = objectHistory[objectIndex]
|
||||
if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!))
|
||||
throw NotSerializableException("Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}'")
|
||||
if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) {
|
||||
throw NotSerializableException(
|
||||
"Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " +
|
||||
"@ ${objectIndex}")
|
||||
}
|
||||
objectRetrieved
|
||||
} else {
|
||||
val objectRead = when (obj) {
|
||||
@ -138,7 +132,9 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
|
||||
// Store the reference in case we need it later on.
|
||||
// Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content
|
||||
if (suitableForObjectReference(objectRead.javaClass)) objectHistory.add(objectRead)
|
||||
if (suitableForObjectReference(objectRead.javaClass)) {
|
||||
objectHistory.add(objectRead)
|
||||
}
|
||||
objectRead
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ class EnumEvolutionSerializer(
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
|
||||
return fromOrd
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't")
|
||||
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
|
@ -9,14 +9,22 @@ import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
|
||||
/**
|
||||
* Serializer for deserializing objects whose definition has changed since they
|
||||
* were serialised.
|
||||
*
|
||||
* @property oldReaders A linked map representing the properties of the object as they were serialized. Note
|
||||
* this may contain properties that are no longer needed by the class. These *must* be read however to ensure
|
||||
* any refferenced objects in the object stream are captured properly
|
||||
* @property kotlinConstructor
|
||||
* @property constructorArgs used to hold the properties as sent to the object's constructor. Passed in as a
|
||||
* pre populated array as properties not present on the old constructor must be initialised in the factory
|
||||
*/
|
||||
class EvolutionSerializer(
|
||||
abstract class EvolutionSerializer(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
val readers: List<OldParam?>,
|
||||
protected val oldReaders: Map<String, OldParam>,
|
||||
override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) {
|
||||
|
||||
// explicitly set as empty to indicate it's unused by this type of serializer
|
||||
@ -26,14 +34,17 @@ class EvolutionSerializer(
|
||||
* Represents a parameter as would be passed to the constructor of the class as it was
|
||||
* when it was serialised and NOT how that class appears now
|
||||
*
|
||||
* @param type The jvm type of the parameter
|
||||
* @param idx where in the parameter list this parameter falls. Required as the parameter
|
||||
* order may have been changed and we need to know where into the list to look
|
||||
* @param resultsIndex index into the constructor argument list where the read property
|
||||
* should be placed
|
||||
* @param property object to read the actual property value
|
||||
*/
|
||||
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
|
||||
fun readProperty(paramValues: List<*>, schemas: SerializationSchemas, input: DeserializationInput) =
|
||||
property.readProperty(paramValues[idx], schemas, input)
|
||||
data class OldParam(var resultsIndex: Int, val property: PropertySerializer) {
|
||||
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, new: Array<Any?>) =
|
||||
property.readProperty(obj, schemas, input).apply {
|
||||
if(resultsIndex >= 0) {
|
||||
new[resultsIndex] = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -47,11 +58,12 @@ class EvolutionSerializer(
|
||||
* TODO: Type evolution
|
||||
* TODO: rename annotation
|
||||
*/
|
||||
private fun getEvolverConstructor(type: Type, oldArgs: Map<String?, Type>): KFunction<Any>? {
|
||||
private fun getEvolverConstructor(type: Type, oldArgs: Map<String, OldParam>): KFunction<Any>? {
|
||||
val clazz: Class<*> = type.asClass()!!
|
||||
|
||||
if (!isConcrete(clazz)) return null
|
||||
|
||||
val oldArgumentSet = oldArgs.map { Pair(it.key, it.value) }
|
||||
val oldArgumentSet = oldArgs.map { Pair(it.key as String?, it.value.property.resolvedType) }
|
||||
|
||||
var maxConstructorVersion = Integer.MIN_VALUE
|
||||
var constructor: KFunction<Any>? = null
|
||||
@ -69,6 +81,36 @@ class EvolutionSerializer(
|
||||
return constructor ?: constructorForDeserialization(type)
|
||||
}
|
||||
|
||||
private fun makeWithConstructor(
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory,
|
||||
constructor: KFunction<Any>,
|
||||
readersAsSerialized: Map<String, OldParam>): AMQPSerializer<Any> {
|
||||
val constructorArgs = arrayOfNulls<Any?>(constructor.parameters.size)
|
||||
|
||||
constructor.parameters.withIndex().forEach {
|
||||
readersAsSerialized.get(it.value.name!!)?.apply {
|
||||
this.resultsIndex = it.index
|
||||
} ?: if (!it.value.type.isMarkedNullable) {
|
||||
throw NotSerializableException(
|
||||
"New parameter ${it.value.name} is mandatory, should be nullable for evolution to worK")
|
||||
}
|
||||
}
|
||||
return EvolutionSerializerViaConstructor (new.type, factory, readersAsSerialized, constructor, constructorArgs)
|
||||
}
|
||||
|
||||
private fun makeWithSetters(
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory,
|
||||
constructor: KFunction<Any>,
|
||||
readersAsSerialized: Map<String, OldParam>,
|
||||
classProperties: Map<String, PropertyDescriptor>): AMQPSerializer<Any> {
|
||||
val setters = propertiesForSerializationFromSetters(classProperties,
|
||||
new.type,
|
||||
factory).associateBy({ it.getter.name }, { it })
|
||||
return EvolutionSerializerViaSetters (new.type, factory, readersAsSerialized, constructor, setters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a serialization object for deserialization only of objects serialised
|
||||
* as different versions of a class.
|
||||
@ -82,38 +124,43 @@ class EvolutionSerializer(
|
||||
*/
|
||||
fun make(old: CompositeType, new: ObjectSerializer,
|
||||
factory: SerializerFactory): AMQPSerializer<Any> {
|
||||
|
||||
val oldFieldToType = old.fields.map {
|
||||
it.name as String? to it.getTypeAsClass(factory.classloader) as Type
|
||||
}.toMap()
|
||||
|
||||
val constructor = getEvolverConstructor(new.type, oldFieldToType) ?:
|
||||
throw NotSerializableException(
|
||||
"Attempt to deserialize an interface: ${new.type}. Serialized form is invalid.")
|
||||
|
||||
val oldArgs = mutableMapOf<String, OldParam>()
|
||||
var idx = 0
|
||||
// The order in which the properties were serialised is important and must be preserved
|
||||
val readersAsSerialized = LinkedHashMap<String, OldParam>()
|
||||
old.fields.forEach {
|
||||
val returnType = it.getTypeAsClass(factory.classloader)
|
||||
oldArgs[it.name] = OldParam(
|
||||
returnType, idx++, PropertySerializer.make(it.name, PublicPropertyReader(null), returnType, factory))
|
||||
readersAsSerialized[it.name] = try {
|
||||
OldParam(-1, PropertySerializer.make(it.name, EvolutionPropertyReader(),
|
||||
it.getTypeAsClass(factory.classloader), factory))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw NotSerializableException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
val readers = constructor.parameters.map {
|
||||
oldArgs[it.name!!] ?: if (!it.type.isMarkedNullable) {
|
||||
throw NotSerializableException(
|
||||
"New parameter ${it.name} is mandatory, should be nullable for evolution to worK")
|
||||
} else null
|
||||
}
|
||||
// cope with the situation where a generic interface was serialised as a type, in such cases
|
||||
// return the synthesised object which is, given the absence of a constructor, a no op
|
||||
val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new
|
||||
|
||||
return EvolutionSerializer(new.type, factory, readers, constructor)
|
||||
val classProperties = new.type.asClass()?.propertyDescriptors() ?: emptyMap()
|
||||
|
||||
return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) {
|
||||
makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties)
|
||||
}
|
||||
else {
|
||||
makeWithConstructor(new, factory, constructor, readersAsSerialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
}
|
||||
|
||||
class EvolutionSerializerViaConstructor(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
oldReaders: Map<String, EvolutionSerializer.OldParam>,
|
||||
kotlinConstructor: KFunction<Any>?,
|
||||
private val constructorArgs: Array<Any?>) : EvolutionSerializer (clazz, factory, oldReaders, kotlinConstructor) {
|
||||
/**
|
||||
* Unlike a normal [readObject] call where we simply apply the parameter deserialisers
|
||||
* to the object list of values we need to map that list, which is ordered per the
|
||||
@ -126,7 +173,40 @@ class EvolutionSerializer(
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
return construct(readers.map { it?.readProperty(obj, schemas, input) })
|
||||
// *must* read all the parameters in the order they were serialized
|
||||
oldReaders.values.zip(obj).map { it.first.readProperty(it.second, schemas, input, constructorArgs) }
|
||||
|
||||
return javaConstructor?.newInstance(*(constructorArgs)) ?:
|
||||
throw NotSerializableException(
|
||||
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific instance of an [EvolutionSerializer] where the properties of the object are set via calling
|
||||
* named setter functions on the instantiated object.
|
||||
*/
|
||||
class EvolutionSerializerViaSetters(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
oldReaders: Map<String, EvolutionSerializer.OldParam>,
|
||||
kotlinConstructor: KFunction<Any>?,
|
||||
private val setters: Map<String, PropertyAccessor>) : EvolutionSerializer (clazz, factory, oldReaders, kotlinConstructor) {
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
val instance : Any = javaConstructor?.newInstance() ?: throw NotSerializableException (
|
||||
"Failed to instantiate instance of object $clazz")
|
||||
|
||||
// *must* read all the parameters in the order they were serialized
|
||||
oldReaders.values.zip(obj).forEach {
|
||||
// if that property still exists on the new object then set it
|
||||
it.first.property.readProperty(it.second, schemas, input).apply {
|
||||
setters[it.first.property.name]?.set(instance, this)
|
||||
}
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,12 +232,24 @@ class EvolutionSerializerGetter : EvolutionSerializerGetterBase() {
|
||||
override fun getEvolutionSerializer(factory: SerializerFactory,
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> =
|
||||
factory.getSerializersByDescriptor().computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||
when (typeNotation) {
|
||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
|
||||
is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, factory, schemas)
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||
return factory.getSerializersByDescriptor().computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||
when (typeNotation) {
|
||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
|
||||
is RestrictedType -> {
|
||||
// The fingerprint of a generic collection can be changed through bug fixes to the
|
||||
// fingerprinting function making it appear as if the class has altered whereas it hasn't.
|
||||
// Given we don't support the evolution of these generic containers, if it appears
|
||||
// one has been changed, simply return the original serializer and associate it with
|
||||
// both the new and old fingerprint
|
||||
if (newSerializer is CollectionSerializer || newSerializer is MapSerializer) {
|
||||
newSerializer
|
||||
} else {
|
||||
EnumEvolutionSerializer.make(typeNotation, newSerializer, factory, schemas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,12 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
obj.javaClass.checkSupportedMapType()
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
@ -81,8 +86,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
data.putMap()
|
||||
data.enter()
|
||||
for ((key, value) in obj as Map<*, *>) {
|
||||
output.writeObjectOrNull(key, data, declaredType.actualTypeArguments[0])
|
||||
output.writeObjectOrNull(value, data, declaredType.actualTypeArguments[1])
|
||||
output.writeObjectOrNull(key, data, declaredType.actualTypeArguments[0], debugIndent)
|
||||
output.writeObjectOrNull(value, data, declaredType.actualTypeArguments[1], debugIndent)
|
||||
}
|
||||
data.exit() // exit map
|
||||
}
|
||||
|
@ -52,13 +52,18 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ clazz.typeName }) {
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
// Write list
|
||||
withList {
|
||||
propertySerializers.serializationOrder.forEach { property ->
|
||||
property.getter.writeProperty(obj, this, output)
|
||||
property.getter.writeProperty(obj, this, output, debugIndent+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import java.lang.reflect.Type
|
||||
*/
|
||||
sealed class PropertySerializer(val name: String, val propertyReader: PropertyReader, val resolvedType: Type) {
|
||||
abstract fun writeClassInfo(output: SerializationOutput)
|
||||
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
|
||||
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int = 0)
|
||||
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput): Any?
|
||||
|
||||
val type: String = generateType()
|
||||
@ -80,8 +80,8 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe
|
||||
input.readObjectOrNull(obj, schemas, resolvedType)
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
||||
output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType)
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int) = ifThrowsAppend({ nameForDebug }) {
|
||||
output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType, debugIndent)
|
||||
}
|
||||
|
||||
private val nameForDebug = "$name(${resolvedType.typeName})"
|
||||
@ -100,7 +100,7 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe
|
||||
return if (obj is Binary) obj.array else obj
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int) {
|
||||
val value = propertyReader.read(obj)
|
||||
if (value is ByteArray) {
|
||||
data.putObject(Binary(value))
|
||||
@ -123,7 +123,7 @@ sealed class PropertySerializer(val name: String, val propertyReader: PropertyRe
|
||||
return if (obj == null) null else (obj as Short).toChar()
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, debugIndent: Int) {
|
||||
val input = propertyReader.read(obj)
|
||||
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ abstract class PropertyReader {
|
||||
abstract fun isNullable(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for those properties of a class that have defined getter functions.
|
||||
*/
|
||||
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
|
||||
init {
|
||||
readMethod?.isAccessible = true
|
||||
@ -43,6 +46,10 @@ class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
|
||||
override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for those properties of a class that do not have defined getter functions. In which case
|
||||
* we used reflection to remove the unreadable status from that property whilst it's accessed.
|
||||
*/
|
||||
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
|
||||
init {
|
||||
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
|
||||
@ -68,6 +75,20 @@ class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special instance of a [PropertyReader] for use only by [EvolutionSerializer]s to make
|
||||
* it explicit that no properties are ever actually read from an object as the evolution
|
||||
* serializer should only be accessing the already serialized form.
|
||||
*/
|
||||
class EvolutionPropertyReader : PropertyReader() {
|
||||
override fun read(obj: Any?): Any? {
|
||||
throw UnsupportedOperationException("It should be impossible for an evolution serializer to "
|
||||
+ "be reading from an object")
|
||||
}
|
||||
|
||||
override fun isNullable() = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a generic interface to a serializable property of an object.
|
||||
*
|
||||
@ -102,12 +123,18 @@ abstract class PropertyAccessor(
|
||||
class PropertyAccessorGetterSetter(
|
||||
initialPosition: Int,
|
||||
getter: PropertySerializer,
|
||||
private val setter: Method?) : PropertyAccessor(initialPosition, getter) {
|
||||
private val setter: Method) : PropertyAccessor(initialPosition, getter) {
|
||||
init {
|
||||
/**
|
||||
* Play nicely with Java interop, public methods aren't marked as accessible
|
||||
*/
|
||||
setter.isAccessible = true
|
||||
}
|
||||
/**
|
||||
* Invokes the setter on the underlying object passing in the serialized value.
|
||||
*/
|
||||
override fun set(instance: Any, obj: Any?) {
|
||||
setter?.invoke(instance, *listOf(obj).toTypedArray())
|
||||
setter.invoke(instance, *listOf(obj).toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package net.corda.nodeapi.internal.serialization.amqp
|
||||
import com.google.common.hash.Hasher
|
||||
import com.google.common.hash.Hashing
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toBase64
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
@ -18,9 +18,7 @@ import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterFiel
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
|
||||
|
||||
const val DESCRIPTOR_DOMAIN: String = "net.corda"
|
||||
|
||||
// "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB
|
||||
val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray())
|
||||
val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0, 0))
|
||||
|
||||
/**
|
||||
* This and the classes below are OO representations of the AMQP XML schema described in the specification. Their
|
||||
@ -349,14 +347,16 @@ private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFact
|
||||
// This method concatenates various elements of the types recursively as unencoded strings into the hasher, effectively
|
||||
// creating a unique string for a type which we then hash in the calling function above.
|
||||
private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>,
|
||||
hasher: Hasher, factory: SerializerFactory, offset: Int = 4): Hasher {
|
||||
|
||||
hasher: Hasher, factory: SerializerFactory, debugIndent: Int = 1): Hasher {
|
||||
// We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
|
||||
// generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
|
||||
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
||||
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
||||
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
||||
return if (type in alreadySeen && (type !is SerializerFactory.AnyType) && (type !is TypeVariable<*>)) {
|
||||
return if ((type in alreadySeen)
|
||||
&& (type !is SerializerFactory.AnyType)
|
||||
&& (type !is TypeVariable<*>)
|
||||
&& (type !is WildcardType)) {
|
||||
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
|
||||
} else {
|
||||
alreadySeen += type
|
||||
@ -370,29 +370,35 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
|
||||
hasher.putUnencodedChars(clazz.name)
|
||||
} else {
|
||||
hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) {
|
||||
fingerprintForObject(type, type, alreadySeen, hasher, factory, offset+4)
|
||||
fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1)
|
||||
}
|
||||
}
|
||||
|
||||
// ... and concatenate the type data for each parameter type.
|
||||
type.actualTypeArguments.fold(startingHash) { orig, paramType ->
|
||||
fingerprintForType(paramType, type, alreadySeen, orig, factory, offset+4)
|
||||
fingerprintForType(paramType, type, alreadySeen, orig, factory, debugIndent+1)
|
||||
}
|
||||
}
|
||||
// Treat generic types as "any type" to prevent fingerprint mismatch. This case we fall into when
|
||||
// looking at A and B from Example<A, B> (remember we call this function recursively). When
|
||||
// serialising a concrete example of the type we have A and B which are TypeVariables<*>'s but
|
||||
// when deserializing we only have the wildcard placeholder ?, or AnyType
|
||||
// Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing
|
||||
// the signature of the fingerprinted object. This, however, doesn't work as it breaks bi-
|
||||
// directional fingerprints. That is, fingerprinting a concrete instance of a generic
|
||||
// type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>)
|
||||
//
|
||||
// Note, TypeVariable<*> used to be encoded as TYPE_VARIABLE_HASH but that again produces a
|
||||
// differing fingerprint on serialisation and deserialization
|
||||
// On serialization Example<Int> is treated as Example<T>, a TypeVariable
|
||||
// On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable
|
||||
// Note: AnyType is a special case of WildcardType used in other parts of the
|
||||
// serializer so both cases need to be dealt with here
|
||||
//
|
||||
// If we treat these types as fundamentally different and alter the fingerprint we will
|
||||
// end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter.
|
||||
is SerializerFactory.AnyType,
|
||||
is WildcardType,
|
||||
is TypeVariable<*> -> {
|
||||
hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH)
|
||||
}
|
||||
is Class<*> -> {
|
||||
if (type.isArray) {
|
||||
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, offset+4)
|
||||
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, debugIndent+1)
|
||||
.putUnencodedChars(ARRAY_HASH)
|
||||
} else if (SerializerFactory.isPrimitive(type)) {
|
||||
hasher.putUnencodedChars(type.name)
|
||||
@ -412,17 +418,15 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
|
||||
// to the CorDapp but maybe reference to the JAR in the short term.
|
||||
hasher.putUnencodedChars(type.name)
|
||||
} else {
|
||||
fingerprintForObject(type, type, alreadySeen, hasher, factory, offset+4)
|
||||
fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hash the element type + some array hash
|
||||
is GenericArrayType -> fingerprintForType(type.genericComponentType, contextType, alreadySeen,
|
||||
hasher, factory, offset+4).putUnencodedChars(ARRAY_HASH)
|
||||
// TODO: include bounds
|
||||
is WildcardType -> {
|
||||
hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH)
|
||||
is GenericArrayType -> {
|
||||
fingerprintForType(type.genericComponentType, contextType, alreadySeen,
|
||||
hasher, factory, debugIndent+1).putUnencodedChars(ARRAY_HASH)
|
||||
}
|
||||
else -> throw NotSerializableException("Don't know how to hash")
|
||||
}
|
||||
@ -443,16 +447,17 @@ private fun fingerprintForObject(
|
||||
alreadySeen: MutableSet<Type>,
|
||||
hasher: Hasher,
|
||||
factory: SerializerFactory,
|
||||
offset: Int = 0): Hasher {
|
||||
debugIndent: Int = 0): Hasher {
|
||||
// Hash the class + properties + interfaces
|
||||
val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
|
||||
|
||||
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory)
|
||||
.serializationOrder
|
||||
.fold(hasher.putUnencodedChars(name)) { orig, prop ->
|
||||
fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, offset+4)
|
||||
fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, debugIndent+1)
|
||||
.putUnencodedChars(prop.getter.name)
|
||||
.putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
|
||||
}
|
||||
interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, offset+4) }
|
||||
interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, debugIndent+1) }
|
||||
return hasher
|
||||
}
|
||||
|
@ -6,11 +6,9 @@ import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.beans.IndexedPropertyDescriptor
|
||||
import java.beans.Introspector
|
||||
import java.beans.PropertyDescriptor
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.*
|
||||
import java.lang.reflect.Field
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
@ -62,71 +60,194 @@ internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the properties to be used during serialization by attempting to find those that match the parameters to the
|
||||
* deserialization constructor, if the class is concrete. If it is abstract, or an interface, then use all the properties.
|
||||
* Identifies the properties to be used during serialization by attempting to find those that match the parameters
|
||||
* to the deserialization constructor, if the class is concrete. If it is abstract, or an interface, then use all
|
||||
* the properties.
|
||||
*
|
||||
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have
|
||||
* names accessible via reflection.
|
||||
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters
|
||||
* have names accessible via reflection.
|
||||
*/
|
||||
internal fun <T : Any> propertiesForSerialization(
|
||||
kotlinConstructor: KFunction<T>?,
|
||||
type: Type,
|
||||
factory: SerializerFactory) = PropertySerializers.make (
|
||||
if (kotlinConstructor != null) {
|
||||
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
|
||||
} else {
|
||||
propertiesForSerializationFromAbstract(type.asClass()!!, type, factory)
|
||||
}.sortedWith(PropertyAccessor))
|
||||
factory: SerializerFactory) = PropertySerializers.make(
|
||||
if (kotlinConstructor != null) {
|
||||
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
|
||||
} else {
|
||||
propertiesForSerializationFromAbstract(type.asClass()!!, type, factory)
|
||||
}.sortedWith(PropertyAccessor))
|
||||
|
||||
|
||||
fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
|
||||
|
||||
/**
|
||||
* Encapsulates the property of a class and its potential getter and setter methods.
|
||||
*
|
||||
* @property field a property of a class.
|
||||
* @property setter the method of a class that sets the field. Determined by locating
|
||||
* a function called setXyz on the class for the property named in field as xyz.
|
||||
* @property getter the method of a class that returns a fields value. Determined by
|
||||
* locating a function named getXyz for the property named in field as xyz.
|
||||
*/
|
||||
data class PropertyDescriptor(var field: Field?, var setter: Method?, var getter: Method?, var iser: Method?) {
|
||||
override fun toString() = StringBuilder("").apply {
|
||||
appendln("Property - ${field?.name ?: "null field"}\n")
|
||||
appendln(" getter - ${getter?.name ?: "no getter"}")
|
||||
appendln(" setter - ${setter?.name ?: "no setter"}")
|
||||
appendln(" iser - ${iser?.name ?: "no isXYZ defined"}")
|
||||
}.toString()
|
||||
|
||||
constructor() : this(null, null, null, null)
|
||||
|
||||
fun preferredGetter() : Method? = getter ?: iser
|
||||
}
|
||||
|
||||
object PropertyDescriptorsRegex {
|
||||
// match an uppercase letter that also has a corresponding lower case equivalent
|
||||
val re = Regex("(?<type>get|set|is)(?<var>\\p{Lu}.*)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Collate the properties of a class and match them with their getter and setter
|
||||
* methods as per a JavaBean.
|
||||
*
|
||||
* for a property
|
||||
* exampleProperty
|
||||
*
|
||||
* We look for methods
|
||||
* setExampleProperty
|
||||
* getExampleProperty
|
||||
* isExampleProperty
|
||||
*
|
||||
* Where setExampleProperty must return a type compatible with exampleProperty, getExampleProperty must
|
||||
* take a single parameter of a type compatible with exampleProperty and isExampleProperty must
|
||||
* return a boolean
|
||||
*/
|
||||
fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
|
||||
val classProperties = mutableMapOf<String, PropertyDescriptor>()
|
||||
|
||||
var clazz: Class<out Any?>? = this
|
||||
|
||||
do {
|
||||
clazz!!.declaredFields.forEach { property ->
|
||||
classProperties.computeIfAbsent(property.name) {
|
||||
PropertyDescriptor()
|
||||
}.apply {
|
||||
this.field = property
|
||||
}
|
||||
}
|
||||
|
||||
// Note: It is possible for a class to have multiple instances of a function where the types
|
||||
// differ. For example:
|
||||
// interface I<out T> { val a: T }
|
||||
// class D(override val a: String) : I<String>
|
||||
// instances of D will have both
|
||||
// getA - returning a String (java.lang.String) and
|
||||
// getA - returning an Object (java.lang.Object)
|
||||
// In this instance we take the most derived object
|
||||
//
|
||||
// In addition, only getters that take zero parameters and setters that take a single
|
||||
// parameter will be considered
|
||||
clazz.declaredMethods?.map { func ->
|
||||
if (!Modifier.isPublic(func.modifiers)) return@map
|
||||
if (func.name == "getClass") return@map
|
||||
|
||||
PropertyDescriptorsRegex.re.find(func.name)?.apply {
|
||||
// take into account those constructor properties that don't directly map to a named
|
||||
// property which are, by default, already added to the map
|
||||
classProperties.computeIfAbsent(groups[2]!!.value.decapitalize()) { PropertyDescriptor() }.apply {
|
||||
when (groups[1]!!.value) {
|
||||
"set" -> {
|
||||
if (func.parameterCount == 1) {
|
||||
if (setter == null) setter = func
|
||||
else if (TypeToken.of(setter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) {
|
||||
setter = func
|
||||
}
|
||||
}
|
||||
}
|
||||
"get" -> {
|
||||
if (func.parameterCount == 0) {
|
||||
if (getter == null) getter = func
|
||||
else if (TypeToken.of(getter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) {
|
||||
getter = func
|
||||
}
|
||||
}
|
||||
}
|
||||
"is" -> {
|
||||
if (func.parameterCount == 0) {
|
||||
val rtnType = TypeToken.of(func.genericReturnType)
|
||||
if ((rtnType == TypeToken.of(Boolean::class.java))
|
||||
|| (rtnType == TypeToken.of(Boolean::class.javaObjectType))) {
|
||||
if (iser == null) iser = func
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clazz = clazz.superclass
|
||||
} while (clazz != null)
|
||||
|
||||
return classProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* From a constructor, determine which properties of a class are to be serialized.
|
||||
*
|
||||
* @param kotlinConstructor The constructor to be used to instantiate instances of the class
|
||||
* @param type The class's [Type]
|
||||
* @param factory The factory generating the serializer wrapping this function.
|
||||
*/
|
||||
internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
kotlinConstructor: KFunction<T>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
|
||||
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
|
||||
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors
|
||||
.filter { it.name != "class" }
|
||||
.groupBy { it.name }
|
||||
.mapValues { it.value[0] }
|
||||
|
||||
if (properties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) {
|
||||
return propertiesForSerializationFromSetters(properties, type, factory)
|
||||
val classProperties = clazz.propertyDescriptors()
|
||||
|
||||
if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) {
|
||||
return propertiesForSerializationFromSetters(classProperties, type, factory)
|
||||
}
|
||||
|
||||
return mutableListOf<PropertyAccessor>().apply {
|
||||
kotlinConstructor.parameters.withIndex().forEach { param ->
|
||||
val name = param.value.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.")
|
||||
val name = param.value.name ?: throw NotSerializableException(
|
||||
"Constructor parameter of $clazz has no name.")
|
||||
|
||||
val propertyReader = if (name in properties) {
|
||||
// it's a publicly accessible property
|
||||
val matchingProperty = properties[name]!!
|
||||
val propertyReader = if (name in classProperties) {
|
||||
if (classProperties[name]!!.getter != null) {
|
||||
// it's a publicly accessible property
|
||||
val matchingProperty = classProperties[name]!!
|
||||
|
||||
// Check that the method has a getter in java.
|
||||
val getter = matchingProperty.readMethod ?: throw NotSerializableException(
|
||||
"Property has no getter method for $name of $clazz. If using Java and the parameter name"
|
||||
+ "looks anonymous, check that you have the -parameters option specified in the Java compiler."
|
||||
+ "Alternately, provide a proxy serializer (SerializationCustomSerializer) if "
|
||||
+ "recompiling isn't an option.")
|
||||
// Check that the method has a getter in java.
|
||||
val getter = matchingProperty.getter ?: throw NotSerializableException(
|
||||
"Property has no getter method for $name of $clazz. If using Java and the parameter name"
|
||||
+ "looks anonymous, check that you have the -parameters option specified in the "
|
||||
+ "Java compiler. Alternately, provide a proxy serializer "
|
||||
+ "(SerializationCustomSerializer) if recompiling isn't an option.")
|
||||
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) {
|
||||
throw NotSerializableException(
|
||||
"Property type $returnType for $name of $clazz differs from constructor parameter "
|
||||
+ "type ${param.value.type.javaType}")
|
||||
}
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) {
|
||||
throw NotSerializableException(
|
||||
"Property '$name' has type '$returnType' on class '$clazz' but differs from constructor " +
|
||||
"parameter type '${param.value.type.javaType}'")
|
||||
}
|
||||
|
||||
Pair(PublicPropertyReader(getter), returnType)
|
||||
} else {
|
||||
val field = classProperties[name]!!.field ?:
|
||||
throw NotSerializableException("No property matching constructor parameter named '$name' " +
|
||||
"of '$clazz'. If using Java, check that you have the -parameters option specified " +
|
||||
"in the Java compiler. Alternately, provide a proxy serializer " +
|
||||
"(SerializationCustomSerializer) if recompiling isn't an option")
|
||||
|
||||
Pair(PublicPropertyReader(getter), returnType)
|
||||
} else {
|
||||
try {
|
||||
val field = clazz.getDeclaredField(param.value.name)
|
||||
Pair(PrivatePropertyReader(field, type), field.genericType)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
throw NotSerializableException("No property matching constructor parameter named '$name' of '$clazz'. " +
|
||||
"If using Java, check that you have the -parameters option specified in the Java compiler. " +
|
||||
"Alternately, provide a proxy serializer (SerializationCustomSerializer) if recompiling isn't an option")
|
||||
}
|
||||
} else {
|
||||
throw NotSerializableException(
|
||||
"Constructor parameter $name doesn't refer to a property of class '$clazz'")
|
||||
}
|
||||
|
||||
this += PropertyAccessorConstructor(
|
||||
@ -140,23 +261,40 @@ internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
* If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters
|
||||
* and use those
|
||||
*/
|
||||
private fun propertiesForSerializationFromSetters(
|
||||
fun propertiesForSerializationFromSetters(
|
||||
properties: Map<String, PropertyDescriptor>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
return mutableListOf<PropertyAccessorGetterSetter>().apply {
|
||||
var idx = 0
|
||||
|
||||
properties.forEach { property ->
|
||||
val getter: Method? = property.value.readMethod
|
||||
val setter: Method? = property.value.writeMethod
|
||||
val getter: Method? = property.value.preferredGetter()
|
||||
val setter: Method? = property.value.setter
|
||||
|
||||
if (getter == null || setter == null) return@forEach
|
||||
|
||||
// NOTE: There is no need to check return and parameter types vs the underlying type for
|
||||
// the getter / setter vs property as if there is a difference then that property isn't reported
|
||||
// by the BEAN inspector and thus we don't consider that case here
|
||||
if (setter.parameterCount != 1) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes too many arguments")
|
||||
}
|
||||
|
||||
this += PropertyAccessorGetterSetter (
|
||||
val setterType = setter.parameterTypes[0]!!
|
||||
|
||||
if ((property.value.field != null) &&
|
||||
(!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType)))) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes parameter of type $setterType yet underlying type is " +
|
||||
"${property.value.field?.genericType!!}")
|
||||
}
|
||||
|
||||
// make sure the setter returns the same type (within inheritance bounds) the getter accepts
|
||||
if (!(TypeToken.of (setterType).isSupertypeOf(getter.returnType))) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes parameter of type $setterType yet the defined getter returns a value of type " +
|
||||
"${getter.returnType}")
|
||||
}
|
||||
this += PropertyAccessorGetterSetter(
|
||||
idx++,
|
||||
PropertySerializer.make(property.key, PublicPropertyReader(getter),
|
||||
resolveTypeVariables(getter.genericReturnType, type), factory),
|
||||
@ -165,32 +303,39 @@ private fun propertiesForSerializationFromSetters(
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawGetterReturnType: Type, param: KParameter): Boolean {
|
||||
val typeToken = TypeToken.of(param.type.javaType)
|
||||
return typeToken.isSupertypeOf(getterReturnType) || typeToken.isSupertypeOf(rawGetterReturnType)
|
||||
private fun constructorParamTakesReturnTypeOfGetter(
|
||||
getterReturnType: Type,
|
||||
rawGetterReturnType: Type,
|
||||
param: KParameter): Boolean {
|
||||
val paramToken = TypeToken.of(param.type.javaType)
|
||||
val rawParamType = TypeToken.of(paramToken.rawType)
|
||||
|
||||
return paramToken.isSupertypeOf(getterReturnType)
|
||||
|| paramToken.isSupertypeOf(rawGetterReturnType)
|
||||
// cope with the case where the constructor parameter is a generic type (T etc) but we
|
||||
// can discover it's raw type. When bounded this wil be the bounding type, unbounded
|
||||
// generics this will be object
|
||||
|| rawParamType.isSupertypeOf(getterReturnType)
|
||||
|| rawParamType.isSupertypeOf(rawGetterReturnType)
|
||||
}
|
||||
|
||||
private fun propertiesForSerializationFromAbstract(
|
||||
clazz: Class<*>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
|
||||
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors
|
||||
.filter { it.name != "class" }
|
||||
.sortedBy { it.name }
|
||||
.filterNot { it is IndexedPropertyDescriptor }
|
||||
val properties = clazz.propertyDescriptors()
|
||||
|
||||
return mutableListOf<PropertyAccessorConstructor>().apply {
|
||||
properties.withIndex().forEach { property ->
|
||||
// Check that the method has a getter in java.
|
||||
val getter = property.value.readMethod ?: throw NotSerializableException(
|
||||
"Property has no getter method for ${property.value.name} of $clazz.")
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
this += PropertyAccessorConstructor(
|
||||
property.index,
|
||||
PropertySerializer.make(property.value.name, PublicPropertyReader(getter), returnType, factory))
|
||||
}
|
||||
}
|
||||
return mutableListOf<PropertyAccessorConstructor>().apply {
|
||||
properties.toList().withIndex().forEach {
|
||||
val getter = it.value.second.getter ?: return@forEach
|
||||
if (it.value.second.field == null) return@forEach
|
||||
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
this += PropertyAccessorConstructor(
|
||||
it.index,
|
||||
PropertySerializer.make(it.value.first, PublicPropertyReader(getter), returnType, factory))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> {
|
||||
@ -258,7 +403,11 @@ private fun resolveTypeVariables(actualType: Type, contextType: Type?): Type {
|
||||
// TODO: surely we check it is concrete at this point with no TypeVariables
|
||||
return if (resolvedType is TypeVariable<*>) {
|
||||
val bounds = resolvedType.bounds
|
||||
return if (bounds.isEmpty()) SerializerFactory.AnyType else if (bounds.size == 1) resolveTypeVariables(bounds[0], contextType) else throw NotSerializableException("Got bounded type $actualType but only support single bound.")
|
||||
return if (bounds.isEmpty()) {
|
||||
SerializerFactory.AnyType
|
||||
} else if (bounds.size == 1) {
|
||||
resolveTypeVariables(bounds[0], contextType)
|
||||
} else throw NotSerializableException("Got bounded type $actualType but only support single bound.")
|
||||
} else {
|
||||
resolvedType
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
|
||||
}
|
||||
val bytes = ByteArray(data.encodedSize().toInt() + 8)
|
||||
val buf = ByteBuffer.wrap(bytes)
|
||||
buf.put(AmqpHeaderV1_0.bytes)
|
||||
amqpMagic.putTo(buf)
|
||||
data.encode(buf)
|
||||
return SerializedBytes(bytes)
|
||||
}
|
||||
@ -86,15 +86,15 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
|
||||
data.putObject(transformsSchema)
|
||||
}
|
||||
|
||||
internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type) {
|
||||
internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type, debugIndent: Int) {
|
||||
if (obj == null) {
|
||||
data.putNull()
|
||||
} else {
|
||||
writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type)
|
||||
writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type, debugIndent)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun writeObject(obj: Any, data: Data, type: Type) {
|
||||
internal fun writeObject(obj: Any, data: Data, type: Type, debugIndent: Int = 0) {
|
||||
val serializer = serializerFactory.get(obj.javaClass, type)
|
||||
if (serializer !in serializerHistory) {
|
||||
serializerHistory.add(serializer)
|
||||
@ -103,11 +103,13 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
|
||||
|
||||
val retrievedRefCount = objectHistory[obj]
|
||||
if (retrievedRefCount == null) {
|
||||
serializer.writeObject(obj, data, type, this)
|
||||
serializer.writeObject(obj, data, type, this, debugIndent)
|
||||
// Important to do it after serialization such that dependent object will have preceding reference numbers
|
||||
// assigned to them first as they will be first read from the stream on receiving end.
|
||||
// Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content
|
||||
if (suitableForObjectReference(obj.javaClass)) objectHistory.put(obj, objectHistory.size)
|
||||
if (suitableForObjectReference(obj.javaClass)) {
|
||||
objectHistory.put(obj, objectHistory.size)
|
||||
}
|
||||
} else {
|
||||
data.writeReferencedObject(ReferencedObject(retrievedRefCount))
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ open class SerializerFactory(
|
||||
*/
|
||||
// TODO: test GenericArrayType
|
||||
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>,
|
||||
declaredType: Type) : Type? = when (declaredType) {
|
||||
declaredType: Type): Type? = when (declaredType) {
|
||||
is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType)
|
||||
// Nothing to infer, otherwise we'd have ParameterizedType
|
||||
is Class<*> -> actualClass
|
||||
@ -218,7 +218,6 @@ open class SerializerFactory(
|
||||
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
|
||||
try {
|
||||
val serialiser = processSchemaEntry(typeNotation)
|
||||
|
||||
// if we just successfully built a serializer for the type but the type fingerprint
|
||||
// doesn't match that of the serialised object then we are dealing with different
|
||||
// instance of the class, as such we need to build an EvolutionSerializer
|
||||
|
@ -22,7 +22,7 @@ class SingletonSerializer(override val type: Class<*>, val singleton: Any, facto
|
||||
output.writeTypeNotations(typeNotation)
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, debugIndent: Int) {
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
data.putBoolean(false)
|
||||
}
|
||||
|
@ -13,8 +13,6 @@ import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
class OpaqueBytesSubSequenceSerializer(factory: SerializerFactory) :
|
||||
CustomSerializer.Proxy<OpaqueBytesSubSequence, OpaqueBytes>(OpaqueBytesSubSequence::class.java, OpaqueBytes::class.java, factory) {
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
|
||||
|
||||
override fun toProxy(obj: OpaqueBytesSubSequence): OpaqueBytes = OpaqueBytes(obj.copy().bytes)
|
||||
|
||||
override fun toProxy(obj: OpaqueBytesSubSequence): OpaqueBytes = OpaqueBytes(obj.copyBytes())
|
||||
override fun fromProxy(proxy: OpaqueBytes): OpaqueBytesSubSequence = OpaqueBytesSubSequence(proxy.bytes, proxy.offset, proxy.size)
|
||||
}
|
@ -120,11 +120,17 @@ val typeStrToType: Map<Pair<String, Boolean>, Class<out Any?>> = mapOf(
|
||||
Pair("byte", false) to Byte::class.javaObjectType
|
||||
)
|
||||
|
||||
fun String.stripGenerics() : String = if(this.endsWith('>')) {
|
||||
this.substring(0, this.indexOf('<'))
|
||||
} else this
|
||||
|
||||
fun AMQPField.getTypeAsClass(classloader: ClassLoader) = typeStrToType[Pair(type, mandatory)] ?: when (type) {
|
||||
"string" -> String::class.java
|
||||
"binary" -> ByteArray::class.java
|
||||
"*" -> if (requires.isEmpty()) Any::class.java else classloader.loadClass(requires[0])
|
||||
else -> classloader.loadClass(type)
|
||||
"*" -> if (requires.isEmpty()) Any::class.java else {
|
||||
classloader.loadClass(requires[0].stripGenerics())
|
||||
}
|
||||
else -> classloader.loadClass(type.stripGenerics())
|
||||
}
|
||||
|
||||
fun AMQPField.validateType(classloader: ClassLoader) = when (type) {
|
||||
|
@ -44,7 +44,6 @@ import org.objenesis.strategy.StdInstantiatorStrategy
|
||||
import org.slf4j.Logger
|
||||
import sun.security.ec.ECPublicKeyImpl
|
||||
import sun.security.provider.certpath.X509CertPath
|
||||
import sun.security.x509.X509CertImpl
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileInputStream
|
||||
@ -200,7 +199,7 @@ object DefaultKryoCustomizer {
|
||||
private object ContractAttachmentSerializer : Serializer<ContractAttachment>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractAttachment) {
|
||||
if (kryo.serializationContext() != null) {
|
||||
output.writeBytes(obj.attachment.id.bytes)
|
||||
obj.attachment.id.writeTo(output)
|
||||
} else {
|
||||
val buffer = ByteArrayOutputStream()
|
||||
obj.attachment.open().use { it.copyTo(buffer) }
|
||||
|
@ -85,8 +85,8 @@ import kotlin.reflect.jvm.javaType
|
||||
*/
|
||||
object SerializedBytesSerializer : Serializer<SerializedBytes<Any>>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: SerializedBytes<Any>) {
|
||||
output.writeVarInt(obj.bytes.size, true)
|
||||
output.writeBytes(obj.bytes)
|
||||
output.writeVarInt(obj.size, true)
|
||||
obj.writeTo(output)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<SerializedBytes<Any>>): SerializedBytes<Any> {
|
||||
@ -190,7 +190,7 @@ object InputStreamSerializer : Serializer<InputStream>() {
|
||||
output.writeInt(numberOfBytesRead, true)
|
||||
output.writeBytes(buffer, 0, numberOfBytesRead)
|
||||
} else {
|
||||
output.writeInt(0)
|
||||
output.writeInt(0, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,25 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.io.ByteArrayOutputStream
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.ByteBufferInputStream
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import com.esotericsoftware.kryo.serializers.ClosureSerializer
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||
import net.corda.nodeapi.internal.serialization.SerializationScheme
|
||||
import java.security.PublicKey
|
||||
|
||||
// "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB
|
||||
val KryoHeaderV0_1: OpaqueBytes = OpaqueBytes("corda\u0000\u0000\u0001".toByteArray(Charsets.UTF_8))
|
||||
val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0, 1))
|
||||
|
||||
private object AutoCloseableSerialisationDetector : Serializer<AutoCloseable>() {
|
||||
override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) {
|
||||
@ -73,65 +71,41 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> withContext(kryo: Kryo, context: SerializationContext, block: (Kryo) -> T): T {
|
||||
kryo.context.ensureCapacity(context.properties.size)
|
||||
context.properties.forEach { kryo.context.put(it.key, it.value) }
|
||||
try {
|
||||
return block(kryo)
|
||||
} finally {
|
||||
kryo.context.clear()
|
||||
private fun <T : Any> SerializationContext.kryo(task: Kryo.() -> T): T {
|
||||
return getPool(this).run { kryo ->
|
||||
kryo.context.ensureCapacity(properties.size)
|
||||
properties.forEach { kryo.context.put(it.key, it.value) }
|
||||
try {
|
||||
kryo.task()
|
||||
} finally {
|
||||
kryo.context.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
val pool = getPool(context)
|
||||
val headerSize = KryoHeaderV0_1.size
|
||||
val header = byteSequence.take(headerSize)
|
||||
if (header != KryoHeaderV0_1) {
|
||||
throw KryoException("Serialized bytes header does not match expected format.")
|
||||
}
|
||||
Input(byteSequence.bytes, byteSequence.offset + headerSize, byteSequence.size - headerSize).use { input ->
|
||||
return pool.run { kryo ->
|
||||
withContext(kryo, context) {
|
||||
if (context.objectReferencesEnabled) {
|
||||
uncheckedCast(kryo.readClassAndObject(input))
|
||||
} else {
|
||||
kryo.withoutReferences { uncheckedCast<Any?, T>(kryo.readClassAndObject(input)) }
|
||||
}
|
||||
val dataBytes = kryoMagic.consume(byteSequence) ?: throw KryoException("Serialized bytes header does not match expected format.")
|
||||
return context.kryo {
|
||||
kryoInput(ByteBufferInputStream(dataBytes)) {
|
||||
if (context.objectReferencesEnabled) {
|
||||
uncheckedCast(readClassAndObject(this))
|
||||
} else {
|
||||
withoutReferences { uncheckedCast<Any?, T>(readClassAndObject(this)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
val pool = getPool(context)
|
||||
return pool.run { kryo ->
|
||||
withContext(kryo, context) {
|
||||
serializeOutputStreamPool.run { stream ->
|
||||
serializeBufferPool.run { buffer ->
|
||||
Output(buffer).use {
|
||||
it.outputStream = stream
|
||||
it.writeBytes(KryoHeaderV0_1.bytes)
|
||||
if (context.objectReferencesEnabled) {
|
||||
kryo.writeClassAndObject(it, obj)
|
||||
} else {
|
||||
kryo.withoutReferences { kryo.writeClassAndObject(it, obj) }
|
||||
}
|
||||
}
|
||||
SerializedBytes(stream.toByteArray())
|
||||
}
|
||||
return context.kryo {
|
||||
SerializedBytes(kryoOutput {
|
||||
kryoMagic.writeTo(this)
|
||||
if (context.objectReferencesEnabled) {
|
||||
writeClassAndObject(this, obj)
|
||||
} else {
|
||||
withoutReferences { writeClassAndObject(this, obj) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val serializeBufferPool = LazyPool(
|
||||
newInstance = { ByteArray(64 * 1024) }
|
||||
)
|
||||
|
||||
private val serializeOutputStreamPool = LazyPool(
|
||||
clear = ByteArrayOutputStream::reset,
|
||||
shouldReturnToPool = { it.size() < 256 * 1024 }, // Discard if it grew too large
|
||||
newInstance = { ByteArrayOutputStream(64 * 1024) }
|
||||
)
|
||||
|
@ -0,0 +1,43 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import net.corda.core.internal.LazyPool
|
||||
import java.io.*
|
||||
|
||||
private val serializationBufferPool = LazyPool(
|
||||
newInstance = { ByteArray(64 * 1024) })
|
||||
private val serializeOutputStreamPool = LazyPool(
|
||||
clear = ByteArrayOutputStream::reset,
|
||||
shouldReturnToPool = { it.size() < 256 * 1024 }, // Discard if it grew too large
|
||||
newInstance = { ByteArrayOutputStream(64 * 1024) })
|
||||
|
||||
internal fun <T> kryoInput(underlying: InputStream, task: Input.() -> T): T {
|
||||
return serializationBufferPool.run {
|
||||
Input(it).use { input ->
|
||||
input.inputStream = underlying
|
||||
input.task()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> kryoOutput(task: Output.() -> T): ByteArray {
|
||||
return serializeOutputStreamPool.run { underlying ->
|
||||
serializationBufferPool.run {
|
||||
Output(it).use { output ->
|
||||
output.outputStream = underlying
|
||||
output.task()
|
||||
}
|
||||
}
|
||||
underlying.toByteArray() // Must happen after close, to allow ZIP footer to be written for example.
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Output.substitute(transform: (OutputStream) -> OutputStream) {
|
||||
flush()
|
||||
outputStream = transform(outputStream)
|
||||
}
|
||||
|
||||
internal fun Input.substitute(transform: (InputStream) -> InputStream) {
|
||||
inputStream = transform(SequenceInputStream(ByteArrayInputStream(buffer.copyOfRange(position(), limit())), inputStream))
|
||||
}
|
@ -33,8 +33,7 @@ public final class ForbiddenLambdaSerializationTests {
|
||||
EnumSet<SerializationContext.UseCase> contexts = EnumSet.complementOf(EnumSet.of(SerializationContext.UseCase.Checkpoint));
|
||||
|
||||
contexts.forEach(ctx -> {
|
||||
SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoHeaderV0_1(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx);
|
||||
|
||||
SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx);
|
||||
String value = "Hey";
|
||||
Callable<String> target = (Callable<String> & Serializable) () -> value;
|
||||
|
||||
@ -56,8 +55,7 @@ public final class ForbiddenLambdaSerializationTests {
|
||||
EnumSet<SerializationContext.UseCase> contexts = EnumSet.complementOf(EnumSet.of(SerializationContext.UseCase.Checkpoint));
|
||||
|
||||
contexts.forEach(ctx -> {
|
||||
SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoHeaderV0_1(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx);
|
||||
|
||||
SerializationContext context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx);
|
||||
String value = "Hey";
|
||||
Callable<String> target = () -> value;
|
||||
|
||||
|
@ -26,7 +26,7 @@ public final class LambdaCheckpointSerializationTest {
|
||||
@Before
|
||||
public void setup() {
|
||||
factory = testSerialization.getSerializationFactory();
|
||||
context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoHeaderV0_1(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, SerializationContext.UseCase.Checkpoint);
|
||||
context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, SerializationContext.UseCase.Checkpoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -23,6 +23,115 @@ public class JavaPrivatePropertyTests {
|
||||
public String getA() { return a; }
|
||||
}
|
||||
|
||||
static class B {
|
||||
private Boolean b;
|
||||
|
||||
B(Boolean b) { this.b = b; }
|
||||
|
||||
public Boolean isB() {
|
||||
return this.b;
|
||||
}
|
||||
}
|
||||
|
||||
static class B2 {
|
||||
private Boolean b;
|
||||
|
||||
public Boolean isB() {
|
||||
return this.b;
|
||||
}
|
||||
|
||||
public void setB(Boolean b) {
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
static class B3 {
|
||||
private Boolean b;
|
||||
|
||||
// break the BEAN format explicitly (i.e. it's not isB)
|
||||
public Boolean isb() {
|
||||
return this.b;
|
||||
}
|
||||
|
||||
public void setB(Boolean b) {
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
static class C3 {
|
||||
private Integer a;
|
||||
|
||||
public Integer getA() {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
public Boolean isA() {
|
||||
return this.a > 0;
|
||||
}
|
||||
|
||||
public void setA(Integer a) {
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateBooleanWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerializerGetter);
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
B b = new B(true);
|
||||
B b2 = des.deserialize(ser.serialize(b), B.class);
|
||||
assertEquals (b.b, b2.b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateBooleanWithNoConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerializerGetter);
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
B2 b = new B2();
|
||||
b.setB(false);
|
||||
B2 b2 = des.deserialize(ser.serialize(b), B2.class);
|
||||
assertEquals (b.b, b2.b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCapitilsationOfIs() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerializerGetter);
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
B3 b = new B3();
|
||||
b.setB(false);
|
||||
B3 b2 = des.deserialize(ser.serialize(b), B3.class);
|
||||
|
||||
// since we can't find a getter for b (isb != isB) then we won't serialize that parameter
|
||||
assertEquals (null, b2.b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateIntWithBoolean() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerializerGetter);
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
C3 c = new C3();
|
||||
c.setA(12345);
|
||||
C3 c2 = des.deserialize(ser.serialize(c), C3.class);
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||
|
@ -286,12 +286,8 @@ public class SetterConstructorTests {
|
||||
tm.setA(10);
|
||||
assertEquals("10", tm.getA());
|
||||
|
||||
TypeMismatch post = new DeserializationInput(factory1).deserialize(new SerializationOutput(factory1).serialize(tm),
|
||||
TypeMismatch.class);
|
||||
|
||||
// because there is a type mismatch in the class, it won't return that info as a BEAN property and thus
|
||||
// we won't serialise it and thus on deserialization it won't be initialized
|
||||
Assertions.assertThatThrownBy(() -> post.getA()).isInstanceOf(NullPointerException.class);
|
||||
Assertions.assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm)).isInstanceOf (
|
||||
NotSerializableException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -306,11 +302,7 @@ public class SetterConstructorTests {
|
||||
tm.setA("10");
|
||||
assertEquals((Integer)10, tm.getA());
|
||||
|
||||
TypeMismatch2 post = new DeserializationInput(factory1).deserialize(new SerializationOutput(factory1).serialize(tm),
|
||||
TypeMismatch2.class);
|
||||
|
||||
// because there is a type mismatch in the class, it won't return that info as a BEAN property and thus
|
||||
// we won't serialise it and thus on deserialization it won't be initialized
|
||||
assertEquals(null, post.getA());
|
||||
Assertions.assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm)).isInstanceOf(
|
||||
NotSerializableException.class);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import net.corda.nodeapi.internal.createDevKeyStores
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.TestIdentity
|
||||
@ -314,7 +314,7 @@ class X509UtilitiesTest {
|
||||
@Test
|
||||
fun `serialize - deserialize X509Certififcate`() {
|
||||
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
|
||||
val context = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val context = SerializationContextImpl(kryoMagic,
|
||||
javaClass.classLoader,
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
@ -329,7 +329,7 @@ class X509UtilitiesTest {
|
||||
@Test
|
||||
fun `serialize - deserialize X509CertPath`() {
|
||||
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
|
||||
val context = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val context = SerializationContextImpl(kryoMagic,
|
||||
javaClass.classLoader,
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
|
@ -7,6 +7,7 @@ import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
@ -35,7 +36,7 @@ class ContractAttachmentSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `write contract attachment and read it back`() {
|
||||
val contractAttachment = ContractAttachment(GeneratedAttachment(ByteArray(0)), DummyContract.PROGRAM_ID)
|
||||
val contractAttachment = ContractAttachment(GeneratedAttachment(EMPTY_BYTE_ARRAY), DummyContract.PROGRAM_ID)
|
||||
// no token context so will serialize the whole attachment
|
||||
val serialized = contractAttachment.serialize(factory, context)
|
||||
val deserialized = serialized.deserialize(factory, context)
|
||||
@ -88,8 +89,7 @@ class ContractAttachmentSerializerTest {
|
||||
|
||||
@Test
|
||||
fun `check attachment in deserialize is lazy loaded when using token context`() {
|
||||
val attachment = GeneratedAttachment(ByteArray(0))
|
||||
|
||||
val attachment = GeneratedAttachment(EMPTY_BYTE_ARRAY)
|
||||
// don't importAttachment in mockService
|
||||
|
||||
val contractAttachment = ContractAttachment(attachment, DummyContract.PROGRAM_ID)
|
||||
|
@ -11,7 +11,7 @@ import net.corda.core.serialization.*
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoaderTests
|
||||
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import org.junit.Rule
|
||||
@ -108,9 +108,8 @@ class CordaClassResolverTests {
|
||||
val emptyMapClass = mapOf<Any, Any>().javaClass
|
||||
}
|
||||
|
||||
private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P)
|
||||
private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P)
|
||||
|
||||
private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P)
|
||||
private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(kryoMagic, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P)
|
||||
@Test
|
||||
fun `Annotation on enum works for specialised entries`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
|
||||
|
@ -13,14 +13,13 @@ import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.sequence
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
@ -28,6 +27,7 @@ import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -36,16 +36,13 @@ class KryoTests {
|
||||
private val ALICE_PUBKEY = TestIdentity(ALICE_NAME, 70).publicKey
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private lateinit var factory: SerializationFactory
|
||||
private lateinit var context: SerializationContext
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
|
||||
context = SerializationContextImpl(KryoHeaderV0_1,
|
||||
context = SerializationContextImpl(kryoMagic,
|
||||
javaClass.classLoader,
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
@ -150,6 +147,14 @@ class KryoTests {
|
||||
assertEquals(-1, readRubbishStream.read())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `InputStream serialisation does not write trailing garbage`() {
|
||||
val byteArrays = listOf("123", "456").map { it.toByteArray() }
|
||||
val streams = byteArrays.map { it.inputStream() }.serialize(factory, context).deserialize(factory, context).iterator()
|
||||
byteArrays.forEach { assertArrayEquals(it, streams.next().readBytes()) }
|
||||
assertFalse(streams.hasNext())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize SignableData`() {
|
||||
val testString = "Hello World"
|
||||
@ -249,7 +254,7 @@ class KryoTests {
|
||||
}
|
||||
Tmp()
|
||||
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
|
||||
val context = SerializationContextImpl(KryoHeaderV0_1,
|
||||
val context = SerializationContextImpl(kryoMagic,
|
||||
javaClass.classLoader,
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
|
@ -7,7 +7,7 @@ import net.corda.node.services.statemachine.DataSessionMessage
|
||||
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.Envelope
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.internal.amqpSpecific
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
@ -68,7 +68,7 @@ class ListsSerializationTest {
|
||||
val nameID = 0
|
||||
val serializedForm = emptyList<Int>().serialize()
|
||||
val output = ByteArrayOutputStream().apply {
|
||||
write(KryoHeaderV0_1.bytes)
|
||||
kryoMagic.writeTo(this)
|
||||
write(DefaultClassResolver.NAME + 2)
|
||||
write(nameID)
|
||||
write(javaEmptyListClass.name.toAscii())
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.statemachine.DataSessionMessage
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.amqpSpecific
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
@ -78,7 +78,7 @@ class MapsSerializationTest {
|
||||
val nameID = 0
|
||||
val serializedForm = emptyMap<Int, Int>().serialize()
|
||||
val output = ByteArrayOutputStream().apply {
|
||||
write(KryoHeaderV0_1.bytes)
|
||||
kryoMagic.writeTo(this)
|
||||
write(DefaultClassResolver.NAME + 2)
|
||||
write(nameID)
|
||||
write(javaEmptyMapClass.name.toAscii())
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.internal.serialization.kryo.CordaKryo
|
||||
import net.corda.nodeapi.internal.serialization.kryo.DefaultKryoCustomizer
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -98,7 +98,7 @@ class SerializationTokenTest {
|
||||
val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(this.context)))
|
||||
val stream = ByteArrayOutputStream()
|
||||
Output(stream).use {
|
||||
it.write(KryoHeaderV0_1.bytes)
|
||||
kryoMagic.writeTo(it)
|
||||
kryo.writeClass(it, SingletonSerializeAsToken::class.java)
|
||||
kryo.writeObject(it, emptyList<Any>())
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.statemachine.DataSessionMessage
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.kryoSpecific
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
@ -55,7 +55,7 @@ class SetsSerializationTest {
|
||||
val nameID = 0
|
||||
val serializedForm = emptySet<Int>().serialize()
|
||||
val output = ByteArrayOutputStream().apply {
|
||||
write(KryoHeaderV0_1.bytes)
|
||||
kryoMagic.writeTo(this)
|
||||
write(DefaultClassResolver.NAME + 2)
|
||||
write(nameID)
|
||||
write(javaEmptySetClass.name.toAscii())
|
||||
|
@ -1,11 +1,21 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.crypto.Crypto.generateKeyPair
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import java.net.URI
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// To regenerate any of the binary test files do the following
|
||||
@ -462,4 +472,113 @@ class EvolvabilityTests {
|
||||
assertEquals(f, db3.f)
|
||||
assertEquals(-1, db3.g)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// This test uses a NetworkParameters signed set of bytes generated by R3 Corda and
|
||||
// is here to ensure we can still read them. This test exists because of the break in
|
||||
// being able to deserialize an object serialized prior to some fixes to the fingerprinter.
|
||||
//
|
||||
// The file itself was generated from R3 Corda at commit
|
||||
// 6a6b6f256 Skip cache invalidation during init() - caches are still null.
|
||||
//
|
||||
// To regenerate the file un-ignore the test below this one (regenerate broken network parameters),
|
||||
// to regenerate at a specific version add that test to a checkout at the desired sha then take
|
||||
// the resulting file and add to the repo, changing the filename as appropriate
|
||||
//
|
||||
@Test
|
||||
@Ignore("Test fails after moving NetworkParameters and NotaryInfo into core from node-api")
|
||||
fun readBrokenNetworkParameters(){
|
||||
val sf = testDefaultFactory()
|
||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
|
||||
//
|
||||
// filename breakdown
|
||||
// networkParams - because this is a serialised set of network parameters
|
||||
// r3corda - generated by R3 Corda instead of Corda
|
||||
// 6a6b6f256 - Commit sha of the build that generated the file we're testing against
|
||||
//
|
||||
val resource = "networkParams.r3corda.6a6b6f256"
|
||||
|
||||
val path = EvolvabilityTests::class.java.getResource(resource)
|
||||
val f = File(path.toURI())
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<SignedData<NetworkParameters>>(sc2))
|
||||
val networkParams = DeserializationInput(sf).deserialize(deserializedC.raw)
|
||||
|
||||
assertEquals(1000, networkParams.maxMessageSize)
|
||||
assertEquals(1000, networkParams.maxTransactionSize)
|
||||
assertEquals(3, networkParams.minimumPlatformVersion)
|
||||
assertEquals(1, networkParams.notaries.size)
|
||||
assertEquals(TestIdentity(DUMMY_NOTARY_NAME, 20).party, networkParams.notaries.firstOrNull()?.identity)
|
||||
}
|
||||
|
||||
//
|
||||
// This test created a serialized and signed set of Network Parameters to test whether we
|
||||
// can still deserialize them
|
||||
//
|
||||
@Test
|
||||
@Ignore("This test simply regenerates the test file used for readBrokenNetworkParameters")
|
||||
fun `regenerate broken network parameters`() {
|
||||
// note: 6a6b6f256 is the sha that generates the file
|
||||
val resource = "networkParams.<corda version>.<commit sha>"
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val networkParameters = NetworkParameters(
|
||||
3, listOf(NotaryInfo(DUMMY_NOTARY, false)),1000, 1000, Instant.EPOCH, 1 )
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(sf))
|
||||
sf.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
|
||||
val testOutput = TestSerializationOutput(true, sf)
|
||||
val serialized = testOutput.serialize(networkParameters)
|
||||
val keyPair = generateKeyPair()
|
||||
val sig = keyPair.private.sign(serialized.bytes, keyPair.public)
|
||||
val signed = SignedData(serialized, sig)
|
||||
val signedAndSerialized = testOutput.serialize(signed)
|
||||
|
||||
File(URI("$localPath/$resource")).writeBytes( signedAndSerialized.bytes)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Test
|
||||
fun getterSetterEvolver1() {
|
||||
val resource = "EvolvabilityTests.getterSetterEvolver1"
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
//
|
||||
// Class as it was serialised
|
||||
//
|
||||
// data class C(var c: Int, var d: Int, var b: Int, var e: Int, var a: Int) {
|
||||
// // This will force the serialization engine to use getter / setter
|
||||
// // instantiation for the object rather than construction
|
||||
// @ConstructorForDeserialization
|
||||
// @Suppress("UNUSED")
|
||||
// constructor() : this(0, 0, 0, 0, 0)
|
||||
// }
|
||||
//
|
||||
// File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(C(3,4,2,5,1)).bytes)
|
||||
|
||||
//
|
||||
// Class as it exists now, c has been removed
|
||||
//
|
||||
data class C(var d: Int, var b: Int, var e: Int, var a: Int) {
|
||||
// This will force the serialization engine to use getter / setter
|
||||
// instantiation for the object rather than construction
|
||||
@ConstructorForDeserialization
|
||||
@Suppress("UNUSED")
|
||||
constructor() : this(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
val path = EvolvabilityTests::class.java.getResource(resource)
|
||||
val f = File(path.toURI())
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
|
||||
assertEquals(1, deserializedC.a)
|
||||
assertEquals(2, deserializedC.b)
|
||||
assertEquals(4, deserializedC.d)
|
||||
assertEquals(5, deserializedC.e)
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,39 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||
import org.junit.Test
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import org.hibernate.Transaction
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
data class TestContractState(
|
||||
override val participants: List<AbstractParty>
|
||||
) : ContractState
|
||||
|
||||
class TestAttachmentConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment) = true
|
||||
}
|
||||
|
||||
class GenericsTests {
|
||||
companion object {
|
||||
val VERBOSE = false
|
||||
val VERBOSE = true
|
||||
|
||||
@Suppress("UNUSED")
|
||||
var localPath = projectRootDir.toUri().resolve(
|
||||
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
|
||||
|
||||
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
}
|
||||
|
||||
private fun printSeparator() = if (VERBOSE) println("\n\n-------------------------------------------\n\n") else Unit
|
||||
@ -252,29 +270,260 @@ class GenericsTests {
|
||||
File(GenericsTests::class.java.getResource(resource).toURI()).readBytes())).t)
|
||||
}
|
||||
|
||||
interface DifferentBounds {
|
||||
fun go()
|
||||
data class StateAndString(val state: TransactionState<*>, val ref: String)
|
||||
data class GenericStateAndString<out T: ContractState>(val state: TransactionState<T>, val ref: String)
|
||||
|
||||
//
|
||||
// If this doesn't blow up all is fine
|
||||
private fun fingerprintingDiffersStrip(state: Any) {
|
||||
class cl : ClassLoader()
|
||||
|
||||
val m = ClassLoader::class.java.getDeclaredMethod("findLoadedClass", *arrayOf<Class<*>>(String::class.java))
|
||||
m.isAccessible = true
|
||||
|
||||
val factory1 = testDefaultFactory()
|
||||
factory1.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state)
|
||||
|
||||
// attempt at having a class loader without some of the derived non core types loaded and thus
|
||||
// possibly altering how we serialise things
|
||||
val altClassLoader = cl()
|
||||
|
||||
val factory2 = SerializerFactory(AllWhitelist, altClassLoader)
|
||||
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
val ser2 = TestSerializationOutput(VERBOSE, factory2).serializeAndReturnSchema(state)
|
||||
|
||||
// now deserialise those objects
|
||||
val factory3 = testDefaultFactory()
|
||||
factory3.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
val des1 = DeserializationInput(factory3).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
val factory4 = SerializerFactory(AllWhitelist, cl())
|
||||
factory4.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
val des2 = DeserializationInput(factory4).deserializeAndReturnEnvelope(ser2.obj)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun differentBounds() {
|
||||
data class A (val a: Int): DifferentBounds {
|
||||
override fun go() {
|
||||
println(a)
|
||||
}
|
||||
}
|
||||
fun fingerprintingDiffers() {
|
||||
val state = TransactionState<TestContractState> (
|
||||
TestContractState(listOf(miniCorp.party)),
|
||||
"wibble", miniCorp.party,
|
||||
encumbrance = null,
|
||||
constraint = TestAttachmentConstraint())
|
||||
|
||||
data class G<out T : DifferentBounds>(val b: T)
|
||||
val sas = StateAndString(state, "wibble")
|
||||
|
||||
val factorys = listOf(
|
||||
SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()))
|
||||
fingerprintingDiffersStrip(sas)
|
||||
}
|
||||
|
||||
val ser = SerializationOutput(factorys[0])
|
||||
@Test
|
||||
fun fingerprintingDiffersList() {
|
||||
val state = TransactionState<TestContractState> (
|
||||
TestContractState(listOf(miniCorp.party)),
|
||||
"wibble", miniCorp.party,
|
||||
encumbrance = null,
|
||||
constraint = TestAttachmentConstraint())
|
||||
|
||||
ser.serialize(G(A(10))).apply {
|
||||
factorys.forEach {
|
||||
}
|
||||
}
|
||||
val sas = StateAndString(state, "wibble")
|
||||
|
||||
fingerprintingDiffersStrip(Collections.singletonList(sas))
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Force object to be serialised as Example<T> and deserialized as Example<?>
|
||||
//
|
||||
@Test
|
||||
fun fingerprintingDiffersListLoaded() {
|
||||
//
|
||||
// using this wrapper class we force the object to be serialised as
|
||||
// net.corda.core.contracts.TransactionState<T>
|
||||
//
|
||||
data class TransactionStateWrapper<out T : ContractState> (val o: List<GenericStateAndString<T>>)
|
||||
|
||||
val state = TransactionState<TestContractState> (
|
||||
TestContractState(listOf(miniCorp.party)),
|
||||
"wibble", miniCorp.party,
|
||||
encumbrance = null,
|
||||
constraint = TestAttachmentConstraint())
|
||||
|
||||
val sas = GenericStateAndString(state, "wibble")
|
||||
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
|
||||
factory1.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
|
||||
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(
|
||||
TransactionStateWrapper(Collections.singletonList(sas)))
|
||||
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(sas.ref, des1.obj.o.firstOrNull()?.ref ?: "WILL NOT MATCH")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedGenericsWithBound() {
|
||||
open class BaseState(val a : Int)
|
||||
class DState(a: Int) : BaseState(a)
|
||||
data class LTransactionState<out T : BaseState> constructor(val data: T)
|
||||
data class StateWrapper<out T : BaseState>(val state: LTransactionState<T>)
|
||||
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
|
||||
val state = LTransactionState(DState(1020304))
|
||||
val stateAndString = StateWrapper(state)
|
||||
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString)
|
||||
|
||||
//val factory2 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(state.data.a, des1.obj.state.data.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedMultiGenericsWithBound() {
|
||||
open class BaseState(val a : Int)
|
||||
class DState(a: Int) : BaseState(a)
|
||||
class EState(a: Int, val msg: String) : BaseState(a)
|
||||
|
||||
data class LTransactionState<out T1 : BaseState, out T2: BaseState> (val data: T1, val context: T2)
|
||||
data class StateWrapper<out T1 : BaseState, out T2: BaseState>(val state: LTransactionState<T1, T2>)
|
||||
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
|
||||
val state = LTransactionState(DState(1020304), EState(5060708, msg = "thigns"))
|
||||
val stateAndString = StateWrapper(state)
|
||||
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString)
|
||||
|
||||
//val factory2 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(state.data.a, des1.obj.state.data.a)
|
||||
assertEquals(state.context.a, des1.obj.state.context.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedMultiGenericsNoBound() {
|
||||
open class BaseState(val a : Int)
|
||||
class DState(a: Int) : BaseState(a)
|
||||
class EState(a: Int, val msg: String) : BaseState(a)
|
||||
|
||||
data class LTransactionState<out T1, out T2> (val data: T1, val context: T2)
|
||||
data class StateWrapper<out T1, out T2>(val state: LTransactionState<T1, T2>)
|
||||
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
|
||||
val state = LTransactionState(DState(1020304), EState(5060708, msg = "things"))
|
||||
val stateAndString = StateWrapper(state)
|
||||
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString)
|
||||
|
||||
//val factory2 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(state.data.a, des1.obj.state.data.a)
|
||||
assertEquals(state.context.a, des1.obj.state.context.a)
|
||||
assertEquals(state.context.msg, des1.obj.state.context.msg)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun baseClassInheritedButNotOverriden() {
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
|
||||
open class BaseState<T1, T2>(open val a : T1, open val b: T2)
|
||||
class DState<T1, T2>(a: T1, b: T2) : BaseState<T1, T2>(a, b)
|
||||
|
||||
val state = DState(100, "hello")
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state)
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(state.a, des1.obj.a)
|
||||
assertEquals(state.b, des1.obj.b)
|
||||
|
||||
class DState2<T1, T2, T3>(a: T1, b: T2, val c: T3) : BaseState<T1, T2>(a, b)
|
||||
|
||||
val state2 = DState2(100, "hello", 100L)
|
||||
val ser2 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state2)
|
||||
val des2 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser2.obj)
|
||||
|
||||
assertEquals(state2.a, des2.obj.a)
|
||||
assertEquals(state2.b, des2.obj.b)
|
||||
assertEquals(state2.c, des2.obj.c)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun baseClassInheritedButNotOverridenBounded() {
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
|
||||
open class Bound(val a: Int)
|
||||
|
||||
open class BaseState<out T1 : Bound>(open val a: T1)
|
||||
class DState<out T1: Bound>(a: T1) : BaseState<T1>(a)
|
||||
|
||||
val state = DState(Bound(100))
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(state)
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(state.a.a, des1.obj.a.a)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedMultiGenericsAtBottomWithBound() {
|
||||
open class BaseState<T1, T2>(val a : T1, val b: T2)
|
||||
class DState<T1, T2>(a: T1, b: T2) : BaseState<T1, T2>(a, b)
|
||||
class EState<T1, T2>(a: T1, b: T2, val c: Long) : BaseState<T1, T2>(a, b)
|
||||
|
||||
data class LTransactionState<T1, T2, T3 : BaseState<T1, T2>, out T4: BaseState<T1, T2>> (val data: T3, val context: T4)
|
||||
data class StateWrapper<T1, T2, T3 : BaseState<T1, T2>, out T4: BaseState<T1, T2>>(val state: LTransactionState<T1, T2, T3, T4>)
|
||||
|
||||
val factory1 = testDefaultFactoryNoEvolution()
|
||||
|
||||
val state = LTransactionState(DState(1020304, "Hello"), EState(5060708, "thins", 100L))
|
||||
val stateAndString = StateWrapper(state)
|
||||
|
||||
val ser1 = TestSerializationOutput(VERBOSE, factory1).serializeAndReturnSchema(stateAndString)
|
||||
|
||||
//val factory2 = testDefaultFactoryNoEvolution()
|
||||
val factory2 = testDefaultFactory()
|
||||
val des1 = DeserializationInput(factory2).deserializeAndReturnEnvelope(ser1.obj)
|
||||
|
||||
assertEquals(state.data.a, des1.obj.state.data.a)
|
||||
assertEquals(state.context.a, des1.obj.state.context.a)
|
||||
}
|
||||
|
||||
fun implemntsGeneric() {
|
||||
open class B<out T>(open val a: T)
|
||||
class D(override val a: String) : B<String>(a)
|
||||
|
||||
val factory = testDefaultFactoryNoEvolution()
|
||||
|
||||
val bytes = SerializationOutput(factory).serialize(D("Test"))
|
||||
|
||||
DeserializationInput(factory).deserialize(bytes).apply { assertEquals("Test", this.a) }
|
||||
}
|
||||
|
||||
interface implementsGenericInterfaceI<out T> {
|
||||
val a: T
|
||||
}
|
||||
|
||||
@Test
|
||||
fun implemntsGenericInterface() {
|
||||
class D(override val a: String) : implementsGenericInterfaceI<String>
|
||||
|
||||
val factory = testDefaultFactoryNoEvolution()
|
||||
|
||||
val bytes = SerializationOutput(factory).serialize(D("Test"))
|
||||
|
||||
DeserializationInput(factory).deserialize(bytes).apply { assertEquals("Test", this.a) }
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import org.assertj.core.api.Assertions
|
||||
@ -30,8 +30,7 @@ class OverridePKSerializerTest {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean = true
|
||||
|
||||
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) = true
|
||||
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.junit.Test
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.assertj.core.api.Assertions
|
||||
import java.io.NotSerializableException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PrivatePropertyTests {
|
||||
@ -29,6 +31,15 @@ class PrivatePropertyTests {
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePrivatePropertyBoolean() {
|
||||
data class C(private val b: Boolean)
|
||||
|
||||
C(false).apply {
|
||||
assertEquals(this, DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(this)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePrivatePropertyNullableNotNull() {
|
||||
data class C(private val b: String?)
|
||||
@ -56,6 +67,60 @@ class PrivatePropertyTests {
|
||||
assertEquals(c1, c2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithInheritance() {
|
||||
open class B(val a: String, protected val b: String)
|
||||
class D (a: String, b: String) : B (a, b) {
|
||||
override fun equals(other: Any?): Boolean = when (other) {
|
||||
is D -> other.a == a && other.b == b
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val d1 = D("clump", "lump")
|
||||
val d2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(d1))
|
||||
|
||||
assertEquals(d1, d2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultiArgSetter() {
|
||||
@Suppress("UNUSED")
|
||||
data class C(private var a: Int, var b: Int) {
|
||||
// This will force the serialization engine to use getter / setter
|
||||
// instantiation for the object rather than construction
|
||||
@ConstructorForDeserialization
|
||||
constructor() : this(0, 0)
|
||||
|
||||
fun setA(a: Int, b: Int) { this.a = a }
|
||||
fun getA() = a
|
||||
}
|
||||
|
||||
val c1 = C(33, 44)
|
||||
val c2 = DeserializationInput(factory).deserialize(SerializationOutput(factory).serialize(c1))
|
||||
assertEquals(0, c2.getA())
|
||||
assertEquals(44, c2.b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBadTypeArgSetter() {
|
||||
@Suppress("UNUSED")
|
||||
data class C(private var a: Int, val b: Int) {
|
||||
@ConstructorForDeserialization
|
||||
constructor() : this(0, 0)
|
||||
|
||||
fun setA(a: String) { this.a = a.toInt() }
|
||||
fun getA() = a
|
||||
}
|
||||
|
||||
val c1 = C(33, 44)
|
||||
Assertions.assertThatThrownBy {
|
||||
SerializationOutput(factory).serialize(c1)
|
||||
}.isInstanceOf(NotSerializableException::class.java).hasMessageContaining(
|
||||
"Defined setter for parameter a takes parameter of type class java.lang.String " +
|
||||
"yet underlying type is int ")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithOnePublicOnePrivateProperty2() {
|
||||
data class C(val a: Int, private val b: Int)
|
||||
@ -127,7 +192,6 @@ class PrivatePropertyTests {
|
||||
// Inner and Outer
|
||||
assertEquals(2, serializersByDescriptor.size)
|
||||
|
||||
val schemaDescriptor = output.schema.types.first().descriptor.name
|
||||
val c2 = DeserializationInput(factory).deserialize(output.obj)
|
||||
|
||||
assertEquals(c1, c2)
|
||||
|
@ -20,6 +20,8 @@ import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.GeneratedAttachment
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive
|
||||
import net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
@ -36,11 +38,13 @@ import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import java.math.BigDecimal
|
||||
import java.nio.ByteBuffer
|
||||
import java.time.*
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.reflect.full.superclasses
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
@ -1080,4 +1084,38 @@ class SerializationOutputTests {
|
||||
serdes(obj, factory, factory2, expectedEqual = false, expectDeserializedEqual = false)
|
||||
}.isInstanceOf(MissingAttachmentsException::class.java)
|
||||
}
|
||||
|
||||
//
|
||||
// Example stacktrace that this test is tryint to reproduce
|
||||
//
|
||||
// java.lang.IllegalArgumentException:
|
||||
// net.corda.core.contracts.TransactionState ->
|
||||
// data(net.corda.core.contracts.ContractState) ->
|
||||
// net.corda.finance.contracts.asset.Cash$State ->
|
||||
// amount(net.corda.core.contracts.Amount<net.corda.core.contracts.Issued<java.util.Currency>>) ->
|
||||
// net.corda.core.contracts.Amount<net.corda.core.contracts.Issued<java.util.Currency>> ->
|
||||
// displayTokenSize(java.math.BigDecimal) ->
|
||||
// wrong number of arguments
|
||||
//
|
||||
// So the actual problem was objects with multiple getters. The code wasn't looking for one with zero
|
||||
// properties, just taking the first one it found with with the most applicable type, and the reflection
|
||||
// ordering of the methods was random, thus occasionally we select the wrong one
|
||||
//
|
||||
@Test
|
||||
fun reproduceWrongNumberOfArguments() {
|
||||
val field = SerializerFactory::class.java.getDeclaredField("serializersByType").apply {
|
||||
this.isAccessible = true
|
||||
}
|
||||
|
||||
data class C(val a: Amount<Currency>)
|
||||
|
||||
val factory = testDefaultFactoryNoEvolution()
|
||||
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
|
||||
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer)
|
||||
|
||||
val c = C(Amount<Currency>(100, BigDecimal("1.5"), Currency.getInstance("USD")))
|
||||
|
||||
// were the issue not fixed we'd blow up here
|
||||
SerializationOutput(factory).serialize(c)
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class KryoStreamsTest {
|
||||
class NegOutputStream(private val stream: OutputStream) : OutputStream() {
|
||||
override fun write(b: Int) = stream.write(-b)
|
||||
}
|
||||
|
||||
class NegInputStream(private val stream: InputStream) : InputStream() {
|
||||
override fun read() = stream.read().let {
|
||||
if (it != -1) 0xff and -it else -1
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `substitute output works`() {
|
||||
assertArrayEquals(byteArrayOf(100, -101), kryoOutput {
|
||||
write(100)
|
||||
substitute(::NegOutputStream)
|
||||
write(101)
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `substitute input works`() {
|
||||
kryoInput(byteArrayOf(100, 101).inputStream()) {
|
||||
assertEquals(100, read())
|
||||
substitute(::NegInputStream)
|
||||
assertEquals(-101, read().toByte())
|
||||
assertEquals(-1, read())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zip round-trip`() {
|
||||
val data = ByteArray(12345).also { Random(0).nextBytes(it) }
|
||||
val encoded = kryoOutput {
|
||||
write(data)
|
||||
substitute(::DeflaterOutputStream)
|
||||
write(data)
|
||||
substitute(::DeflaterOutputStream) // Potentially useful if a different codec.
|
||||
write(data)
|
||||
}
|
||||
kryoInput(encoded.inputStream()) {
|
||||
assertArrayEquals(data, readBytes(data.size))
|
||||
substitute(::InflaterInputStream)
|
||||
assertArrayEquals(data, readBytes(data.size))
|
||||
substitute(::InflaterInputStream)
|
||||
assertArrayEquals(data, readBytes(data.size))
|
||||
assertEquals(-1, read())
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user