Merge commit '86fb1ed852c69121f989c9eeea92cfb4c27f9d13' into aslemmer-merge-19-Feb

This commit is contained in:
Andras Slemmer
2018-02-19 16:14:43 +00:00
234 changed files with 5046 additions and 1609 deletions

View File

@ -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)
}

View File

@ -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.*

View File

@ -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()

View File

@ -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()
}
}

View File

@ -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.

View 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(),

View File

@ -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.

View File

@ -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
}
}

View File

@ -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

View File

@ -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(),

View 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 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(),

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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.

View File

@ -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) }
}
}
}

View File

@ -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]) })
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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) {

View File

@ -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)
}
}
}
}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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())
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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) }

View File

@ -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
}
}

View File

@ -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) }
)

View File

@ -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))
}

View File

@ -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;

View File

@ -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

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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(),

View File

@ -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)

View File

@ -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)

View File

@ -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(),

View File

@ -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())

View File

@ -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())

View File

@ -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>())
}

View File

@ -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())

View File

@ -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)
}
}

View File

@ -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) }
}
}

View File

@ -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.
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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())
}
}
}