mirror of
https://github.com/corda/corda.git
synced 2025-06-17 06:38:21 +00:00
Merge remote-tracking branch 'open/master' into aslemmer-enterprise-merge-september-8
This commit is contained in:
@ -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
|
||||
|
@ -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.<username>.<nonce>".
|
||||
*/
|
||||
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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)) {
|
||||
|
@ -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.")
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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.")
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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" })
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user