Merge remote-tracking branch 'open/master' into aslemmer-enterprise-merge-september-8

This commit is contained in:
Andras Slemmer
2017-09-08 10:13:29 +01:00
448 changed files with 6895 additions and 3318 deletions

View File

@ -1,6 +1,6 @@
package net.corda.nodeapi
import net.corda.core.crypto.toBase58String
import net.corda.core.utilities.toBase58String
import net.corda.core.messaging.MessageRecipientGroup
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.SingleMessageRecipient

View File

@ -4,14 +4,6 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.Try
import net.corda.nodeapi.RPCApi.ClientToServer
import net.corda.nodeapi.RPCApi.ObservableId
import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVALS
import net.corda.nodeapi.RPCApi.RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION
import net.corda.nodeapi.RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX
import net.corda.nodeapi.RPCApi.RPC_SERVER_QUEUE_NAME
import net.corda.nodeapi.RPCApi.RpcRequestId
import net.corda.nodeapi.RPCApi.ServerToClient
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.*
import org.apache.activemq.artemis.api.core.management.CoreNotificationType
@ -20,41 +12,43 @@ import org.apache.activemq.artemis.reader.MessageUtil
import rx.Notification
import java.util.*
// The RPC protocol:
//
// The server consumes the queue "RPC_SERVER_QUEUE_NAME" and receives RPC requests (ClientToServer.RpcRequest) on it.
// When a client starts up it should create a queue for its inbound messages, this should be of the form
// "RPC_CLIENT_QUEUE_NAME_PREFIX.$username.$nonce". Each RPC request contains this address (in
// ClientToServer.RpcRequest.clientAddress), this is where the server will send the reply to the request as well as
// subsequent Observations rooted in the RPC. The requests/replies are muxed using a unique RpcRequestId generated by
// the client for each request.
//
// If an RPC reply's payload (ServerToClient.RpcReply.result) contains observables then the server will generate a
// unique ObservableId for each and serialise them in place of the observables themselves. Subsequently the client
// should be prepared to receive observations (ServerToClient.Observation), muxed by the relevant ObservableId.
// In addition each observation itself may contain further observables, this case should behave the same as before.
//
// Additionally the client may send ClientToServer.ObservablesClosed messages indicating that certain observables
// aren't consumed anymore, which should subsequently stop the stream from the server. Note that some observations may
// already be in flight when this is sent, the client should handle this gracefully.
//
// An example session:
// Client Server
// ----------RpcRequest(RID0)-----------> // Client makes RPC request with ID "RID0"
// <----RpcReply(RID0, Payload(OID0))---- // Server sends reply containing an observable with ID "OID0"
// <---------Observation(OID0)----------- // Server sends observation onto "OID0"
// <---Observation(OID0, Payload(OID1))-- // Server sends another observation, this time containing another observable
// <---------Observation(OID1)----------- // Observation onto new "OID1"
// <---------Observation(OID0)-----------
// -----ObservablesClosed(OID0, OID1)---> // Client indicates it stopped consuming the observables.
// <---------Observation(OID1)----------- // Observation was already in-flight before the previous message was processed
// (FIN)
//
// Note that multiple sessions like the above may interleave in an arbitrary fashion.
//
// Additionally the server may listen on client binding removals for cleanup using RPC_CLIENT_BINDING_REMOVALS. This
// requires the server to create a filter on the Artemis notification address using RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION
/**
* The RPC protocol:
*
* The server consumes the queue "[RPC_SERVER_QUEUE_NAME]" and receives RPC requests ([ClientToServer.RpcRequest]) on it.
* When a client starts up it should create a queue for its inbound messages, this should be of the form
* "[RPC_CLIENT_QUEUE_NAME_PREFIX].$username.$nonce". Each RPC request contains this address (in
* [ClientToServer.RpcRequest.clientAddress]), this is where the server will send the reply to the request as well as
* subsequent Observations rooted in the RPC. The requests/replies are muxed using a unique [RpcRequestId] generated by
* the client for each request.
*
* If an RPC reply's payload ([ServerToClient.RpcReply.result]) contains [Observable]s then the server will generate a
* unique [ObservableId] for each and serialise them in place of the [Observable]s themselves. Subsequently the client
* should be prepared to receive observations ([ServerToClient.Observation]), muxed by the relevant [ObservableId].
* In addition each observation itself may contain further [Observable]s, this case should behave the same as before.
*
* Additionally the client may send [ClientToServer.ObservablesClosed] messages indicating that certain observables
* aren't consumed anymore, which should subsequently stop the stream from the server. Note that some observations may
* already be in flight when this is sent, the client should handle this gracefully.
*
* An example session:
* Client Server
* ----------RpcRequest(RID0)-----------> // Client makes RPC request with ID "RID0"
* <----RpcReply(RID0, Payload(OID0))---- // Server sends reply containing an observable with ID "OID0"
* <---------Observation(OID0)----------- // Server sends observation onto "OID0"
* <---Observation(OID0, Payload(OID1))-- // Server sends another observation, this time containing another observable
* <---------Observation(OID1)----------- // Observation onto new "OID1"
* <---------Observation(OID0)-----------
* -----ObservablesClosed(OID0, OID1)---> // Client indicates it stopped consuming the Observables.
* <---------Observation(OID1)----------- // Observation was already in-flight before the previous message was processed
* (FIN)
*
* Note that multiple sessions like the above may interleave in an arbitrary fashion.
*
* Additionally the server may listen on client binding removals for cleanup using [RPC_CLIENT_BINDING_REMOVALS]. This
* requires the server to create a filter on the artemis notification address using [RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION]
* Constants and data types used by the RPC API.
*/
object RPCApi {
private val TAG_FIELD_NAME = "tag"
@ -62,10 +56,15 @@ object RPCApi {
private val OBSERVABLE_ID_FIELD_NAME = "observable-id"
private val METHOD_NAME_FIELD_NAME = "method-name"
val RPC_SERVER_QUEUE_NAME = "rpc.server"
val RPC_CLIENT_QUEUE_NAME_PREFIX = "rpc.client"
val RPC_CLIENT_BINDING_REMOVALS = "rpc.clientqueueremovals"
val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions"
/** Name of the Artemis queue on which the server receives RPC requests (as [ClientToServer.RpcRequest]). */
const val RPC_SERVER_QUEUE_NAME = "rpc.server"
/**
* Prefix to Artemis queue names used by clients to receive communication back from a server. The full queue name
* should be of the form "rpc.client.&lt;username&gt;.&lt;nonce&gt;".
*/
const val RPC_CLIENT_QUEUE_NAME_PREFIX = "rpc.client"
const val RPC_CLIENT_BINDING_REMOVALS = "rpc.clientqueueremovals"
const val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions"
val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION =
"${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " +
@ -83,12 +82,23 @@ object RPCApi {
return ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }
}
/**
* Message content types which can be sent from a Corda client to a server.
*/
sealed class ClientToServer {
private enum class Tag {
RPC_REQUEST,
OBSERVABLES_CLOSED
}
/**
* Request to a server to trigger the specified method with the provided arguments.
*
* @param clientAddress return address to contact the client at.
* @param id a unique ID for the request, which the server will use to identify its response with.
* @param methodName name of the method (procedure) to be called.
* @param arguments arguments to pass to the method, if any.
*/
data class RpcRequest(
val clientAddress: SimpleString,
val id: RpcRequestId,
@ -141,6 +151,9 @@ object RPCApi {
}
}
/**
* Message content types which can be sent from a Corda server back to a client.
*/
sealed class ServerToClient {
private enum class Tag {
RPC_REPLY,
@ -149,6 +162,7 @@ object RPCApi {
abstract fun writeToClientMessage(context: SerializationContext, message: ClientMessage)
/** Reply in response to an [ClientToServer.RpcRequest]. */
data class RpcReply(
val id: RpcRequestId,
val result: Try<Any?>
@ -156,7 +170,7 @@ object RPCApi {
override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) {
message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal)
message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong)
message.bodyBuffer.writeBytes(result.serialize(context = context).bytes)
message.bodyBuffer.writeBytes(result.safeSerialize(context) { Try.Failure(it) }.bytes)
}
}
@ -167,11 +181,17 @@ object RPCApi {
override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) {
message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVATION.ordinal)
message.putLongProperty(OBSERVABLE_ID_FIELD_NAME, id.toLong)
message.bodyBuffer.writeBytes(content.serialize(context = context).bytes)
message.bodyBuffer.writeBytes(content.safeSerialize(context) { Notification.createOnError<Void?>(it) }.bytes)
}
}
companion object {
private fun Any.safeSerialize(context: SerializationContext, wrap: (Throwable) -> Any) = try {
serialize(context = context)
} catch (t: Throwable) {
wrap(t).serialize(context = context)
}
fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient {
val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)]
return when (tag) {

View File

@ -41,10 +41,17 @@ open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeExcep
constructor(msg: String) : this(msg, null)
}
/**
* Thrown to indicate that the calling user does not have permission for something they have requested (for example
* calling a method).
*/
@CordaSerializable
class PermissionException(msg: String) : RuntimeException(msg)
// The Kryo used for the RPC wire protocol. Every type in the wire protocol is listed here explicitly.
/**
* The Kryo used for the RPC wire protocol.
*/
// Every type in the wire protocol is listed here explicitly.
// This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes,
// because we can see everything we're using in one place.
class RPCKryo(observableSerializer: Serializer<Observable<*>>, serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationContext)) {

View File

@ -3,7 +3,6 @@ package net.corda.nodeapi.config
import com.typesafe.config.Config
import com.typesafe.config.ConfigUtil
import net.corda.core.internal.noneOrSingle
import net.corda.core.utilities.validateX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.parseNetworkHostAndPort
import org.bouncycastle.asn1.x500.X500Name

View File

@ -1,20 +1,35 @@
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.core.serialization.SerializedBytes
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
import java.util.*
import java.util.concurrent.ConcurrentHashMap
internal val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0
val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0
class AMQPSerializationCustomization(val factory: SerializerFactory) : SerializationCustomization {
override fun addToWhitelist(vararg types: Class<*>) {
factory.addToWhitelist(*types)
}
}
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
for (type in types) {
(this.whitelist as? MutableClassWhitelist)?.add(type)
}
}
abstract class AbstractAMQPSerializationScheme : SerializationScheme {
internal companion object {
private val pluginRegistries: List<CordaPluginRegistry> by lazy {
ServiceLoader.load(CordaPluginRegistry::class.java, this::class.java.classLoader).toList()
}
fun registerCustomSerializers(factory: SerializerFactory) {
factory.apply {
register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer)
@ -40,6 +55,8 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme {
register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateHolderSerializer)
register(net.corda.nodeapi.internal.serialization.amqp.custom.PartyAndCertificateSerializer(factory))
}
val customizer = AMQPSerializationCustomization(factory)
pluginRegistries.forEach { it.customizeSerialization(customizer) }
}
}

View File

@ -6,7 +6,10 @@ 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.core.serialization.*
import net.corda.core.serialization.AttachmentsClassLoader
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import net.corda.core.utilities.loggerFor
import java.io.PrintWriter
import java.lang.reflect.Modifier.isAbstract
@ -16,8 +19,10 @@ import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.util.*
fun Kryo.addToWhitelist(type: Class<*>) {
((classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type)
fun Kryo.addToWhitelist(vararg types: Class<*>) {
for (type in types) {
((classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type)
}
}
/**
@ -134,7 +139,11 @@ object EmptyWhitelist : ClassWhitelist {
}
class BuiltInExceptionsWhitelist : ClassWhitelist {
override fun hasListed(type: Class<*>): Boolean = Throwable::class.java.isAssignableFrom(type) && type.`package`.name.startsWith("java.")
companion object {
private val packageName = "^(?:java|kotlin)(?:[.]|$)".toRegex()
}
override fun hasListed(type: Class<*>) = Throwable::class.java.isAssignableFrom(type) && packageName.containsMatchIn(type.`package`.name)
}
object AllWhitelist : ClassWhitelist {

View File

@ -11,7 +11,7 @@ import de.javakaffee.kryoserializers.BitSetSerializer
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
import de.javakaffee.kryoserializers.guava.*
import net.corda.core.contracts.PrivacySalt
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.serialization.SerializeAsToken

View File

@ -18,54 +18,54 @@ import java.util.*
class DefaultWhitelist : CordaPluginRegistry() {
override fun customizeSerialization(custom: SerializationCustomization): Boolean {
custom.apply {
// TODO: Turn this into an array and use map {}
addToWhitelist(Array<Any>(0, {}).javaClass)
addToWhitelist(Notification::class.java)
addToWhitelist(Notification.Kind::class.java)
addToWhitelist(ArrayList::class.java)
addToWhitelist(listOf<Any>().javaClass) // EmptyList
addToWhitelist(Pair::class.java)
addToWhitelist(ByteArray::class.java)
addToWhitelist(UUID::class.java)
addToWhitelist(LinkedHashSet::class.java)
addToWhitelist(setOf<Unit>().javaClass) // EmptySet
addToWhitelist(Currency::class.java)
addToWhitelist(listOf(Unit).javaClass) // SingletonList
addToWhitelist(setOf(Unit).javaClass) // SingletonSet
addToWhitelist(mapOf(Unit to Unit).javaClass) // SingletonSet
addToWhitelist(NetworkHostAndPort::class.java)
addToWhitelist(SimpleString::class.java)
addToWhitelist(KryoException::class.java)
addToWhitelist(StringBuffer::class.java)
addToWhitelist(Unit::class.java)
addToWhitelist(java.io.ByteArrayInputStream::class.java)
addToWhitelist(java.lang.Class::class.java)
addToWhitelist(java.math.BigDecimal::class.java)
addToWhitelist(java.security.KeyPair::class.java)
addToWhitelist(Array<Any>(0, {}).javaClass,
Notification::class.java,
Notification.Kind::class.java,
ArrayList::class.java,
listOf<Any>().javaClass, // EmptyList
Pair::class.java,
ByteArray::class.java,
UUID::class.java,
LinkedHashSet::class.java,
setOf<Unit>().javaClass, // EmptySet
Currency::class.java,
listOf(Unit).javaClass, // SingletonList
setOf(Unit).javaClass, // SingletonSet
mapOf(Unit to Unit).javaClass, // SingletonSet
NetworkHostAndPort::class.java,
SimpleString::class.java,
KryoException::class.java,
StringBuffer::class.java,
Unit::class.java,
java.io.ByteArrayInputStream::class.java,
java.lang.Class::class.java,
java.math.BigDecimal::class.java,
java.security.KeyPair::class.java,
// Matches the list in TimeSerializers.addDefaultSerializers:
addToWhitelist(java.time.Duration::class.java)
addToWhitelist(java.time.Instant::class.java)
addToWhitelist(java.time.LocalDate::class.java)
addToWhitelist(java.time.LocalDateTime::class.java)
addToWhitelist(java.time.ZoneOffset::class.java)
addToWhitelist(java.time.ZoneId::class.java)
addToWhitelist(java.time.OffsetTime::class.java)
addToWhitelist(java.time.OffsetDateTime::class.java)
addToWhitelist(java.time.ZonedDateTime::class.java)
addToWhitelist(java.time.Year::class.java)
addToWhitelist(java.time.YearMonth::class.java)
addToWhitelist(java.time.MonthDay::class.java)
addToWhitelist(java.time.Period::class.java)
addToWhitelist(java.time.DayOfWeek::class.java) // No custom serialiser but it's an enum.
// Matches the list in TimeSerializers.addDefaultSerializers:
java.time.Duration::class.java,
java.time.Instant::class.java,
java.time.LocalDate::class.java,
java.time.LocalDateTime::class.java,
java.time.ZoneOffset::class.java,
java.time.ZoneId::class.java,
java.time.OffsetTime::class.java,
java.time.OffsetDateTime::class.java,
java.time.ZonedDateTime::class.java,
java.time.Year::class.java,
java.time.YearMonth::class.java,
java.time.MonthDay::class.java,
java.time.Period::class.java,
java.time.DayOfWeek::class.java, // No custom serialiser but it's an enum.
addToWhitelist(java.util.Collections.singletonMap("A", "B").javaClass)
addToWhitelist(java.util.LinkedHashMap::class.java)
addToWhitelist(BigDecimal::class.java)
addToWhitelist(LocalDate::class.java)
addToWhitelist(Period::class.java)
addToWhitelist(BitSet::class.java)
addToWhitelist(OnErrorNotImplementedException::class.java)
java.util.Collections.singletonMap("A", "B").javaClass,
java.util.LinkedHashMap::class.java,
BigDecimal::class.java,
LocalDate::class.java,
Period::class.java,
BitSet::class.java,
OnErrorNotImplementedException::class.java,
StackTraceElement::class.java)
}
return true
}

View File

@ -8,12 +8,11 @@ import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
import com.esotericsoftware.kryo.serializers.FieldSerializer
import com.esotericsoftware.kryo.util.MapReferenceResolver
import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party
import net.corda.core.internal.VisibleForTesting
import net.corda.core.serialization.AttachmentsClassLoader
import net.corda.core.serialization.MissingAttachmentsException
import net.corda.core.serialization.SerializeAsTokenContext
@ -245,9 +244,6 @@ fun Input.readBytesWithLength(): ByteArray {
/** A serialisation engine that knows how to deserialise code inside a sandbox */
@ThreadSafe
object WireTransactionSerializer : Serializer<WireTransaction>() {
@VisibleForTesting
internal val attachmentsClassLoaderEnabled = "attachments.class.loader.enabled"
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
kryo.writeClassAndObject(output, obj.inputs)
kryo.writeClassAndObject(output, obj.attachments)
@ -259,7 +255,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
}
private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List<SecureHash>): ClassLoader? {
kryo.context[attachmentsClassLoaderEnabled] as? Boolean ?: false || return null
kryo.context[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return null
val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one.
val missing = ArrayList<SecureHash>()
val attachments = ArrayList<Attachment>()

View File

@ -4,7 +4,7 @@ import com.esotericsoftware.kryo.Kryo
import net.corda.core.serialization.SerializationCustomization
class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization {
override fun addToWhitelist(type: Class<*>) {
kryo.addToWhitelist(type)
override fun addToWhitelist(vararg types: Class<*>) {
kryo.addToWhitelist(*types)
}
}

View File

@ -8,6 +8,10 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
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.LazyPool
import net.corda.core.serialization.*
import net.corda.core.utilities.ByteSequence
@ -17,6 +21,8 @@ import java.io.NotSerializableException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled"
object NotSupportedSeralizationScheme : SerializationScheme {
private fun doThrow(): Nothing = throw UnsupportedOperationException("Serialization scheme not supported.")
@ -33,6 +39,24 @@ data class SerializationContextImpl(override val preferredSerializationVersion:
override val properties: Map<Any, Any>,
override val objectReferencesEnabled: Boolean,
override val useCase: SerializationContext.UseCase) : SerializationContext {
private val cache: Cache<List<SecureHash>, AttachmentsClassLoader> = CacheBuilder.newBuilder().weakValues().maximumSize(1024).build()
// We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context.
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return this
val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContextImpl ?: return this // Some tests don't set one.
return withClassLoader(cache.get(attachmentHashes) {
val missing = ArrayList<SecureHash>()
val attachments = ArrayList<Attachment>()
attachmentHashes.forEach { id ->
serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id }
}
missing.isNotEmpty() && throw MissingAttachmentsException(missing)
AttachmentsClassLoader(attachments)
})
}
override fun withProperty(property: Any, value: Any): SerializationContext {
return copy(properties = properties + (property to value))
}
@ -56,7 +80,7 @@ data class SerializationContextImpl(override val preferredSerializationVersion:
private const val HEADER_SIZE: Int = 8
open class SerializationFactoryImpl : SerializationFactory {
open class SerializationFactoryImpl : SerializationFactory() {
private val creator: List<StackTraceElement> = Exception().stackTrace.asList()
private val registeredSchemes: MutableCollection<SerializationScheme> = Collections.synchronizedCollection(mutableListOf())
@ -75,10 +99,12 @@ open class SerializationFactoryImpl : SerializationFactory {
}
@Throws(NotSerializableException::class)
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T = schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context)
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) } }
}
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
return schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context)
return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) } }
}
fun registerScheme(scheme: SerializationScheme) {

View File

@ -1,6 +1,7 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -10,7 +11,7 @@ import java.lang.reflect.Type
* [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary].
*/
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
override val typeDescriptor: String = SerializerFactory.primitiveTypeName(clazz)!!
override val typeDescriptor = Symbol.valueOf(SerializerFactory.primitiveTypeName(clazz)!!)
override val type: Type = clazz
// NOOP since this is a primitive type.

View File

@ -1,5 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -18,7 +19,7 @@ interface AMQPSerializer<out T> {
*
* This should be unique enough that we can use one global cache of [AMQPSerializer]s and use this as the look up key.
*/
val typeDescriptor: String
val typeDescriptor: Symbol
/**
* Add anything required to the AMQP schema via [SerializationOutput.writeTypeNotations] and any dependent serializers

View File

@ -1,5 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
@ -31,12 +32,12 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
"${type.componentType().typeName}$arrayType"
}
override val typeDescriptor by lazy { "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" }
override val typeDescriptor by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") }
internal val elementType: Type by lazy { type.componentType() }
internal open val typeName by lazy { calcTypeName(type) }
internal val typeNotation: TypeNotation by lazy {
RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
}
override fun writeClassInfo(output: SerializationOutput) {

View File

@ -1,6 +1,7 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.utilities.NonEmptySet
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
@ -14,27 +15,51 @@ import kotlin.collections.Set
* Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s.
*/
class CollectionSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString())
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
companion object {
private val supportedTypes: Map<Class<out Collection<*>>, (List<*>) -> Collection<*>> = mapOf(
// NB: Order matters in this map, the most specific classes should be listed at the end
private val supportedTypes: Map<Class<out Collection<*>>, (List<*>) -> Collection<*>> = Collections.unmodifiableMap(linkedMapOf(
Collection::class.java to { list -> Collections.unmodifiableCollection(list) },
List::class.java to { list -> Collections.unmodifiableList(list) },
Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) },
SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) },
NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) },
NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) }
)
))
private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.")
}
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
if(supportedTypes.containsKey(declaredClass)) {
// Simple case - it is already known to be a collection.
@Suppress("UNCHECKED_CAST")
return deriveParametrizedType(declaredType, declaredClass as Class<out Collection<*>>)
}
else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) {
// Declared class is not collection, but [actualClass] is - represent it accordingly.
val collectionClass = findMostSuitableCollectionType(actualClass)
return deriveParametrizedType(declaredType, collectionClass)
}
throw NotSerializableException("Cannot derive collection type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'")
}
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Collection<*>>): ParameterizedType =
(declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType))
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
}
private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {

View File

@ -1,7 +1,7 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.SerializationDefaults
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -50,8 +50,8 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor, nameForType(clazz))}"
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(clazz), null, emptyList(), SerializerFactory.nameForType(superClassSerializer.type), Descriptor(typeDescriptor, null), emptyList())
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}")
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(clazz), null, emptyList(), SerializerFactory.nameForType(superClassSerializer.type), Descriptor(typeDescriptor), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation)
}
@ -73,7 +73,7 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
*/
abstract class CustomSerializerImp<T>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}"
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}")
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
@ -160,11 +160,11 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
descriptor, emptyList())))
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) {
data.putObject(unmaker(obj))
data.putString(unmaker(obj))
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
val proxy = input.readObject(obj, schema, String::class.java) as String
val proxy = obj as String
return maker(proxy)
}
}

View File

@ -6,12 +6,12 @@ import net.corda.core.utilities.ByteSequence
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.UnsignedByte
import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.nio.ByteBuffer
import java.util.*
data class ObjectAndEnvelope<out T>(val obj: T, val envelope: Envelope)
@ -22,11 +22,10 @@ data class ObjectAndEnvelope<out T>(val obj: T, val envelope: Envelope)
* instances and threads.
*/
class DeserializationInput(internal val serializerFactory: SerializerFactory) {
// TODO: we're not supporting object refs yet
private val objectHistory: MutableList<Any> = ArrayList()
private val objectHistory: MutableList<Any> = mutableListOf()
internal companion object {
val BYTES_NEEDED_TO_PEEK: Int = 23
private val BYTES_NEEDED_TO_PEEK: Int = 23
fun peekSize(bytes: ByteArray): Int {
// There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor
@ -58,7 +57,6 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
inline internal fun <reified T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>): ObjectAndEnvelope<T> =
deserializeAndReturnEnvelope(bytes, T::class.java)
@Throws(NotSerializableException::class)
private fun getEnvelope(bytes: ByteSequence): Envelope {
// Check that the lead bytes match expected header
@ -95,40 +93,53 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
* be deserialized and a schema describing the types of the objects.
*/
@Throws(NotSerializableException::class)
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T {
return des {
val envelope = getEnvelope(bytes)
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
}
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T = des {
val envelope = getEnvelope(bytes)
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
}
@Throws(NotSerializableException::class)
internal fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> {
return des {
val envelope = getEnvelope(bytes)
// Now pick out the obj and schema from the envelope.
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
}
fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): ObjectAndEnvelope<T> = des {
val envelope = getEnvelope(bytes)
// Now pick out the obj and schema from the envelope.
ObjectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
}
internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
return if (obj == null) null else readObject(obj, schema, type)
}
internal fun readObject(obj: Any, schema: Schema, type: Type): Any {
if (obj is DescribedType) {
// Look up serializer in factory by descriptor
val serializer = serializerFactory.get(obj.descriptor, schema)
if (serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
"expected to be of type $type but was ${serializer.type}")
return serializer.readObject(obj.described, schema, this)
} else if (obj is Binary) {
return obj.array
} else {
return obj
}
}
internal fun readObject(obj: Any, schema: Schema, type: Type): 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()
if (objectIndex !in 0..objectHistory.size)
throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " +
"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}'")
objectRetrieved
} else {
val objectRead = when (obj) {
is DescribedType -> {
// Look up serializer in factory by descriptor
val serializer = serializerFactory.get(obj.descriptor, schema)
if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) })
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " +
"expected to be of type $type but was ${serializer.type}")
serializer.readObject(obj.described, schema, this)
}
is Binary -> obj.array
else -> obj // this will be the case for primitive types like [boolean] et al.
}
// 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)
objectRead
}
/**
* TODO: Currently performs rather basic checks aimed in particular at [java.util.List<Command<?>>] and
@ -136,5 +147,5 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
* In the future tighter control might be needed
*/
private fun Type.materiallyEquivalentTo(that: Type): Boolean =
asClass() == that.asClass() && that is ParameterizedType
}
asClass() == that.asClass() && that is ParameterizedType
}

View File

@ -53,6 +53,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
var typeStart = 0
var needAType = true
var skippingWhitespace = false
while (pos < params.length) {
if (params[pos] == '<') {
val typeEnd = pos++
@ -102,7 +103,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
} else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) {
pos++
} else {
throw NotSerializableException("Invalid character in middle of type: ${params[pos]}")
throw NotSerializableException("Invalid character ${params[pos]} in middle of type $params at idx $pos")
}
}
}

View File

@ -0,0 +1,52 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
/**
* Our definition of an enum with the AMQP spec is a list (of two items, a string and an int) that is
* a restricted type with a number of choices associated with it
*/
class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
private val typeNotation: TypeNotation
init {
typeNotation = RestrictedType(
SerializerFactory.nameForType(declaredType),
null, emptyList(), "list", Descriptor(typeDescriptor),
declaredClass.enumConstants.zip(IntRange(0, declaredClass.enumConstants.size)).map {
Choice(it.first.toString(), it.second.toString())
})
}
override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
val enumName = (obj as List<*>)[0] as String
val enumOrd = obj[1] as Int
val fromOrd = type.asClass()!!.enumConstants[enumOrd]
if (enumName != fromOrd?.toString()) {
throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but "
+ "ordinality has changed")
}
return fromOrd
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't")
data.withDescribed(typeNotation.descriptor) {
withList {
data.putString(obj.name)
data.putInt(obj.ordinal)
}
}
}
}

View File

@ -1,5 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
@ -10,15 +11,18 @@ import kotlin.collections.Map
import kotlin.collections.iterator
import kotlin.collections.map
private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *>
/**
* Serialization / deserialization of certain supported [Map] types.
*/
class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString())
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
companion object {
private val supportedTypes: Map<Class<out Any?>, (Map<*, *>) -> Map<*, *>> = mapOf(
// NB: Order matters in this map, the most specific classes should be listed at the end
private val supportedTypes: Map<Class<out Map<*, *>>, MapCreationFunction> = Collections.unmodifiableMap(linkedMapOf(
// Interfaces
Map::class.java to { map -> Collections.unmodifiableMap(map) },
SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) },
@ -26,15 +30,38 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact
// concrete classes for user convenience
LinkedHashMap::class.java to { map -> LinkedHashMap(map) },
TreeMap::class.java to { map -> TreeMap(map) }
)
private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> {
))
private fun findConcreteType(clazz: Class<*>): MapCreationFunction {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
}
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
if(supportedTypes.containsKey(declaredClass)) {
// Simple case - it is already known to be a map.
@Suppress("UNCHECKED_CAST")
return deriveParametrizedType(declaredType, declaredClass as Class<out Map<*, *>>)
}
else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) {
// Declared class is not map, but [actualClass] is - represent it accordingly.
val mapClass = findMostSuitableMapType(actualClass)
return deriveParametrizedType(declaredType, mapClass)
}
throw NotSerializableException("Cannot derive map type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'")
}
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Map<*, *>>): ParameterizedType =
(declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType, SerializerFactory.AnyType))
private fun findMostSuitableMapType(actualClass: Class<*>): Class<out Map<*, *>> =
MapSerializer.supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
}
private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>)
private val concreteBuilder: MapCreationFunction = findConcreteType(declaredType.rawType as Class<*>)
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList())
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {

View File

@ -1,7 +1,9 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
@ -13,7 +15,9 @@ import kotlin.reflect.jvm.javaConstructor
open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz
open val kotlinConstructor = constructorForDeserialization(clazz)
val javaConstructor by lazy { kotlinConstructor?.javaConstructor }
val javaConstructor by lazy { kotlinConstructor?.javaConstructor?.apply { isAccessible = true } }
private val logger = loggerFor<ObjectSerializer>()
open internal val propertySerializers: Collection<PropertySerializer> by lazy {
propertiesForSerialization(kotlinConstructor, clazz, factory)
@ -21,10 +25,10 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
private val typeName = nameForType(clazz)
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
private val interfaces = interfacesForSerialization(clazz, factory) // We restrict to only those annotated or whitelisted
open internal val typeNotation : TypeNotation by lazy {CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) }
open internal val typeNotation: TypeNotation by lazy { CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields()) }
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
@ -50,10 +54,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
if (obj is UnsignedInteger) {
// TODO: Object refs
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} else if (obj is List<*>) {
if (obj is List<*>) {
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
return construct(params)
@ -66,6 +67,12 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
private fun generateProvides(): List<String> = interfaces.map { nameForType(it) }
fun construct(properties: List<Any?>) = javaConstructor?.newInstance(*properties.toTypedArray()) ?:
throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
fun construct(properties: List<Any?>): Any {
logger.debug { "Calling constructor: '$javaConstructor' with properties '$properties'" }
return javaConstructor?.newInstance(*properties.toTypedArray()) ?:
throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
}
}

View File

@ -2,17 +2,18 @@ package net.corda.nodeapi.internal.serialization.amqp
import com.google.common.hash.Hasher
import com.google.common.hash.Hashing
import net.corda.core.crypto.toBase64
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toBase64
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.amqp.UnsignedLong
import org.apache.qpid.proton.codec.Data
import org.apache.qpid.proton.codec.DescribedTypeConstructor
import java.io.NotSerializableException
import java.lang.reflect.*
import java.util.*
import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField
import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
@ -24,6 +25,21 @@ val DESCRIPTOR_DOMAIN: String = "net.corda"
// "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB
val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteArray())
private enum class DescriptorRegistry(val id: Long) {
ENVELOPE(1),
SCHEMA(2),
OBJECT_DESCRIPTOR(3),
FIELD(4),
COMPOSITE_TYPE(5),
RESTRICTED_TYPE(6),
CHOICE(7),
REFERENCED_OBJECT(8),
;
val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS)
}
/**
* This class wraps all serialized data, so that the schema can be carried along with it. We will provide various internal utilities
* to decompose and recompose with/without schema etc so that e.g. we can store objects with a (relationally) normalised out schema to
@ -32,7 +48,7 @@ val AmqpHeaderV1_0: OpaqueBytes = OpaqueBytes("corda\u0001\u0000\u0000".toByteAr
// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we don't recognise a type descriptor.
data class Envelope(val obj: Any?, val schema: Schema) : DescribedType {
companion object : DescribedTypeConstructor<Envelope> {
val DESCRIPTOR = UnsignedLong(1L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.ENVELOPE.amqpDescriptor
val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR)
fun get(data: Data): Envelope {
@ -63,7 +79,7 @@ data class Envelope(val obj: Any?, val schema: Schema) : DescribedType {
*/
data class Schema(val types: List<TypeNotation>) : DescribedType {
companion object : DescribedTypeConstructor<Schema> {
val DESCRIPTOR = UnsignedLong(2L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.SCHEMA.amqpDescriptor
fun get(obj: Any): Schema {
val describedType = obj as DescribedType
@ -90,9 +106,11 @@ data class Schema(val types: List<TypeNotation>) : DescribedType {
override fun toString(): String = types.joinToString("\n")
}
data class Descriptor(val name: String?, val code: UnsignedLong? = null) : DescribedType {
data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : DescribedType {
constructor(name: String?) : this(Symbol.valueOf(name))
companion object : DescribedTypeConstructor<Descriptor> {
val DESCRIPTOR = UnsignedLong(3L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor
fun get(obj: Any): Descriptor {
val describedType = obj as DescribedType
@ -106,7 +124,7 @@ data class Descriptor(val name: String?, val code: UnsignedLong? = null) : Descr
override fun newInstance(described: Any?): Descriptor {
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
return Descriptor(list[0] as? String, list[1] as? UnsignedLong)
return Descriptor(list[0] as? Symbol, list[1] as? UnsignedLong)
}
}
@ -130,7 +148,7 @@ data class Descriptor(val name: String?, val code: UnsignedLong? = null) : Descr
data class Field(val name: String, val type: String, val requires: List<String>, val default: String?, val label: String?, val mandatory: Boolean, val multiple: Boolean) : DescribedType {
companion object : DescribedTypeConstructor<Field> {
val DESCRIPTOR = UnsignedLong(4L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.FIELD.amqpDescriptor
fun get(obj: Any): Field {
val describedType = obj as DescribedType
@ -175,12 +193,10 @@ sealed class TypeNotation : DescribedType {
companion object {
fun get(obj: Any): TypeNotation {
val describedType = obj as DescribedType
if (describedType.descriptor == CompositeType.DESCRIPTOR) {
return CompositeType.get(describedType)
} else if (describedType.descriptor == RestrictedType.DESCRIPTOR) {
return RestrictedType.get(describedType)
} else {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
return when (describedType.descriptor) {
CompositeType.DESCRIPTOR -> CompositeType.get(describedType)
RestrictedType.DESCRIPTOR -> RestrictedType.get(describedType)
else -> throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
}
}
@ -193,7 +209,7 @@ sealed class TypeNotation : DescribedType {
data class CompositeType(override val name: String, override val label: String?, override val provides: List<String>, override val descriptor: Descriptor, val fields: List<Field>) : TypeNotation() {
companion object : DescribedTypeConstructor<CompositeType> {
val DESCRIPTOR = UnsignedLong(5L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor
fun get(describedType: DescribedType): CompositeType {
if (describedType.descriptor != DESCRIPTOR) {
@ -236,9 +252,14 @@ data class CompositeType(override val name: String, override val label: String?,
}
}
data class RestrictedType(override val name: String, override val label: String?, override val provides: List<String>, val source: String, override val descriptor: Descriptor, val choices: List<Choice>) : TypeNotation() {
data class RestrictedType(override val name: String,
override val label: String?,
override val provides: List<String>,
val source: String,
override val descriptor: Descriptor,
val choices: List<Choice>) : TypeNotation() {
companion object : DescribedTypeConstructor<RestrictedType> {
val DESCRIPTOR = UnsignedLong(6L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor
fun get(describedType: DescribedType): RestrictedType {
if (describedType.descriptor != DESCRIPTOR) {
@ -274,6 +295,9 @@ data class RestrictedType(override val name: String, override val label: String?
}
sb.append(">\n")
sb.append(" $descriptor\n")
choices.forEach {
sb.append(" $it\n")
}
sb.append("</type>")
return sb.toString()
}
@ -281,7 +305,7 @@ data class RestrictedType(override val name: String, override val label: String?
data class Choice(val name: String, val value: String) : DescribedType {
companion object : DescribedTypeConstructor<Choice> {
val DESCRIPTOR = UnsignedLong(7L or DESCRIPTOR_TOP_32BITS)
val DESCRIPTOR = DescriptorRegistry.CHOICE.amqpDescriptor
fun get(obj: Any): Choice {
val describedType = obj as DescribedType
@ -308,7 +332,35 @@ data class Choice(val name: String, val value: String) : DescribedType {
}
}
data class ReferencedObject(private val refCounter: Int) : DescribedType {
companion object : DescribedTypeConstructor<ReferencedObject> {
val DESCRIPTOR = DescriptorRegistry.REFERENCED_OBJECT.amqpDescriptor
fun get(obj: Any): ReferencedObject {
val describedType = obj as DescribedType
if (describedType.descriptor != DESCRIPTOR) {
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
}
return newInstance(describedType.described)
}
override fun getTypeClass(): Class<*> = ReferencedObject::class.java
override fun newInstance(described: Any?): ReferencedObject {
val unInt = described as? UnsignedInteger ?: throw IllegalStateException("Was expecting an UnsignedInteger")
return ReferencedObject(unInt.toInt())
}
}
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): UnsignedInteger = UnsignedInteger(refCounter)
override fun toString(): String = "<refObject refCounter=$refCounter/>"
}
private val ARRAY_HASH: String = "Array = true"
private val ENUM_HASH: String = "Enum = true"
private val ALREADY_SEEN_HASH: String = "Already seen = true"
private val NULLABLE_HASH: String = "Nullable = true"
private val NOT_NULLABLE_HASH: String = "Nullable = false"
@ -339,7 +391,7 @@ internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String {
return hasher.hash().asBytes().toBase64()
}
private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher) : Hasher {
private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher): Hasher {
// Need to check if a custom serializer is applicable
val customSerializer = factory.findCustomSerializer(clazz, declaredType)
return if (customSerializer != null) {
@ -357,51 +409,58 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta
} else {
alreadySeen += type
try {
if (type is SerializerFactory.AnyType) {
hasher.putUnencodedChars(ANY_TYPE_HASH)
} else if (type is Class<*>) {
if (type.isArray) {
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (SerializerFactory.isPrimitive(type)) {
hasher.putUnencodedChars(type.name)
} else if (isCollectionOrMap(type)) {
hasher.putUnencodedChars(type.name)
} else {
hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) {
if (type.kotlin.objectInstance != null) {
// TODO: name collision is too likely for kotlin objects, we need to introduce some reference
// to the CorDapp but maybe reference to the JAR in the short term.
hasher.putUnencodedChars(type.name)
} else {
fingerprintForObject(type, type, alreadySeen, hasher, factory)
when (type) {
is SerializerFactory.AnyType -> hasher.putUnencodedChars(ANY_TYPE_HASH)
is Class<*> -> {
if (type.isArray) {
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (SerializerFactory.isPrimitive(type)) {
hasher.putUnencodedChars(type.name)
} else if (isCollectionOrMap(type)) {
hasher.putUnencodedChars(type.name)
} else if (type.isEnum) {
// ensures any change to the enum (adding constants) will trigger the need for evolution
hasher.apply {
type.enumConstants.forEach {
putUnencodedChars(it.toString())
}
}.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH)
} else {
hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) {
if (type.kotlin.objectInstance != null) {
// TODO: name collision is too likely for kotlin objects, we need to introduce some reference
// to the CorDapp but maybe reference to the JAR in the short term.
hasher.putUnencodedChars(type.name)
} else {
fingerprintForObject(type, type, alreadySeen, hasher, factory)
}
}
}
}
} else if (type is ParameterizedType) {
// Hash the rawType + params
val clazz = type.rawType as Class<*>
val startingHash = if (isCollectionOrMap(clazz)) {
hasher.putUnencodedChars(clazz.name)
} else {
hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) {
fingerprintForObject(type, type, alreadySeen, hasher, factory)
is ParameterizedType -> {
// Hash the rawType + params
val clazz = type.rawType as Class<*>
val startingHash = if (isCollectionOrMap(clazz)) {
hasher.putUnencodedChars(clazz.name)
} else {
hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) {
fingerprintForObject(type, type, alreadySeen, hasher, factory)
}
}
// ... and concatentate the type data for each parameter type.
type.actualTypeArguments.fold(startingHash) { orig, paramType ->
fingerprintForType(paramType, type, alreadySeen, orig, factory)
}
}
// ... and concatentate the type data for each parameter type.
type.actualTypeArguments.fold(startingHash) { orig, paramType -> fingerprintForType(paramType, type, alreadySeen, orig, factory) }
} else if (type is GenericArrayType) {
// Hash the element type + some array hash
fingerprintForType(type.genericComponentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (type is TypeVariable<*>) {
// TODO: include bounds
hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH)
} else if (type is WildcardType) {
hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH)
// Hash the element type + some array hash
is GenericArrayType -> fingerprintForType(type.genericComponentType, contextType, alreadySeen,
hasher, factory).putUnencodedChars(ARRAY_HASH)
// TODO: include bounds
is TypeVariable<*> -> hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH)
is WildcardType -> hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH)
else -> throw NotSerializableException("Don't know how to hash")
}
else {
throw NotSerializableException("Don't know how to hash")
}
} catch(e: NotSerializableException) {
} catch (e: NotSerializableException) {
val msg = "${e.message} -> $type"
logger.error(msg, e)
throw NotSerializableException(msg)

View File

@ -1,5 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeToken
import org.apache.qpid.proton.codec.Data
import java.beans.IndexedPropertyDescriptor
@ -36,7 +37,7 @@ internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
val kotlinConstructors = clazz.kotlin.constructors
val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() }
for (kotlinConstructor in kotlinConstructors) {
if (preferredCandidate == null && kotlinConstructors.size == 1 && !hasDefault) {
if (preferredCandidate == null && kotlinConstructors.size == 1) {
preferredCandidate = kotlinConstructor
} else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) {
preferredCandidate = kotlinConstructor
@ -155,6 +156,19 @@ fun Data.withList(block: Data.() -> Unit) {
exit() // exit list
}
/**
* Extension helper for outputting reference to already observed object
*/
fun Data.writeReferencedObject(refObject: ReferencedObject) {
// Write described
putDescribed()
enter()
// Write descriptor
putObject(refObject.descriptor)
putUnsignedInteger(refObject.described)
exit() // exit described
}
private fun resolveTypeVariables(actualType: Type, contextType: Type?): Type {
val resolvedType = if (contextType != null) TypeToken.of(contextType).resolveType(actualType).type else actualType
// TODO: surely we check it is concrete at this point with no TypeVariables
@ -206,4 +220,10 @@ internal fun Type.asParameterizedType(): ParameterizedType {
internal fun Type.isSubClassOf(type: Type): Boolean {
return TypeToken.of(this).isSubtypeOf(type)
}
// ByteArrays, primtives and boxed primitives are not stored in the object history
internal fun suitableForObjectReference(type: Type): Boolean {
val clazz = type.asClass()
return type != ByteArray::class.java && (clazz != null && !clazz.isPrimitive && !Primitives.unwrap(clazz).isPrimitive)
}

View File

@ -8,6 +8,7 @@ import java.nio.ByteBuffer
import java.util.*
import kotlin.collections.LinkedHashSet
/**
* Main entry point for serializing an object to AMQP.
*
@ -15,40 +16,48 @@ import kotlin.collections.LinkedHashSet
* instances and threads.
*/
open class SerializationOutput(internal val serializerFactory: SerializerFactory) {
// TODO: we're not supporting object refs yet
private val objectHistory: MutableMap<Any, Int> = IdentityHashMap()
private val serializerHistory: MutableSet<AMQPSerializer<*>> = LinkedHashSet()
private val schemaHistory: MutableSet<TypeNotation> = LinkedHashSet()
internal val schemaHistory: MutableSet<TypeNotation> = LinkedHashSet()
/**
* Serialize the given object to AMQP, wrapped in our [Envelope] wrapper which carries an AMQP 1.0 schema, and prefixed
* with a header to indicate that this is serialized with AMQP and not [Kryo], and what version of the Corda implementation
* of AMQP serialization contructed the serialized form.
* with a header to indicate that this is serialized with AMQP and not Kryo, and what version of the Corda implementation
* of AMQP serialization constructed the serialized form.
*/
@Throws(NotSerializableException::class)
fun <T : Any> serialize(obj: T): SerializedBytes<T> {
try {
val data = Data.Factory.create()
data.withDescribed(Envelope.DESCRIPTOR_OBJECT) {
withList {
// Our object
writeObject(obj, this)
// The schema
writeSchema(Schema(schemaHistory.toList()), this)
}
}
val bytes = ByteArray(data.encodedSize().toInt() + 8)
val buf = ByteBuffer.wrap(bytes)
buf.put(AmqpHeaderV1_0.bytes)
data.encode(buf)
return SerializedBytes(bytes)
return _serialize(obj)
} finally {
objectHistory.clear()
serializerHistory.clear()
schemaHistory.clear()
andFinally()
}
}
internal fun andFinally() {
objectHistory.clear()
serializerHistory.clear()
schemaHistory.clear()
}
internal fun <T : Any> _serialize(obj: T): SerializedBytes<T> {
val data = Data.Factory.create()
data.withDescribed(Envelope.DESCRIPTOR_OBJECT) {
withList {
// Our object
writeObject(obj, this)
// The schema
writeSchema(Schema(schemaHistory.toList()), this)
}
}
val bytes = ByteArray(data.encodedSize().toInt() + 8)
val buf = ByteBuffer.wrap(bytes)
buf.put(AmqpHeaderV1_0.bytes)
data.encode(buf)
return SerializedBytes(bytes)
}
internal fun writeObject(obj: Any, data: Data) {
writeObject(obj, data, obj.javaClass)
}
@ -71,7 +80,18 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
serializerHistory.add(serializer)
serializer.writeClassInfo(this)
}
serializer.writeObject(obj, data, type, this)
val retrievedRefCount = objectHistory[obj]
if(retrievedRefCount == null) {
serializer.writeObject(obj, data, type, this)
// 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)
}
else {
data.writeReferencedObject(ReferencedObject(retrievedRefCount))
}
}
open internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean {

View File

@ -7,49 +7,43 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.nodeapi.internal.serialization.carpenter.*
import org.apache.qpid.proton.amqp.*
import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.WildcardType
import java.lang.reflect.*
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any)
data class schemaAndDescriptor(val schema: Schema, val typeDescriptor: Any)
/**
* Factory of serializers designed to be shared across threads and invocations.
*/
// TODO: enums
// TODO: object references - need better fingerprinting?
// TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal)
// TODO: Inner classes etc. Should we allow? Currently not considered.
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
// TODO: maybe support for caching of serialized form of some core types for performance
// TODO: profile for performance in general
// TODO: use guava caches etc so not unbounded
// TODO: do we need to support a transient annotation to exclude certain properties?
// TODO: allow definition of well known types that are left out of the schema.
// TODO: generally map Object to '*' all over the place in the schema and make sure use of '*' amd '?' is consistent and documented in generics.
// TODO: found a document that states textual descriptors are Symbols. Adjust schema class appropriately.
// TODO: document and alert to the fact that classes cannot default superclass/interface properties otherwise they are "erased" due to matching with constructor.
// TODO: type name prefixes for interfaces and abstract classes? Or use label?
// TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
@ThreadSafe
class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
private val customSerializers = CopyOnWriteArrayList<CustomSerializer<out Any>>()
private val classCarpenter = ClassCarpenter(cl)
val classloader : ClassLoader
val classloader: ClassLoader
get() = classCarpenter.classloader
fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: ObjectSerializer) : AMQPSerializer<Any> {
private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>): AMQPSerializer<Any> {
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
EvolutionSerializer.make(typeNotation as CompositeType, newSerializer, this)
when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
is RestrictedType -> throw NotSerializableException("Enum evolution is not currently supported")
}
}
}
@ -66,18 +60,29 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val serializer = if (Collection::class.java.isAssignableFrom(declaredClass)) {
serializersByType.computeIfAbsent(declaredType) {
CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(
declaredClass, arrayOf(AnyType), null), this)
val serializer = when {
// Declared class may not be set to Collection, but actual class could be a collection.
// In this case use of CollectionSerializer is perfectly appropriate.
(Collection::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) -> {
val declaredTypeAmended= CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
serializersByType.computeIfAbsent(declaredTypeAmended) {
CollectionSerializer(declaredTypeAmended, this)
}
}
} else if (Map::class.java.isAssignableFrom(declaredClass)) {
serializersByType.computeIfAbsent(declaredClass) {
makeMapSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(
declaredClass, arrayOf(AnyType, AnyType), null))
// Declared class may not be set to Map, but actual class could be a map.
// In this case use of MapSerializer is perfectly appropriate.
(Map::class.java.isAssignableFrom(declaredClass) ||
(actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> {
val declaredTypeAmended= MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
serializersByType.computeIfAbsent(declaredClass) {
makeMapSerializer(declaredTypeAmended)
}
}
} else {
makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
Enum::class.java.isAssignableFrom(declaredClass) -> serializersByType.computeIfAbsent(declaredClass) {
EnumSerializer(actualType, actualClass ?: declaredClass, this)
}
else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
}
serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
@ -90,17 +95,17 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
* type.
*/
// TODO: test GenericArrayType
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: Type): Type? {
if (declaredType is ParameterizedType) {
return inferTypeVariables(actualClass, declaredClass, declaredType)
} else if (declaredType is Class<*>) {
// Nothing to infer, otherwise we'd have ParameterizedType
return actualClass
} else if (declaredType is GenericArrayType) {
val declaredComponent = declaredType.genericComponentType
return inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray()
} else return null
}
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: Type): Type? =
when (declaredType) {
is ParameterizedType -> inferTypeVariables(actualClass, declaredClass, declaredType)
// Nothing to infer, otherwise we'd have ParameterizedType
is Class<*> -> actualClass
is GenericArrayType -> {
val declaredComponent = declaredType.genericComponentType
inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray()
}
else -> null
}
/**
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
@ -117,8 +122,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
if (implementationChain != null) {
val start = implementationChain.last()
val rest = implementationChain.dropLast(1).drop(1)
val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) {
resolved, chainEntry ->
val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) { resolved, chainEntry ->
val newResolved = resolved.resolveType(chainEntry)
TypeResolver().where(chainEntry, newResolved)
}
@ -194,7 +198,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
// 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 EvolutionSerialiser
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
getEvolutionSerializer(typeNotation, serialiser as ObjectSerializer)
getEvolutionSerializer(typeNotation, serialiser)
}
} catch (e: ClassNotFoundException) {
if (sentinel || (typeNotation !is CompositeType)) throw e
@ -210,16 +214,14 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
}
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface)
is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics
}
private fun processRestrictedType(typeNotation: RestrictedType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
return get(null, type)
is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface)
is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics
}
// TODO: class loader logic, and compare the schema.
private fun processRestrictedType(typeNotation: RestrictedType) = get(null,
typeForName(typeNotation.name, classloader))
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
@ -233,7 +235,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
findCustomSerializer(clazz, declaredType) ?: run {
if (type.isArray()) {
// Allow Object[] since this can be quite common (i.e. an untyped array)
if(type.componentType() != Object::class.java) whitelisted(type.componentType())
if (type.componentType() != Object::class.java) whitelisted(type.componentType())
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
else ArraySerializer.make(type, this)
} else if (clazz.kotlin.objectInstance != null) {
@ -248,8 +250,9 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
}
internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is AbstractMap, only Map.
// Otherwise it needs to inject additional schema for a RestrictedType source of the super type. Could be done, but do we need it?
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is
// AbstractMap, only Map. Otherwise it needs to inject additional schema for a RestrictedType source of the
// super type. Could be done, but do we need it?
for (customSerializer in customSerializers) {
if (customSerializer.isSerializerFor(clazz)) {
val declaredSuperClass = declaredType.asClass()?.superclass
@ -258,7 +261,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
} else {
// Make a subclass serializer for the subclass and return that...
@Suppress("UNCHECKED_CAST")
return CustomSerializer.SubClass<Any>(clazz, customSerializer as CustomSerializer<Any>)
return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer<Any>)
}
}
}
@ -277,7 +280,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
(!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz))
// Recursively check the class, interfaces and superclasses for our annotation.
internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
private fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
return type.isAnnotationPresent(CordaSerializable::class.java) ||
type.interfaces.any { hasAnnotationInHierarchy(it) }
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
@ -334,7 +337,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
}
is ParameterizedType -> "${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
is WildcardType -> "Any"
is WildcardType -> "?"
is TypeVariable<*> -> "?"
else -> throw NotSerializableException("Unable to render type $type to a string.")
}

View File

@ -1,5 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -9,12 +10,13 @@ import java.lang.reflect.Type
* want converting back to that singleton instance on the receiving JVM.
*/
class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer<Any> {
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
private val interfaces = interfacesForSerialization(type, factory)
private fun generateProvides(): List<String> = interfaces.map { it.typeName }
internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor, null), emptyList())
internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation)

View File

@ -0,0 +1,36 @@
package net.corda.nodeapi.internal.serialization.amqp;
import org.junit.Test;
import net.corda.nodeapi.internal.serialization.AllWhitelist;
import net.corda.core.serialization.SerializedBytes;
import java.io.NotSerializableException;
public class JavaSerialiseEnumTests {
public enum Bras {
TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED
}
private static class Bra {
private final Bras bra;
private Bra(Bras bra) {
this.bra = bra;
}
public Bras getBra() {
return this.bra;
}
}
@Test
public void testJavaConstructorAnnotations() throws NotSerializableException {
Bra bra = new Bra(Bras.UNDERWIRE);
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
SerializationOutput ser = new SerializationOutput(factory1);
SerializedBytes<Object> bytes = ser.serialize(bra);
}
}

View File

@ -25,15 +25,17 @@ public class JavaSerializationOutputTests {
}
@ConstructorForDeserialization
public Foo(String fred, int count) {
private Foo(String fred, int count) {
this.bob = fred;
this.count = count;
}
@SuppressWarnings("unused")
public String getFred() {
return bob;
}
@SuppressWarnings("unused")
public int getCount() {
return count;
}
@ -61,15 +63,17 @@ public class JavaSerializationOutputTests {
private final String bob;
private final int count;
public UnAnnotatedFoo(String fred, int count) {
private UnAnnotatedFoo(String fred, int count) {
this.bob = fred;
this.count = count;
}
@SuppressWarnings("unused")
public String getFred() {
return bob;
}
@SuppressWarnings("unused")
public int getCount() {
return count;
}
@ -97,7 +101,7 @@ public class JavaSerializationOutputTests {
private final String fred;
private final Integer count;
public BoxedFoo(String fred, Integer count) {
private BoxedFoo(String fred, Integer count) {
this.fred = fred;
this.count = count;
}
@ -134,7 +138,7 @@ public class JavaSerializationOutputTests {
private final String fred;
private final Integer count;
public BoxedFooNotNull(String fred, Integer count) {
private BoxedFooNotNull(String fred, Integer count) {
this.fred = fred;
this.count = count;
}

View File

@ -10,19 +10,18 @@ import net.corda.core.internal.declaredField
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.serialization.*
import net.corda.core.serialization.SerializationDefaults.P2P_CONTEXT
import net.corda.core.serialization.SerializationFactory
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ByteSequence
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.internal.serialization.AMQP_ENABLED
import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl
import net.corda.nodeapi.internal.serialization.WireTransactionSerializer
import net.corda.nodeapi.internal.serialization.attachmentsClassLoaderEnabledPropertyName
import net.corda.nodeapi.internal.serialization.withTokenContext
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.MEGA_CORP
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.kryoSpecific
import net.corda.testing.node.MockAttachmentStorage
import org.apache.commons.io.IOUtils
import org.junit.Assert
@ -52,7 +51,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
private fun SerializationContext.withAttachmentStorage(attachmentStorage: AttachmentStorage): SerializationContext {
val serviceHub = mock<ServiceHub>()
whenever(serviceHub.attachments).thenReturn(attachmentStorage)
return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(WireTransactionSerializer.attachmentsClassLoaderEnabled, true)
return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(attachmentsClassLoaderEnabledPropertyName, true)
}
}
@ -224,7 +223,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val context = P2P_CONTEXT.withClassLoader(cl).withWhitelisted(contract.javaClass)
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl).withWhitelisted(contract.javaClass)
val state2 = bytes.deserialize(context = context)
assertTrue(state2.javaClass.classLoader is AttachmentsClassLoader)
assertNotNull(state2)
@ -240,7 +239,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
assertNotNull(data.contract)
val context2 = P2P_CONTEXT.withWhitelisted(data.contract.javaClass)
val context2 = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(data.contract.javaClass)
val bytes = data.serialize(context = context2)
@ -252,7 +251,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val context = P2P_CONTEXT.withClassLoader(cl).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl))
val context = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl))
val state2 = bytes.deserialize(context = context)
assertEquals(cl, state2.contract.javaClass.classLoader)
@ -261,7 +260,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
// We should be able to load same class from a different class loader and have them be distinct.
val cl2 = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
val context3 = P2P_CONTEXT.withClassLoader(cl2).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl2))
val context3 = SerializationFactory.defaultFactory.defaultContext.withClassLoader(cl2).withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl2))
val state3 = bytes.deserialize(context = context3)
assertEquals(cl2, state3.contract.javaClass.classLoader)
@ -313,7 +312,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
val contract = contractClass.newInstance() as DummyContractBackdoor
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
val storage = MockAttachmentStorage()
val context = P2P_CONTEXT.withWhitelisted(contract.javaClass)
val context = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(contract.javaClass)
.withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract\$State", true, child))
.withWhitelisted(Class.forName("net.corda.contracts.isolated.AnotherDummyContract\$Commands\$Create", true, child))
.withAttachmentStorage(storage)
@ -332,8 +331,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
}
@Test
// Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not
fun `test deserialize of WireTransaction where contract cannot be found`() = kryoSpecific {
fun `test deserialize of WireTransaction where contract cannot be found`() = kryoSpecific<AttachmentClassLoaderTests>("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") {
val child = ClassLoaderForTests()
val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor
@ -348,13 +346,13 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
val wireTransaction = tx.toWireTransaction()
wireTransaction.serialize(context = P2P_CONTEXT.withAttachmentStorage(storage))
wireTransaction.serialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(storage))
}
// use empty attachmentStorage
val e = assertFailsWith(MissingAttachmentsException::class) {
val mockAttStorage = MockAttachmentStorage()
bytes.deserialize(context = P2P_CONTEXT.withAttachmentStorage(mockAttStorage))
bytes.deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentStorage(mockAttStorage))
if(mockAttStorage.openAttachment(attachmentRef) == null) {
throw MissingAttachmentsException(listOf(attachmentRef))
@ -363,9 +361,20 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() {
assertEquals(attachmentRef, e.ids.single())
}
private fun kryoSpecific(function: () -> Unit) = if(!AMQP_ENABLED) {
function()
} else {
loggerFor<AttachmentClassLoaderTests>().info("Ignoring Kryo specific test")
@Test
fun `test loading a class from attachment during deserialization`() {
val child = ClassLoaderForTests()
val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child)
val contract = contractClass.newInstance() as DummyContractBackdoor
val storage = MockAttachmentStorage()
val attachmentRef = importJar(storage)
val outboundContext = SerializationFactory.defaultFactory.defaultContext.withClassLoader(child)
// We currently ignore annotations in attachments, so manually whitelist.
val inboundContext = SerializationFactory.defaultFactory.defaultContext.withWhitelisted(contract.javaClass).withAttachmentStorage(storage).withAttachmentsClassLoader(listOf(attachmentRef))
// Serialize with custom context to avoid populating the default context with the specially loaded class
val serialized = contract.serialize(context = outboundContext)
// Then deserialize with the attachment class loader associated with the attachment
serialized.deserialize(context = inboundContext)
}
}

View File

@ -6,7 +6,7 @@ import com.typesafe.config.ConfigRenderOptions.defaults
import com.typesafe.config.ConfigValueFactory
import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.testing.getTestX509Name
import net.corda.core.utilities.getX500Name
import org.assertj.core.api.Assertions.assertThat
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
@ -112,7 +112,7 @@ class ConfigParsingTest {
@Test
fun x500Name() {
testPropertyType<X500NameData, X500NameListData, X500Name>(getTestX509Name("Mock Party"), getTestX509Name("Mock Party 2"), valuesToString = true)
testPropertyType<X500NameData, X500NameListData, X500Name>(getX500Name(O = "Mock Party", L = "London", C = "GB"), getX500Name(O = "Mock Party 2", L = "London", C = "GB"), valuesToString = true)
}
@Test

View File

@ -76,7 +76,7 @@ class DefaultSerializableSerializer : Serializer<DefaultSerializable>() {
}
class CordaClassResolverTests {
val factory: SerializationFactory = object : SerializationFactory {
val factory: SerializationFactory = object : SerializationFactory() {
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}

View File

@ -0,0 +1,58 @@
package net.corda.nodeapi.internal.serialization
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.amqpSpecific
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.NotSerializableException
import kotlin.test.assertEquals
class ListsSerializationTest : TestDependencyInjectionBase() {
@Test
fun `check list can be serialized as root of serialization graph`() {
assertEqualAfterRoundTripSerialization(emptyList<Int>())
assertEqualAfterRoundTripSerialization(listOf(1))
assertEqualAfterRoundTripSerialization(listOf(1, 2))
}
@Test
fun `check list can be serialized as part of SessionData`() {
run {
val sessionData = SessionData(123, listOf(1))
assertEqualAfterRoundTripSerialization(sessionData)
}
run {
val sessionData = SessionData(123, listOf(1, 2))
assertEqualAfterRoundTripSerialization(sessionData)
}
}
@CordaSerializable
data class WrongPayloadType(val payload: ArrayList<Int>)
@Test
fun `check throws for forbidden declared type`() = amqpSpecific<ListsSerializationTest>("Such exceptions are not expected in Kryo mode.") {
val payload = ArrayList<Int>()
payload.add(1)
payload.add(2)
val wrongPayloadType = WrongPayloadType(payload)
Assertions.assertThatThrownBy { wrongPayloadType.serialize() }
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType")
}
}
internal inline fun<reified T : Any> assertEqualAfterRoundTripSerialization(obj: T) {
val serializedForm: SerializedBytes<T> = obj.serialize()
val deserializedInstance = serializedForm.deserialize()
assertEquals(obj, deserializedInstance)
}

View File

@ -0,0 +1,57 @@
package net.corda.nodeapi.internal.serialization
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.serialize
import net.corda.node.services.statemachine.SessionData
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.amqpSpecific
import org.assertj.core.api.Assertions
import org.junit.Test
import org.bouncycastle.asn1.x500.X500Name
import java.io.NotSerializableException
class MapsSerializationTest : TestDependencyInjectionBase() {
private val smallMap = mapOf("foo" to "bar", "buzz" to "bull")
@Test
fun `check EmptyMap serialization`() = amqpSpecific<MapsSerializationTest>("kotlin.collections.EmptyMap is not enabled for Kryo serialization") {
assertEqualAfterRoundTripSerialization(emptyMap<Any, Any>())
}
@Test
fun `check Map can be root of serialization graph`() {
assertEqualAfterRoundTripSerialization(smallMap)
}
@Test
fun `check list can be serialized as part of SessionData`() {
val sessionData = SessionData(123, smallMap)
assertEqualAfterRoundTripSerialization(sessionData)
}
@CordaSerializable
data class WrongPayloadType(val payload: HashMap<String, String>)
@Test
fun `check throws for forbidden declared type`() = amqpSpecific<ListsSerializationTest>("Such exceptions are not expected in Kryo mode.") {
val payload = HashMap<String, String>(smallMap)
val wrongPayloadType = WrongPayloadType(payload)
Assertions.assertThatThrownBy { wrongPayloadType.serialize() }
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType")
}
@CordaSerializable
data class MyKey(val keyContent: Double)
@CordaSerializable
data class MyValue(val valueContent: X500Name)
@Test
fun `check map serialization works with custom types`() {
val myMap = mapOf(
MyKey(1.0) to MyValue(X500Name("CN=one")),
MyKey(10.0) to MyValue(X500Name("CN=ten")))
assertEqualAfterRoundTripSerialization(myMap)
}
}

View File

@ -1,8 +1,10 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.SerializedBytes
import org.apache.qpid.proton.codec.Data
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
import java.io.NotSerializableException
fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())
@ -17,3 +19,18 @@ class TestSerializationOutput(
super.writeSchema(schema, data)
}
}
fun testName(): String = Thread.currentThread().stackTrace[2].methodName
data class BytesAndSchema<T : Any>(val obj: SerializedBytes<T>, val schema: Schema)
// Extension for the serialize routine that returns the scheme encoded into the
// bytes as well as the bytes for simple testing
@Throws(NotSerializableException::class)
fun <T : Any> SerializationOutput.serializeAndReturnSchema(obj: T): BytesAndSchema<T> {
try {
return BytesAndSchema(_serialize(obj), Schema(schemaHistory.toList()))
} finally {
andFinally()
}
}

View File

@ -7,11 +7,9 @@ import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
class DeserializeAndReturnEnvelopeTests {
fun testName(): String = Thread.currentThread().stackTrace[2].methodName
// the 'this' reference means we can't just move this to the common test utils
@Suppress("NOTHING_TO_INLINE")
inline fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
@Test
fun oneType() {

View File

@ -1,6 +1,8 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.NotSerializableException
import java.util.*
class DeserializeCollectionTests {
@ -11,7 +13,7 @@ class DeserializeCollectionTests {
private const val VERBOSE = false
}
val sf = testDefaultFactory()
private val sf = testDefaultFactory()
@Test
fun mapTest() {
@ -57,7 +59,7 @@ class DeserializeCollectionTests {
DeserializationInput(sf).deserialize(serialisedBytes)
}
@Test(expected=java.io.NotSerializableException::class)
@Test
fun dictionaryTest() {
data class C(val c: Dictionary<String, Int>)
val v : Hashtable<String, Int> = Hashtable()
@ -66,10 +68,11 @@ class DeserializeCollectionTests {
val c = C(v)
// expected to throw
TestSerializationOutput(VERBOSE, sf).serialize(c)
Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) }
.isInstanceOf(IllegalArgumentException::class.java).hasMessageContaining("Unable to serialise deprecated type class java.util.Dictionary.")
}
@Test(expected=java.lang.IllegalArgumentException::class)
@Test
fun hashtableTest() {
data class C(val c: Hashtable<String, Int>)
val v : Hashtable<String, Int> = Hashtable()
@ -78,24 +81,27 @@ class DeserializeCollectionTests {
val c = C(v)
// expected to throw
TestSerializationOutput(VERBOSE, sf).serialize(c)
Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) }
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType")
}
@Test(expected=java.lang.IllegalArgumentException::class)
@Test
fun hashMapTest() {
data class C(val c : HashMap<String, Int>)
val c = C (HashMap (mapOf("A" to 1, "B" to 2)))
// expect this to throw
TestSerializationOutput(VERBOSE, sf).serialize(c)
Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) }
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType")
}
@Test(expected=java.lang.IllegalArgumentException::class)
@Test
fun weakHashMapTest() {
data class C(val c : WeakHashMap<String, Int>)
val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2)))
TestSerializationOutput(VERBOSE, sf).serialize(c)
Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) }
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive map type for declaredType")
}
@Test

View File

@ -0,0 +1,189 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.junit.Test
import java.time.DayOfWeek
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import java.io.File
import java.io.NotSerializableException
import net.corda.core.serialization.SerializedBytes
class EnumTests {
enum class Bras {
TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED
}
// The state of the OldBras enum when the tests in changedEnum1 were serialised
// - use if the test file needs regenerating
//enum class OldBras {
// TSHIRT, UNDERWIRE, PUSHUP, BRALETTE
//}
// the new state, SPACER has been added to change the ordinality
enum class OldBras {
SPACER, TSHIRT, UNDERWIRE, PUSHUP, BRALETTE
}
// The state of the OldBras2 enum when the tests in changedEnum2 were serialised
// - use if the test file needs regenerating
//enum class OldBras2 {
// TSHIRT, UNDERWIRE, PUSHUP, BRALETTE
//}
// the new state, note in the test we serialised with value UNDERWIRE so the spacer
// occuring after this won't have changed the ordinality of our serialised value
// and thus should still be deserialisable
enum class OldBras2 {
TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2
}
enum class BrasWithInit (val someList: List<Int>) {
TSHIRT(emptyList()),
UNDERWIRE(listOf(1, 2, 3)),
PUSHUP(listOf(100, 200)),
BRALETTE(emptyList())
}
private val brasTestName = "${this.javaClass.name}\$Bras"
companion object {
/**
* If you want to see the schema encoded into the envelope after serialisation change this to true
*/
private const val VERBOSE = false
}
@Suppress("NOTHING_TO_INLINE")
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
private val sf1 = testDefaultFactory()
@Test
fun serialiseSimpleTest() {
data class C(val c: Bras)
val schema = TestSerializationOutput(VERBOSE, sf1).serializeAndReturnSchema(C(Bras.UNDERWIRE)).schema
assertEquals(2, schema.types.size)
val schema_c = schema.types.find { it.name == classTestName("C") } as CompositeType
val schema_bras = schema.types.find { it.name == brasTestName } as RestrictedType
assertNotNull(schema_c)
assertNotNull(schema_bras)
assertEquals(1, schema_c.fields.size)
assertEquals("c", schema_c.fields.first().name)
assertEquals(brasTestName, schema_c.fields.first().type)
assertEquals(8, schema_bras.choices.size)
Bras.values().forEach {
val bra = it
assertNotNull (schema_bras.choices.find { it.name == bra.name })
}
}
@Test
fun deserialiseSimpleTest() {
data class C(val c: Bras)
val objAndEnvelope = DeserializationInput(sf1).deserializeAndReturnEnvelope(
TestSerializationOutput(VERBOSE, sf1).serialize(C(Bras.UNDERWIRE)))
val obj = objAndEnvelope.obj
val schema = objAndEnvelope.envelope.schema
assertEquals(2, schema.types.size)
val schema_c = schema.types.find { it.name == classTestName("C") } as CompositeType
val schema_bras = schema.types.find { it.name == brasTestName } as RestrictedType
assertEquals(1, schema_c.fields.size)
assertEquals("c", schema_c.fields.first().name)
assertEquals(brasTestName, schema_c.fields.first().type)
assertEquals(8, schema_bras.choices.size)
Bras.values().forEach {
val bra = it
assertNotNull (schema_bras.choices.find { it.name == bra.name })
}
// Test the actual deserialised object
assertEquals(obj.c, Bras.UNDERWIRE)
}
@Test
fun multiEnum() {
data class Support (val top: Bras, val day : DayOfWeek)
data class WeeklySupport (val tops: List<Support>)
val week = WeeklySupport (listOf(
Support (Bras.PUSHUP, DayOfWeek.MONDAY),
Support (Bras.UNDERWIRE, DayOfWeek.WEDNESDAY),
Support (Bras.PADDED, DayOfWeek.SUNDAY)))
val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(week))
assertEquals(week.tops[0].top, obj.tops[0].top)
assertEquals(week.tops[0].day, obj.tops[0].day)
assertEquals(week.tops[1].top, obj.tops[1].top)
assertEquals(week.tops[1].day, obj.tops[1].day)
assertEquals(week.tops[2].top, obj.tops[2].top)
assertEquals(week.tops[2].day, obj.tops[2].day)
}
@Test
fun enumWithInit() {
data class C(val c: BrasWithInit)
val c = C (BrasWithInit.PUSHUP)
val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(c))
assertEquals(c.c, obj.c)
}
@Test(expected = NotSerializableException::class)
fun changedEnum1() {
val path = EnumTests::class.java.getResource("EnumTests.changedEnum1")
val f = File(path.toURI())
data class C (val a: OldBras)
// Original version of the class for the serialised version of this class
//
// val a = OldBras.TSHIRT
// val sc = SerializationOutput(sf1).serialize(C(a))
// f.writeBytes(sc.bytes)
// println(path)
val sc2 = f.readBytes()
// we expect this to throw
DeserializationInput(sf1).deserialize(SerializedBytes<C>(sc2))
}
@Test(expected = NotSerializableException::class)
fun changedEnum2() {
val path = EnumTests::class.java.getResource("EnumTests.changedEnum2")
val f = File(path.toURI())
data class C (val a: OldBras2)
// DO NOT CHANGE THIS, it's important we serialise with a value that doesn't
// change position in the upated enum class
// Original version of the class for the serialised version of this class
//
// val a = OldBras2.UNDERWIRE
// val sc = SerializationOutput(sf1).serialize(C(a))
// f.writeBytes(sc.bytes)
// println(path)
val sc2 = f.readBytes()
// we expect this to throw
DeserializationInput(sf1).deserialize(SerializedBytes<C>(sc2))
}
}

View File

@ -1,8 +1,7 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializedBytes
import org.junit.Test
import java.io.File
import java.io.NotSerializableException

View File

@ -12,15 +12,17 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.nodeapi.RPCException
import net.corda.nodeapi.internal.serialization.AbstractAMQPSerializationScheme
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.nodeapi.internal.serialization.EmptyWhitelist
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive
import net.corda.nodeapi.internal.serialization.AllWhitelist
import net.corda.testing.BOB_IDENTITY
import net.corda.testing.MEGA_CORP
import net.corda.testing.MEGA_CORP_PUBKEY
import org.apache.qpid.proton.amqp.*
import org.apache.qpid.proton.codec.DecoderImpl
import org.apache.qpid.proton.codec.EncoderImpl
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertSame
import org.junit.Ignore
import org.junit.Test
import java.io.IOException
@ -29,9 +31,7 @@ import java.math.BigDecimal
import java.nio.ByteBuffer
import java.time.*
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalUnit
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@ -140,13 +140,13 @@ class SerializationOutputTests {
data class PolymorphicProperty(val foo: FooInterface?)
private fun serdes(obj: Any,
private inline fun<reified T : Any> serdes(obj: T,
factory: SerializerFactory = SerializerFactory (
AllWhitelist, ClassLoader.getSystemClassLoader()),
freshDeserializationFactory: SerializerFactory = SerializerFactory(
AllWhitelist, ClassLoader.getSystemClassLoader()),
expectedEqual: Boolean = true,
expectDeserializedEqual: Boolean = true): Any {
expectDeserializedEqual: Boolean = true): T {
val ser = SerializationOutput(factory)
val bytes = ser.serialize(obj)
@ -158,6 +158,7 @@ class SerializationOutputTests {
this.register(CompositeType.DESCRIPTOR, CompositeType.Companion)
this.register(Choice.DESCRIPTOR, Choice.Companion)
this.register(RestrictedType.DESCRIPTOR, RestrictedType.Companion)
this.register(ReferencedObject.DESCRIPTOR, ReferencedObject.Companion)
}
EncoderImpl(decoder)
decoder.setByteBuffer(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8))
@ -265,7 +266,7 @@ class SerializationOutputTests {
serdes(obj)
}
@Test(expected = NotSerializableException::class)
@Test
fun `test top level list array`() {
val obj = arrayOf(listOf("Fred", "Ginger"), listOf("Rogers", "Hammerstein"))
serdes(obj)
@ -436,7 +437,7 @@ class SerializationOutputTests {
throw IllegalStateException("Layer 2", t)
}
} catch(t: Throwable) {
val desThrowable = serdes(t, factory, factory2, false) as Throwable
val desThrowable = serdes(t, factory, factory2, false)
assertSerializedThrowableEquivalent(t, desThrowable)
}
}
@ -468,7 +469,7 @@ class SerializationOutputTests {
throw e
}
} catch(t: Throwable) {
val desThrowable = serdes(t, factory, factory2, false) as Throwable
val desThrowable = serdes(t, factory, factory2, false)
assertSerializedThrowableEquivalent(t, desThrowable)
}
}
@ -538,7 +539,6 @@ class SerializationOutputTests {
AbstractAMQPSerializationScheme.registerCustomSerializers(factory2)
val desState = serdes(state, factory, factory2, expectedEqual = false, expectDeserializedEqual = false)
assertTrue(desState is TransactionState<*>)
assertTrue((desState as TransactionState<*>).data is FooState)
assertTrue(desState.notary == state.notary)
assertTrue(desState.encumbrance == state.encumbrance)
@ -763,4 +763,107 @@ class SerializationOutputTests {
val obj = StateRef(SecureHash.randomSHA256(), 0)
serdes(obj, factory, factory2)
}
}
interface Container
data class SimpleContainer(val one: String, val another: String) : Container
data class ParentContainer(val left: SimpleContainer, val right: Container)
@Test
fun `test object referenced multiple times`() {
val simple = SimpleContainer("Fred", "Ginger")
val parentContainer = ParentContainer(simple, simple)
assertSame(parentContainer.left, parentContainer.right)
val parentCopy = serdes(parentContainer)
assertSame(parentCopy.left, parentCopy.right)
}
data class TestNode(val content: String, val children: MutableCollection<TestNode> = ArrayList())
@Test
@Ignore("Ignored due to cyclic graphs not currently supported by AMQP serialization")
fun `test serialization of cyclic graph`() {
val nodeA = TestNode("A")
val nodeB = TestNode("B", ArrayList(Arrays.asList(nodeA)))
nodeA.children.add(nodeB)
// Also blows with StackOverflow error
assertTrue(nodeB.hashCode() > 0)
val bCopy = serdes(nodeB)
assertEquals("A", bCopy.children.single().content)
}
data class Bob(val byteArrays: List<ByteArray>)
@Ignore("Causes DeserializedParameterizedType.make() to fail")
@Test
fun `test list of byte arrays`() {
val a = ByteArray(1)
val b = ByteArray(2)
val obj = Bob(listOf(a, b, a))
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
serdes(obj, factory, factory2)
}
data class Vic(val a: List<String>, val b: List<String>)
@Test
fun `test generics ignored from graph logic`() {
val a = listOf("a", "b")
val obj = Vic(a, a)
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
val objCopy = serdes(obj, factory, factory2)
assertSame(objCopy.a, objCopy.b)
}
data class Spike private constructor(val a: String) {
constructor() : this("a")
}
@Test
fun `test private constructor`() {
val obj = Spike()
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
serdes(obj, factory, factory2)
}
data class BigDecimals(val a: BigDecimal, val b: BigDecimal)
@Test
fun `test toString custom serializer`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
val obj = BigDecimals(BigDecimal.TEN, BigDecimal.TEN)
val objCopy = serdes(obj, factory, factory2)
assertEquals(objCopy.a, objCopy.b)
}
data class ByteArrays(val a: ByteArray, val b: ByteArray)
@Test
fun `test byte arrays not reference counted`() {
val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
val bytes = ByteArray(1)
val obj = ByteArrays(bytes, bytes)
val objCopy = serdes(obj, factory, factory2, false, false)
assertNotSame(objCopy.a, objCopy.b)
}
}

View File

@ -0,0 +1,34 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class SerializeAndReturnSchemaTest {
// the 'this' reference means we can't just move this to the common test utils
@Suppress("NOTHING_TO_INLINE")
inline private fun classTestName(clazz: String) = "${this.javaClass.name}\$${testName()}\$$clazz"
val factory = testDefaultFactory()
// just a simple test to verify the internal test extension for serialize does
// indeed give us the correct schema back. This is more useful in support of other
// tests rather than by itself but for those to be reliable this also needs
// testing
@Test
fun getSchema() {
data class C(val a: Int, val b: Int)
val a = 1
val b = 2
val sc = SerializationOutput(factory).serializeAndReturnSchema(C(a, b))
assertEquals(1, sc.schema.types.size)
assertEquals(classTestName("C"), sc.schema.types.first().name)
assertTrue(sc.schema.types.first() is CompositeType)
assertEquals(2, (sc.schema.types.first() as CompositeType).fields.size)
assertNotNull((sc.schema.types.first() as CompositeType).fields.find { it.name == "a" })
assertNotNull((sc.schema.types.first() as CompositeType).fields.find { it.name == "b" })
}
}