mirror of
https://github.com/corda/corda.git
synced 2025-06-12 20:28:18 +00:00
Moved the serialisation logic in nodeapi.internal.serialization into its own module: serialization (#3179)
node-api now depends on this module and upcoming changes will use this as well rather than having to depend on node-api. EnumEvolveTests.deserializeWithRename and EnumEvolveTests.multiOperations are temporarily ignored since their test resources can't be regenerated due to bugs.
This commit is contained in:
@ -1,113 +0,0 @@
|
||||
package net.corda.nodeapi.internal
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.isUploaderTrusted
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
import java.net.URLStreamHandler
|
||||
import java.security.CodeSigner
|
||||
import java.security.CodeSource
|
||||
import java.security.SecureClassLoader
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A custom ClassLoader that knows how to load classes from a set of attachments. The attachments themselves only
|
||||
* need to provide JAR streams, and so could be fetched from a database, local disk, etc. Constructing an
|
||||
* AttachmentsClassLoader is somewhat expensive, as every attachment is scanned to ensure that there are no overlapping
|
||||
* file paths.
|
||||
*/
|
||||
class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader = ClassLoader.getSystemClassLoader()) : SecureClassLoader(parent) {
|
||||
private val pathsToAttachments = HashMap<String, Attachment>()
|
||||
private val idsToAttachments = HashMap<SecureHash, Attachment>()
|
||||
|
||||
@CordaSerializable
|
||||
class OverlappingAttachments(val path: String) : Exception() {
|
||||
override fun toString() = "Multiple attachments define a file at path $path"
|
||||
}
|
||||
|
||||
init {
|
||||
require(attachments.mapNotNull { it as? ContractAttachment }.all { isUploaderTrusted(it.uploader) }) {
|
||||
"Attempting to load Contract Attachments downloaded from the network"
|
||||
}
|
||||
|
||||
for (attachment in attachments) {
|
||||
attachment.openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
val entry = jar.nextJarEntry ?: break
|
||||
|
||||
// We already verified that paths are not strange/game playing when we inserted the attachment
|
||||
// into the storage service. So we don't need to repeat it here.
|
||||
//
|
||||
// We forbid files that differ only in case, or path separator to avoid issues for Windows/Mac developers where the
|
||||
// filesystem tries to be case insensitive. This may break developers who attempt to use ProGuard.
|
||||
//
|
||||
// Also convert to Unix path separators as all resource/class lookups will expect this.
|
||||
val path = entry.name.toLowerCase().replace('\\', '/')
|
||||
if (path in pathsToAttachments)
|
||||
throw OverlappingAttachments(path)
|
||||
pathsToAttachments[path] = attachment
|
||||
}
|
||||
}
|
||||
idsToAttachments[attachment.id] = attachment
|
||||
}
|
||||
}
|
||||
|
||||
// Example: attachment://0b4fc1327f3bbebf1bfe98330ea402ae035936c3cb6da9bd3e26eeaa9584e74d/some/file.txt
|
||||
//
|
||||
// We have to provide a fake stream handler to satisfy the URL class that the scheme is known. But it's not
|
||||
// a real scheme and we don't register it. It's just here to ensure that there aren't codepaths that could
|
||||
// lead to data loading that we don't control right here in this class (URLs can have evil security properties!)
|
||||
private val fakeStreamHandler = object : URLStreamHandler() {
|
||||
override fun openConnection(u: URL?): URLConnection? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Attachment.toURL(path: String?) = URL(null, "attachment://$id/" + (path ?: ""), fakeStreamHandler)
|
||||
|
||||
override fun findClass(name: String): Class<*> {
|
||||
val path = name.replace('.', '/').toLowerCase() + ".class"
|
||||
val attachment = pathsToAttachments[path] ?: throw ClassNotFoundException(name)
|
||||
val stream = ByteArrayOutputStream()
|
||||
try {
|
||||
attachment.extractFile(path, stream)
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw ClassNotFoundException(name)
|
||||
}
|
||||
val bytes = stream.toByteArray()
|
||||
// We don't attempt to propagate signatures from the JAR into the codesource, because our sandbox does not
|
||||
// depend on external policy files to specify what it can do, so the data wouldn't be useful.
|
||||
val codesource = CodeSource(attachment.toURL(null), emptyArray<CodeSigner>())
|
||||
// TODO: Define an empty ProtectionDomain to start enforcing the standard Java sandbox.
|
||||
// The standard Java sandbox is insufficient for our needs and a much more sophisticated sandboxing
|
||||
// ClassLoader will appear here in future, but it can't hurt to use the default one too: defence in depth!
|
||||
return defineClass(name, bytes, 0, bytes.size, codesource)
|
||||
}
|
||||
|
||||
override fun findResource(name: String): URL? {
|
||||
val attachment = pathsToAttachments[name.toLowerCase()] ?: return null
|
||||
return attachment.toURL(name)
|
||||
}
|
||||
|
||||
override fun getResourceAsStream(name: String): InputStream? {
|
||||
val url = getResource(name) ?: return null // May check parent classloaders, for example.
|
||||
if (url.protocol != "attachment") return null
|
||||
val attachment = idsToAttachments[SecureHash.parse(url.host)] ?: return null
|
||||
val path = url.path?.substring(1) ?: return null // Chop off the leading slash.
|
||||
return try {
|
||||
val stream = ByteArrayOutputStream()
|
||||
attachment.extractFile(path, stream)
|
||||
stream.toByteArray().inputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,13 +23,13 @@ import net.corda.nodeapi.internal.ContractsJarFile
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AbstractAMQPSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
|
||||
import net.corda.serialization.internal.CordaSerializationMagic
|
||||
import net.corda.serialization.internal.SerializationFactoryImpl
|
||||
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme
|
||||
import net.corda.serialization.internal.amqp.amqpMagic
|
||||
import net.corda.serialization.internal.kryo.AbstractKryoSerializationScheme
|
||||
import net.corda.serialization.internal.kryo.kryoMagic
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
|
@ -1,141 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import sun.misc.Unsafe
|
||||
import sun.security.util.Password
|
||||
import java.io.*
|
||||
import java.lang.invoke.*
|
||||
import java.lang.reflect.AccessibleObject
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Parameter
|
||||
import java.lang.reflect.ReflectPermission
|
||||
import java.net.DatagramSocket
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.net.URLConnection
|
||||
import java.security.AccessController
|
||||
import java.security.KeyStore
|
||||
import java.security.Permission
|
||||
import java.security.Provider
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.logging.Handler
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.collections.HashSet
|
||||
import kotlin.collections.LinkedHashSet
|
||||
|
||||
/**
|
||||
* This is a [ClassWhitelist] implementation where everything is whitelisted except for blacklisted classes and interfaces.
|
||||
* In practice, as flows are arbitrary code in which it is convenient to do many things,
|
||||
* we can often end up pulling in a lot of objects that do not make sense to put in a checkpoint.
|
||||
* Thus, by blacklisting classes/interfaces we don't expect to be serialised, we can better handle/monitor the aforementioned behaviour.
|
||||
* Inheritance works for blacklisted items, but one can specifically exclude classes from blacklisting as well.
|
||||
* Note: Custom serializer registration trumps white/black lists. So if a given type has a custom serializer and has its name
|
||||
* in the blacklist - it will still be serialized as specified by custom serializer.
|
||||
* For more details, see [net.corda.nodeapi.internal.serialization.CordaClassResolver.getRegistration]
|
||||
*/
|
||||
object AllButBlacklisted : ClassWhitelist {
|
||||
|
||||
private val blacklistedClasses = hashSetOf<String>(
|
||||
|
||||
// Known blacklisted classes.
|
||||
Thread::class.java.name,
|
||||
HashSet::class.java.name,
|
||||
HashMap::class.java.name,
|
||||
WeakHashMap::class.java.name,
|
||||
Dictionary::class.java.name, // Deprecated (marked obsolete) in the jdk
|
||||
Hashtable::class.java.name, // see [Dictionary]
|
||||
ClassLoader::class.java.name,
|
||||
Handler::class.java.name, // MemoryHandler, StreamHandler
|
||||
Runtime::class.java.name,
|
||||
Unsafe::class.java.name,
|
||||
ZipFile::class.java.name,
|
||||
Provider::class.java.name,
|
||||
SecurityManager::class.java.name,
|
||||
Random::class.java.name,
|
||||
|
||||
// Known blacklisted interfaces.
|
||||
Connection::class.java.name,
|
||||
// TODO: AutoCloseable::class.java.name,
|
||||
|
||||
// java.security.
|
||||
KeyStore::class.java.name,
|
||||
Password::class.java.name,
|
||||
AccessController::class.java.name,
|
||||
Permission::class.java.name,
|
||||
|
||||
// java.net.
|
||||
DatagramSocket::class.java.name,
|
||||
ServerSocket::class.java.name,
|
||||
Socket::class.java.name,
|
||||
URLConnection::class.java.name,
|
||||
// TODO: add more from java.net.
|
||||
|
||||
// java.io.
|
||||
Console::class.java.name,
|
||||
File::class.java.name,
|
||||
FileDescriptor::class.java.name,
|
||||
FilePermission::class.java.name,
|
||||
RandomAccessFile::class.java.name,
|
||||
Reader::class.java.name,
|
||||
Writer::class.java.name,
|
||||
// TODO: add more from java.io.
|
||||
|
||||
// java.lang.invoke classes.
|
||||
CallSite::class.java.name, // for all CallSites eg MutableCallSite, VolatileCallSite etc.
|
||||
LambdaMetafactory::class.java.name,
|
||||
MethodHandle::class.java.name,
|
||||
MethodHandleProxies::class.java.name,
|
||||
MethodHandles::class.java.name,
|
||||
MethodHandles.Lookup::class.java.name,
|
||||
MethodType::class.java.name,
|
||||
SerializedLambda::class.java.name,
|
||||
SwitchPoint::class.java.name,
|
||||
|
||||
// java.lang.invoke interfaces.
|
||||
MethodHandleInfo::class.java.name,
|
||||
|
||||
// java.lang.invoke exceptions.
|
||||
LambdaConversionException::class.java.name,
|
||||
WrongMethodTypeException::class.java.name,
|
||||
|
||||
// java.lang.reflect.
|
||||
AccessibleObject::class.java.name, // For Executable, Field, Method, Constructor.
|
||||
Modifier::class.java.name,
|
||||
Parameter::class.java.name,
|
||||
ReflectPermission::class.java.name
|
||||
// TODO: add more from java.lang.reflect.
|
||||
)
|
||||
|
||||
// Specifically exclude classes from the blacklist,
|
||||
// even if any of their superclasses and/or implemented interfaces are blacklisted.
|
||||
private val forciblyAllowedClasses = hashSetOf<String>(
|
||||
LinkedHashSet::class.java.name,
|
||||
LinkedHashMap::class.java.name,
|
||||
InputStream::class.java.name,
|
||||
BufferedInputStream::class.java.name,
|
||||
Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream").name
|
||||
)
|
||||
|
||||
/**
|
||||
* This implementation supports inheritance; thus, if a superclass or superinterface is blacklisted, so is the input class.
|
||||
*/
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
// Check if excluded.
|
||||
if (type.name !in forciblyAllowedClasses) {
|
||||
// Check if listed.
|
||||
if (type.name in blacklistedClasses)
|
||||
throw IllegalStateException("Class ${type.name} is blacklisted, so it cannot be used in serialization.")
|
||||
// Inheritance check.
|
||||
else {
|
||||
val aMatch = blacklistedClasses.firstOrNull { Class.forName(it).isAssignableFrom(type) }
|
||||
if (aMatch != null) {
|
||||
// TODO: blacklistedClasses += type.name // add it, so checking is faster next time we encounter this class.
|
||||
val matchType = if (Class.forName(aMatch).isInterface) "superinterface" else "superclass"
|
||||
throw IllegalStateException("The $matchType $aMatch of ${type.name} is blacklisted, so it cannot be used in serialization.")
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
@file:JvmName("ByteBufferStreams")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.internal.LazyPool
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.min
|
||||
|
||||
internal val serializeOutputStreamPool = LazyPool(
|
||||
clear = ByteBufferOutputStream::reset,
|
||||
shouldReturnToPool = { it.size() < 256 * 1024 }, // Discard if it grew too large
|
||||
newInstance = { ByteBufferOutputStream(64 * 1024) })
|
||||
|
||||
internal fun <T> byteArrayOutput(task: (ByteBufferOutputStream) -> T): ByteArray {
|
||||
return serializeOutputStreamPool.run { underlying ->
|
||||
task(underlying)
|
||||
underlying.toByteArray() // Must happen after close, to allow ZIP footer to be written for example.
|
||||
}
|
||||
}
|
||||
|
||||
class ByteBufferInputStream(val byteBuffer: ByteBuffer) : InputStream() {
|
||||
@Throws(IOException::class)
|
||||
override fun read(): Int {
|
||||
return if (byteBuffer.hasRemaining()) byteBuffer.get().toInt() else -1
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(b: ByteArray, offset: Int, length: Int): Int {
|
||||
if (offset < 0 || length < 0 || length > b.size - offset) {
|
||||
throw IndexOutOfBoundsException()
|
||||
} else if (length == 0) {
|
||||
return 0
|
||||
} else if (!byteBuffer.hasRemaining()) {
|
||||
return -1
|
||||
}
|
||||
val size = min(length, byteBuffer.remaining())
|
||||
byteBuffer.get(b, offset, size)
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
class ByteBufferOutputStream(size: Int) : ByteArrayOutputStream(size) {
|
||||
companion object {
|
||||
private val ensureCapacity = ByteArrayOutputStream::class.java.getDeclaredMethod("ensureCapacity", Int::class.java).apply {
|
||||
isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> alsoAsByteBuffer(remaining: Int, task: (ByteBuffer) -> T): T {
|
||||
ensureCapacity.invoke(this, count + remaining)
|
||||
val buffer = ByteBuffer.wrap(buf, count, remaining)
|
||||
val result = task(buffer)
|
||||
count = buffer.position()
|
||||
return result
|
||||
}
|
||||
|
||||
fun copyTo(stream: OutputStream) {
|
||||
stream.write(buf, 0, count)
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
@file:JvmName("ClientContexts")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
/*
|
||||
* Serialisation contexts for the client.
|
||||
* These have been refactored into a separate file to prevent
|
||||
* servers from trying to instantiate any of them.
|
||||
*/
|
||||
|
||||
|
||||
val AMQP_RPC_CLIENT_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.RPCClient,
|
||||
null)
|
@ -1,239 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.*
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
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.internal.writer
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.nodeapi.internal.serialization.amqp.hasAnnotationInHierarchy
|
||||
import net.corda.nodeapi.internal.serialization.kryo.ThrowableSerializer
|
||||
import java.io.PrintWriter
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Modifier.isAbstract
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Corda specific class resolver which enables extra customisation for the purposes of serialization using Kryo
|
||||
*/
|
||||
class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() {
|
||||
val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist)
|
||||
|
||||
// These classes are assignment-compatible Java equivalents of Kotlin classes.
|
||||
// The point is that we do not want to send Kotlin types "over the wire" via RPC.
|
||||
private val javaAliases: Map<Class<*>, Class<*>> = mapOf(
|
||||
listOf<Any>().javaClass to Collections.emptyList<Any>().javaClass,
|
||||
setOf<Any>().javaClass to Collections.emptySet<Any>().javaClass,
|
||||
mapOf<Any, Any>().javaClass to Collections.emptyMap<Any, Any>().javaClass
|
||||
)
|
||||
|
||||
private fun typeForSerializationOf(type: Class<*>): Class<*> = javaAliases[type] ?: type
|
||||
|
||||
/** Returns the registration for the specified class, or null if the class is not registered. */
|
||||
override fun getRegistration(type: Class<*>): Registration? {
|
||||
val targetType = typeForSerializationOf(type)
|
||||
return super.getRegistration(targetType) ?: checkClass(targetType)
|
||||
}
|
||||
|
||||
private var whitelistEnabled = true
|
||||
|
||||
fun disableWhitelist() {
|
||||
whitelistEnabled = false
|
||||
}
|
||||
|
||||
fun enableWhitelist() {
|
||||
whitelistEnabled = true
|
||||
}
|
||||
|
||||
private fun checkClass(type: Class<*>): Registration? {
|
||||
// If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking.
|
||||
if (!whitelistEnabled) return null
|
||||
// If array, recurse on element type
|
||||
if (type.isArray) return checkClass(type.componentType)
|
||||
// Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry.
|
||||
if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) return checkClass(type.superclass)
|
||||
// Allow primitives, abstracts and interfaces. Note that we can also create abstract Enum types,
|
||||
// but we don't want to whitelist those here.
|
||||
if (type.isPrimitive || type == Any::class.java || type == String::class.java || (!type.isEnum && isAbstract(type.modifiers))) return null
|
||||
// It's safe to have the Class already, since Kryo loads it with initialisation off.
|
||||
// If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted.
|
||||
// Thus, blacklisting precedes annotation checking.
|
||||
if (!whitelist.hasListed(type) && !checkForAnnotation(type)) {
|
||||
throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun registerImplicit(type: Class<*>): Registration {
|
||||
val targetType = typeForSerializationOf(type)
|
||||
// Is this a Kotlin object? We use our own reflection here rather than .kotlin.objectInstance because Kotlin
|
||||
// reflection won't work for private objects, and can throw exceptions in other circumstances as well.
|
||||
val objectInstance = try {
|
||||
targetType.declaredFields.singleOrNull {
|
||||
it.name == "INSTANCE" &&
|
||||
it.type == type &&
|
||||
Modifier.isStatic(it.modifiers) &&
|
||||
Modifier.isFinal(it.modifiers) &&
|
||||
Modifier.isPublic(it.modifiers)
|
||||
}?.let {
|
||||
it.isAccessible = true
|
||||
type.cast(it.get(null)!!)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
// We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent.
|
||||
val references = kryo.references
|
||||
try {
|
||||
kryo.references = true
|
||||
val serializer = when {
|
||||
objectInstance != null -> KotlinObjectSerializer(objectInstance)
|
||||
kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(targetType) -> // Kotlin lambdas extend this class and any captured variables are stored in synthetic fields
|
||||
FieldSerializer<Any>(kryo, targetType).apply { setIgnoreSyntheticFields(false) }
|
||||
Throwable::class.java.isAssignableFrom(targetType) -> ThrowableSerializer(kryo, targetType)
|
||||
else -> kryo.getDefaultSerializer(targetType)
|
||||
}
|
||||
return register(Registration(targetType, serializer, NAME.toInt()))
|
||||
} finally {
|
||||
kryo.references = references
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeName(output: Output, type: Class<*>, registration: Registration) {
|
||||
super.writeName(output, registration.type ?: type, registration)
|
||||
}
|
||||
|
||||
// Trivial Serializer which simply returns the given instance, which we already know is a Kotlin object
|
||||
private class KotlinObjectSerializer(private val objectInstance: Any) : Serializer<Any>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any = objectInstance
|
||||
override fun write(kryo: Kryo, output: Output, obj: Any) = Unit
|
||||
}
|
||||
|
||||
// We don't allow the annotation for classes in attachments for now. The class will be on the main classpath if we have the CorDapp installed.
|
||||
// We also do not allow extension of KryoSerializable for annotated classes, or combination with @DefaultSerializer for custom serialisation.
|
||||
// TODO: Later we can support annotations on attachment classes and spin up a proxy via bytecode that we know is harmless.
|
||||
private fun checkForAnnotation(type: Class<*>): Boolean {
|
||||
return (type.classLoader !is AttachmentsClassLoader)
|
||||
&& !KryoSerializable::class.java.isAssignableFrom(type)
|
||||
&& !type.isAnnotationPresent(DefaultSerializer::class.java)
|
||||
&& (type.isAnnotationPresent(CordaSerializable::class.java) || whitelist.hasAnnotationInHierarchy(type))
|
||||
}
|
||||
|
||||
// Need to clear out class names from attachments.
|
||||
override fun reset() {
|
||||
super.reset()
|
||||
// Kryo creates a cache of class name to Class<*> which does not work so well with multiple class loaders.
|
||||
// TODO: come up with a more efficient way. e.g. segregate the name space by class loader.
|
||||
if (nameToClass != null) {
|
||||
val classesToRemove: MutableList<String> = ArrayList(nameToClass.size)
|
||||
nameToClass.entries()
|
||||
.filter { it.value.classLoader is AttachmentsClassLoader }
|
||||
.forEach { classesToRemove += it.key }
|
||||
for (className in classesToRemove) {
|
||||
nameToClass.remove(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface MutableClassWhitelist : ClassWhitelist {
|
||||
fun add(entry: Class<*>)
|
||||
}
|
||||
|
||||
class BuiltInExceptionsWhitelist : ClassWhitelist {
|
||||
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 {
|
||||
override fun hasListed(type: Class<*>): Boolean = true
|
||||
}
|
||||
|
||||
sealed class AbstractMutableClassWhitelist(private val whitelist: MutableSet<String>, private val delegate: ClassWhitelist) : MutableClassWhitelist {
|
||||
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
/**
|
||||
* There are certain delegates like [net.corda.nodeapi.internal.serialization.AllButBlacklisted]
|
||||
* which may throw when asked whether the type is listed.
|
||||
* In such situations - it may be a good idea to ask [delegate] first before making a check against own [whitelist].
|
||||
*/
|
||||
return delegate.hasListed(type) || (type.name in whitelist)
|
||||
}
|
||||
|
||||
override fun add(entry: Class<*>) {
|
||||
whitelist += entry.name
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Need some concept of from which class loader
|
||||
class GlobalTransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(GlobalTransientClassWhiteList.whitelist, delegate) {
|
||||
companion object {
|
||||
private val whitelist: MutableSet<String> = Collections.synchronizedSet(mutableSetOf())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A whitelist that can be customised via the [net.corda.core.serialization.SerializationWhitelist],
|
||||
* since it implements [MutableClassWhitelist].
|
||||
*/
|
||||
class TransientClassWhiteList(delegate: ClassWhitelist) : AbstractMutableClassWhitelist(Collections.synchronizedSet(mutableSetOf()), delegate)
|
||||
|
||||
/**
|
||||
* This class is not currently used, but can be installed to log a large number of missing entries from the whitelist
|
||||
* and was used to track down the initial set.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) : MutableClassWhitelist {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
val globallySeen: MutableSet<String> = Collections.synchronizedSet(mutableSetOf())
|
||||
val journalWriter: PrintWriter? = openOptionalDynamicWhitelistJournal()
|
||||
|
||||
private fun openOptionalDynamicWhitelistJournal(): PrintWriter? {
|
||||
val fileName = System.getenv("WHITELIST_FILE")
|
||||
if (fileName != null && fileName.isNotEmpty()) {
|
||||
try {
|
||||
return PrintWriter(Paths.get(fileName).writer(UTF_8, CREATE, APPEND, WRITE), true)
|
||||
} catch (ioEx: Exception) {
|
||||
log.error("Could not open/create whitelist journal file for append: $fileName", ioEx)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private val locallySeen: MutableSet<String> = mutableSetOf()
|
||||
private val alreadySeen: MutableSet<String> get() = if (global) globallySeen else locallySeen
|
||||
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
if (type.name !in alreadySeen && !delegate.hasListed(type)) {
|
||||
alreadySeen += type.name
|
||||
val className = Util.className(type)
|
||||
log.warn("Dynamically whitelisted class $className")
|
||||
journalWriter?.println(className)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun add(entry: Class<*>) {
|
||||
if (delegate is MutableClassWhitelist) {
|
||||
delegate.add(entry)
|
||||
} else {
|
||||
throw UnsupportedOperationException("Cannot add to whitelist since delegate whitelist is not mutable.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import rx.Notification
|
||||
import rx.exceptions.OnErrorNotImplementedException
|
||||
import sun.security.x509.X509CertImpl
|
||||
import java.security.cert.CRLReason
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* NOTE: We do not whitelist [HashMap] or [HashSet] since they are unstable under serialization.
|
||||
*/
|
||||
object DefaultWhitelist : SerializationWhitelist {
|
||||
override val whitelist =
|
||||
listOf(Array<Any>(0, {}).javaClass,
|
||||
Notification::class.java,
|
||||
Notification.Kind::class.java,
|
||||
ArrayList::class.java,
|
||||
Pair::class.java,
|
||||
Triple::class.java,
|
||||
ByteArray::class.java,
|
||||
UUID::class.java,
|
||||
LinkedHashSet::class.java,
|
||||
Currency::class.java,
|
||||
listOf(Unit).javaClass, // SingletonList
|
||||
setOf(Unit).javaClass, // SingletonSet
|
||||
mapOf(Unit to Unit).javaClass, // SingletonMap
|
||||
NetworkHostAndPort::class.java,
|
||||
SimpleString::class.java,
|
||||
KryoException::class.java, // TODO: Will be removed when we migrate away from Kryo
|
||||
StringBuffer::class.java,
|
||||
Unit::class.java,
|
||||
java.io.ByteArrayInputStream::class.java,
|
||||
java.lang.Class::class.java,
|
||||
java.math.BigDecimal::class.java,
|
||||
|
||||
// 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.LocalTime::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 serializer but it's an enum.
|
||||
java.time.Month::class.java, // No custom serializer but it's an enum.
|
||||
|
||||
java.util.Collections.emptyMap<Any, Any>().javaClass,
|
||||
java.util.Collections.emptySet<Any>().javaClass,
|
||||
java.util.Collections.emptyList<Any>().javaClass,
|
||||
java.util.LinkedHashMap::class.java,
|
||||
BitSet::class.java,
|
||||
OnErrorNotImplementedException::class.java,
|
||||
StackTraceElement::class.java,
|
||||
|
||||
// Implementation of X509Certificate.
|
||||
X509CertImpl::class.java,
|
||||
CRLReason::class.java
|
||||
)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
|
||||
class GeneratedAttachment(val bytes: ByteArray) : AbstractAttachment({ bytes }) {
|
||||
override val id = bytes.sha256()
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import java.io.EOFException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class OrdinalBits(private val ordinal: Int) {
|
||||
interface OrdinalWriter {
|
||||
val bits: OrdinalBits
|
||||
val encodedSize get() = 1
|
||||
fun writeTo(stream: OutputStream) = stream.write(bits.ordinal)
|
||||
fun putTo(buffer: ByteBuffer) = buffer.put(bits.ordinal.toByte())!!
|
||||
}
|
||||
|
||||
init {
|
||||
require(ordinal >= 0) { "The ordinal must be non-negative." }
|
||||
require(ordinal < 128) { "Consider implementing a varint encoding." }
|
||||
}
|
||||
}
|
||||
|
||||
class OrdinalReader<out E : Any>(private val values: Array<E>) {
|
||||
private val enumName = values[0].javaClass.simpleName
|
||||
private val range = 0 until values.size
|
||||
fun readFrom(stream: InputStream): E {
|
||||
val ordinal = stream.read()
|
||||
if (ordinal == -1) throw EOFException("Expected a $enumName ordinal.")
|
||||
if (ordinal !in range) throw NoSuchElementException("No $enumName with ordinal: $ordinal")
|
||||
return values[ordinal]
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.serialization.SerializationEncoding
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.internal.serialization.OrdinalBits.OrdinalWriter
|
||||
import org.iq80.snappy.SnappyFramedInputStream
|
||||
import org.iq80.snappy.SnappyFramedOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
class CordaSerializationMagic(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
private val bufferView = slice()
|
||||
fun consume(data: ByteSequence): ByteBuffer? {
|
||||
return if (data.slice(end = size) == bufferView) data.slice(size) else null
|
||||
}
|
||||
}
|
||||
|
||||
enum class SectionId : OrdinalWriter {
|
||||
/** Serialization data follows, and then discard the rest of the stream (if any) as legacy data may have trailing garbage. */
|
||||
DATA_AND_STOP,
|
||||
/** Identical behaviour to [DATA_AND_STOP], historically used for Kryo. Do not use in new code. */
|
||||
ALT_DATA_AND_STOP,
|
||||
/** The ordinal of a [CordaSerializationEncoding] follows, which should be used to decode the remainder of the stream. */
|
||||
ENCODING;
|
||||
|
||||
companion object {
|
||||
val reader = OrdinalReader(values())
|
||||
}
|
||||
|
||||
override val bits = OrdinalBits(ordinal)
|
||||
}
|
||||
|
||||
enum class CordaSerializationEncoding : SerializationEncoding, OrdinalWriter {
|
||||
DEFLATE {
|
||||
override fun wrap(stream: OutputStream) = DeflaterOutputStream(stream)
|
||||
override fun wrap(stream: InputStream) = InflaterInputStream(stream)
|
||||
},
|
||||
SNAPPY {
|
||||
override fun wrap(stream: OutputStream) = SnappyFramedOutputStream(stream)
|
||||
override fun wrap(stream: InputStream) = SnappyFramedInputStream(stream, false)
|
||||
};
|
||||
|
||||
companion object {
|
||||
val reader = OrdinalReader(values())
|
||||
}
|
||||
|
||||
override val bits = OrdinalBits(ordinal)
|
||||
abstract fun wrap(stream: OutputStream): OutputStream
|
||||
abstract fun wrap(stream: InputStream): InputStream
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal val encodingNotPermittedFormat = "Encoding not permitted: %s"
|
@ -1,170 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.copyBytes
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.AttachmentsClassLoader
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.NotSerializableException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
const val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled"
|
||||
|
||||
internal object NullEncodingWhitelist : EncodingWhitelist {
|
||||
override fun acceptEncoding(encoding: SerializationEncoding) = false
|
||||
}
|
||||
|
||||
data class SerializationContextImpl @JvmOverloads constructor(override val preferredSerializationVersion: SerializationMagic,
|
||||
override val deserializationClassLoader: ClassLoader,
|
||||
override val whitelist: ClassWhitelist,
|
||||
override val properties: Map<Any, Any>,
|
||||
override val objectReferencesEnabled: Boolean,
|
||||
override val useCase: SerializationContext.UseCase,
|
||||
override val encoding: SerializationEncoding?,
|
||||
override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : SerializationContext {
|
||||
private val builder = AttachmentsClassLoaderBuilder(properties, deserializationClassLoader)
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context.
|
||||
*/
|
||||
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
|
||||
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this
|
||||
val classLoader = builder.build(attachmentHashes) ?: return this
|
||||
return withClassLoader(classLoader)
|
||||
}
|
||||
|
||||
override fun withProperty(property: Any, value: Any): SerializationContext {
|
||||
return copy(properties = properties + (property to value))
|
||||
}
|
||||
|
||||
override fun withoutReferences(): SerializationContext {
|
||||
return copy(objectReferencesEnabled = false)
|
||||
}
|
||||
|
||||
override fun withClassLoader(classLoader: ClassLoader): SerializationContext {
|
||||
return copy(deserializationClassLoader = classLoader)
|
||||
}
|
||||
|
||||
override fun withWhitelisted(clazz: Class<*>): SerializationContext {
|
||||
return copy(whitelist = object : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>): Boolean = whitelist.hasListed(type) || type.name == clazz.name
|
||||
})
|
||||
}
|
||||
|
||||
override fun withPreferredSerializationVersion(magic: SerializationMagic) = copy(preferredSerializationVersion = magic)
|
||||
override fun withEncoding(encoding: SerializationEncoding?) = copy(encoding = encoding)
|
||||
}
|
||||
|
||||
/*
|
||||
* This class is internal rather than private so that node-api-deterministic
|
||||
* can replace it with an alternative version.
|
||||
*/
|
||||
internal class AttachmentsClassLoaderBuilder(private val properties: Map<Any, Any>, private val deserializationClassLoader: ClassLoader) {
|
||||
private val cache: Cache<List<SecureHash>, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build()
|
||||
|
||||
fun build(attachmentHashes: List<SecureHash>): AttachmentsClassLoader? {
|
||||
val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContext ?: return null // Some tests don't set one.
|
||||
try {
|
||||
return 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, parent = deserializationClassLoader)
|
||||
}!!
|
||||
} catch (e: ExecutionException) {
|
||||
// Caught from within the cache get, so unwrap.
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class SerializationFactoryImpl(
|
||||
// TODO: This is read-mostly. Probably a faster implementation to be found.
|
||||
private val schemes: MutableMap<Pair<CordaSerializationMagic, SerializationContext.UseCase>, SerializationScheme>
|
||||
) : SerializationFactory() {
|
||||
constructor() : this(ConcurrentHashMap())
|
||||
|
||||
companion object {
|
||||
val magicSize = sequenceOf(kryoMagic, amqpMagic).map { it.size }.distinct().single()
|
||||
}
|
||||
|
||||
private val creator: List<StackTraceElement> = Exception().stackTrace.asList()
|
||||
|
||||
private val registeredSchemes: MutableCollection<SerializationScheme> = Collections.synchronizedCollection(mutableListOf())
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): Pair<SerializationScheme, CordaSerializationMagic> {
|
||||
// truncate sequence to at most magicSize, and make sure it's a copy to avoid holding onto large ByteArrays
|
||||
val magic = CordaSerializationMagic(byteSequence.slice(end = magicSize).copyBytes())
|
||||
val lookupKey = magic to target
|
||||
return schemes.computeIfAbsent(lookupKey) {
|
||||
registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single?
|
||||
logger.warn("Cannot find serialization scheme for: [$lookupKey, " +
|
||||
"${if (magic == amqpMagic) "AMQP" else if (magic == kryoMagic) "Kryo" else "UNKNOWN MAGIC"}] registeredSchemes are: $registeredSchemes")
|
||||
throw UnsupportedOperationException("Serialization scheme $lookupKey not supported.")
|
||||
} to magic
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
return asCurrent { withCurrentContext(context) { schemeFor(byteSequence, context.useCase).first.deserialize(byteSequence, clazz, context) } }
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
override fun <T : Any> deserializeWithCompatibleContext(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): ObjectWithCompatibleContext<T> {
|
||||
return asCurrent {
|
||||
withCurrentContext(context) {
|
||||
val (scheme, magic) = schemeFor(byteSequence, context.useCase)
|
||||
val deserializedObject = scheme.deserialize(byteSequence, clazz, context)
|
||||
ObjectWithCompatibleContext(deserializedObject, context.withPreferredSerializationVersion(magic))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
return asCurrent { withCurrentContext(context) { schemeFor(context.preferredSerializationVersion, context.useCase).first.serialize(obj, context) } }
|
||||
}
|
||||
|
||||
fun registerScheme(scheme: SerializationScheme) {
|
||||
check(schemes.isEmpty()) { "All serialization schemes must be registered before any scheme is used." }
|
||||
registeredSchemes += scheme
|
||||
}
|
||||
|
||||
val alreadyRegisteredSchemes: Collection<SerializationScheme> get() = Collections.unmodifiableCollection(registeredSchemes)
|
||||
|
||||
override fun toString(): String {
|
||||
return "${this.javaClass.name} registeredSchemes=$registeredSchemes ${creator.joinToString("\n")}"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is SerializationFactoryImpl &&
|
||||
other.registeredSchemes == this.registeredSchemes
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = registeredSchemes.hashCode()
|
||||
}
|
||||
|
||||
|
||||
interface SerializationScheme {
|
||||
fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T>
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
|
||||
val serializationContextKey = SerializeAsTokenContext::class.java
|
||||
|
||||
fun SerializationContext.withTokenContext(serializationContext: SerializeAsTokenContext): SerializationContext = this.withProperty(serializationContextKey, serializationContext)
|
||||
|
||||
/**
|
||||
* A context for mapping SerializationTokens to/from SerializeAsTokens.
|
||||
*
|
||||
* A context is initialised with an object containing all the instances of [SerializeAsToken] to eagerly register all the tokens.
|
||||
* In our case this can be the [ServiceHub].
|
||||
*
|
||||
* Then it is a case of using the companion object methods on [SerializeAsTokenSerializer] to set and clear context as necessary
|
||||
* when serializing to enable/disable tokenization.
|
||||
*/
|
||||
class SerializeAsTokenContextImpl(override val serviceHub: ServiceHub, init: SerializeAsTokenContext.() -> Unit) : SerializeAsTokenContext {
|
||||
constructor(toBeTokenized: Any, serializationFactory: SerializationFactory, context: SerializationContext, serviceHub: ServiceHub) : this(serviceHub, {
|
||||
serializationFactory.serialize(toBeTokenized, context.withTokenContext(this))
|
||||
})
|
||||
|
||||
private val classNameToSingleton = mutableMapOf<String, SerializeAsToken>()
|
||||
private var readOnly = false
|
||||
|
||||
init {
|
||||
/**
|
||||
* Go ahead and eagerly serialize the object to register all of the tokens in the context.
|
||||
*
|
||||
* This results in the toToken() method getting called for any [SingletonSerializeAsToken] instances which
|
||||
* are encountered in the object graph as they are serialized and will therefore register the token to
|
||||
* object mapping for those instances. We then immediately set the readOnly flag to stop further adhoc or
|
||||
* accidental registrations from occuring as these could not be deserialized in a deserialization-first
|
||||
* scenario if they are not part of this iniital context construction serialization.
|
||||
*/
|
||||
init(this)
|
||||
readOnly = true
|
||||
}
|
||||
|
||||
override fun putSingleton(toBeTokenized: SerializeAsToken) {
|
||||
val className = toBeTokenized.javaClass.name
|
||||
if (className !in classNameToSingleton) {
|
||||
// Only allowable if we are in SerializeAsTokenContext init (readOnly == false)
|
||||
if (readOnly) {
|
||||
throw UnsupportedOperationException("Attempt to write token for lazy registered $className. All tokens should be registered during context construction.")
|
||||
}
|
||||
classNameToSingleton[className] = toBeTokenized
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSingleton(className: String) = classNameToSingleton[className]
|
||||
?: throw IllegalStateException("Unable to find tokenized instance of $className in context $this")
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
@file:JvmName("ServerContexts")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
/*
|
||||
* Serialisation contexts for the server.
|
||||
* These have been refactored into a separate file to prevent
|
||||
* clients from trying to instantiate any of them.
|
||||
*
|
||||
* NOTE: The [KRYO_STORAGE_CONTEXT] and [AMQP_STORAGE_CONTEXT]
|
||||
* CANNOT always be instantiated outside of the server and so
|
||||
* MUST be kept separate!
|
||||
*/
|
||||
|
||||
|
||||
val AMQP_STORAGE_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
AllButBlacklisted,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Storage,
|
||||
null,
|
||||
AlwaysAcceptEncodingWhitelist)
|
||||
|
||||
val AMQP_RPC_SERVER_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.RPCServer,
|
||||
null)
|
@ -1,39 +0,0 @@
|
||||
@file:JvmName("SharedContexts")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.nodeapi.internal.serialization.amqp.amqpMagic
|
||||
import net.corda.nodeapi.internal.serialization.kryo.kryoMagic
|
||||
|
||||
/*
|
||||
* Serialisation contexts shared by the server and client.
|
||||
*
|
||||
* NOTE: The [KRYO_STORAGE_CONTEXT] and [AMQP_STORAGE_CONTEXT]
|
||||
* CANNOT always be instantiated outside of the server and so
|
||||
* MUST be kept separate from these ones!
|
||||
*/
|
||||
val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(kryoMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
QuasarWhitelist,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Checkpoint,
|
||||
null,
|
||||
AlwaysAcceptEncodingWhitelist)
|
||||
|
||||
val AMQP_P2P_CONTEXT = SerializationContextImpl(amqpMagic,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P,
|
||||
null)
|
||||
|
||||
internal object AlwaysAcceptEncodingWhitelist : EncodingWhitelist {
|
||||
override fun acceptEncoding(encoding: SerializationEncoding) = true
|
||||
}
|
||||
|
||||
object QuasarWhitelist : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>): Boolean = true
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import java.util.*
|
||||
|
||||
internal fun checkUseCase(allowedUseCases: EnumSet<SerializationContext.UseCase>) {
|
||||
val currentContext: SerializationContext = SerializationFactory.currentFactory?.currentContext
|
||||
?: throw IllegalStateException("Current context is not set")
|
||||
if (!allowedUseCases.contains(currentContext.useCase)) {
|
||||
throw IllegalStateException("UseCase '${currentContext.useCase}' is not within '$allowedUseCases'")
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import org.apache.qpid.proton.amqp.UnsignedLong
|
||||
|
||||
/**
|
||||
* R3 AMQP assigned enterprise number
|
||||
*
|
||||
* see [here](https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers)
|
||||
*
|
||||
* Repeated here for brevity:
|
||||
* 50530 - R3 - Mike Hearn - mike&r3.com
|
||||
*/
|
||||
const val DESCRIPTOR_TOP_32BITS: Long = 0xc562L shl (32 + 16)
|
||||
|
||||
/**
|
||||
* AMQP descriptor ID's for our custom types.
|
||||
*
|
||||
* NEVER DELETE OR CHANGE THE ID ASSOCIATED WITH A TYPE
|
||||
*
|
||||
* these are encoded as part of a serialised blob and doing so would render us unable to
|
||||
* de-serialise that blob!!!
|
||||
*/
|
||||
enum class AMQPDescriptorRegistry(val id: Long) {
|
||||
ENVELOPE(1),
|
||||
SCHEMA(2),
|
||||
OBJECT_DESCRIPTOR(3),
|
||||
FIELD(4),
|
||||
COMPOSITE_TYPE(5),
|
||||
RESTRICTED_TYPE(6),
|
||||
CHOICE(7),
|
||||
REFERENCED_OBJECT(8),
|
||||
TRANSFORM_SCHEMA(9),
|
||||
TRANSFORM_ELEMENT(10),
|
||||
TRANSFORM_ELEMENT_KEY(11)
|
||||
;
|
||||
|
||||
val amqpDescriptor = UnsignedLong(id or DESCRIPTOR_TOP_32BITS)
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class SyntheticParameterException(val type: Type) : NotSerializableException("Type '${type.typeName} has synthetic "
|
||||
+ "fields and is likely a nested inner class. This is not support by the Corda AMQP serialization framework")
|
@ -1,42 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
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
|
||||
|
||||
/**
|
||||
* Serializer / deserializer for native AMQP types (Int, Float, String etc).
|
||||
*
|
||||
* [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary].
|
||||
*/
|
||||
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
|
||||
override val typeDescriptor = Symbol.valueOf(SerializerFactory.primitiveTypeName(clazz)!!)!!
|
||||
override val type: Type = clazz
|
||||
|
||||
// NOOP since this is a primitive type.
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
}
|
||||
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
context: SerializationContext,
|
||||
debugIndent: Int
|
||||
) {
|
||||
if (obj is ByteArray) {
|
||||
data.putObject(Binary(obj))
|
||||
} else {
|
||||
data.putObject(obj)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(
|
||||
obj: Any,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = (obj as? Binary)?.array ?: obj
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
@file:JvmName("AMQPSerializationScheme")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.internal.objectOrNewInstance
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.MutableClassWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.SerializationScheme
|
||||
import java.lang.reflect.Modifier
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == amqpMagic
|
||||
|
||||
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
|
||||
require(types.toSet().size == types.size) {
|
||||
val duplicates = types.toMutableList()
|
||||
types.toSet().forEach { duplicates -= it }
|
||||
"Cannot add duplicate classes to the whitelist ($duplicates)."
|
||||
}
|
||||
for (type in types) {
|
||||
(this.whitelist as? MutableClassWhitelist)?.add(type)
|
||||
}
|
||||
}
|
||||
|
||||
// Allow us to create a SerializerFactory with a different ClassCarpenter implementation.
|
||||
interface SerializerFactoryFactory {
|
||||
fun make(context: SerializationContext): SerializerFactory
|
||||
}
|
||||
|
||||
abstract class AbstractAMQPSerializationScheme(
|
||||
private val cordappCustomSerializers: Set<SerializationCustomSerializer<*,*>>,
|
||||
private val serializerFactoriesForContexts: MutableMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>,
|
||||
val sff: SerializerFactoryFactory = createSerializerFactoryFactory()
|
||||
) : SerializationScheme {
|
||||
constructor(cordapps: List<Cordapp>) : this(cordapps.customSerializers, ConcurrentHashMap())
|
||||
|
||||
// TODO: This method of initialisation for the Whitelist and plugin serializers will have to change
|
||||
// when we have per-cordapp contexts and dynamic app reloading but for now it's the easiest way
|
||||
companion object {
|
||||
|
||||
const val SCAN_SPEC_PROP_NAME = "amqp.custom.serialization.scanSpec"
|
||||
|
||||
private val serializationWhitelists: List<SerializationWhitelist> by lazy {
|
||||
ServiceLoader.load(SerializationWhitelist::class.java, this::class.java.classLoader).toList() + DefaultWhitelist
|
||||
}
|
||||
|
||||
private val customSerializers: List<SerializationCustomSerializer<*, *>> by lazy {
|
||||
|
||||
val scanSpec: String? = System.getProperty(SCAN_SPEC_PROP_NAME)
|
||||
|
||||
if (scanSpec == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
FastClasspathScanner(scanSpec).addClassLoader(this::class.java.classLoader).scan()
|
||||
.getNamesOfClassesImplementing(SerializationCustomSerializer::class.java)
|
||||
.mapNotNull { this::class.java.classLoader.loadClass(it).asSubclass(SerializationCustomSerializer::class.java) }
|
||||
.filterNot { Modifier.isAbstract(it.modifiers) }
|
||||
.map { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
}
|
||||
|
||||
val List<Cordapp>.customSerializers get() = flatMap { it.serializationCustomSerializers }.toSet()
|
||||
}
|
||||
|
||||
// Parameter "context" is unused directy but passed in by reflection. Removing it will cause failures.
|
||||
private fun registerCustomSerializers(context: SerializationContext, factory: SerializerFactory) {
|
||||
with(factory) {
|
||||
register(publicKeySerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.PrivateKeySerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.BigIntegerSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.OpaqueBytesSubSequenceSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.DurationSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateTimeSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalTimeSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.ZonedDateTimeSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.ZoneIdSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetTimeSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetDateTimeSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.YearSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.YearMonthSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.MonthDaySerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.PeriodSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.ClassSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CRLSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.CertPathSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.StringBufferSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.InputStreamSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.ContractAttachmentSerializer(this))
|
||||
registerNonDeterministicSerializers(factory)
|
||||
}
|
||||
for (whitelistProvider in serializationWhitelists) {
|
||||
factory.addToWhitelist(*whitelistProvider.whitelist.toTypedArray())
|
||||
}
|
||||
|
||||
// If we're passed in an external list we trust that, otherwise revert to looking at the scan of the
|
||||
// classpath to find custom serializers.
|
||||
if (cordappCustomSerializers.isEmpty()) {
|
||||
for (customSerializer in customSerializers) {
|
||||
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
|
||||
}
|
||||
} else {
|
||||
cordappCustomSerializers.forEach { customSerializer ->
|
||||
factory.registerExternal(CorDappCustomSerializer(customSerializer, factory))
|
||||
}
|
||||
}
|
||||
|
||||
context.properties[ContextPropertyKeys.SERIALIZERS]?.apply {
|
||||
uncheckedCast<Any, List<CustomSerializer<out Any>>>(this).forEach {
|
||||
factory.register(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Register the serializers which will be excluded from the DJVM.
|
||||
*/
|
||||
private fun registerNonDeterministicSerializers(factory: SerializerFactory) {
|
||||
with(factory) {
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.SimpleStringSerializer)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory
|
||||
protected abstract fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory
|
||||
|
||||
// Not used as a simple direct import to facilitate testing
|
||||
open val publicKeySerializer : CustomSerializer<*> = net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer
|
||||
|
||||
private fun getSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
return serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) {
|
||||
when (context.useCase) {
|
||||
SerializationContext.UseCase.Checkpoint ->
|
||||
throw IllegalStateException("AMQP should not be used for checkpoint serialization.")
|
||||
SerializationContext.UseCase.RPCClient ->
|
||||
rpcClientSerializerFactory(context)
|
||||
SerializationContext.UseCase.RPCServer ->
|
||||
rpcServerSerializerFactory(context)
|
||||
else -> sff.make(context)
|
||||
}.also {
|
||||
registerCustomSerializers(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
val serializerFactory = getSerializerFactory(context)
|
||||
return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz, context)
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
val serializerFactory = getSerializerFactory(context)
|
||||
|
||||
return SerializationOutput(serializerFactory).serialize(obj, context)
|
||||
}
|
||||
|
||||
protected fun canDeserializeVersion(magic: CordaSerializationMagic) = magic == amqpMagic
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Implemented to serialize and deserialize different types of objects to/from AMQP.
|
||||
*/
|
||||
interface AMQPSerializer<out T> {
|
||||
/**
|
||||
* The JVM type this can serialize and deserialize.
|
||||
*/
|
||||
val type: Type
|
||||
|
||||
/**
|
||||
* Textual unique representation of the JVM type this represents. Will be encoded into the AMQP stream and
|
||||
* will appear in the schema.
|
||||
*
|
||||
* 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: Symbol
|
||||
|
||||
/**
|
||||
* Add anything required to the AMQP schema via [SerializationOutput.writeTypeNotations] and any dependent serializers
|
||||
* via [SerializationOutput.requireSerializer]. e.g. for the elements of an array.
|
||||
*/
|
||||
fun writeClassInfo(output: SerializationOutput)
|
||||
|
||||
/**
|
||||
* Write the given object, with declared type, to the output.
|
||||
*/
|
||||
fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int = 0)
|
||||
|
||||
/**
|
||||
* Read the given object from the input. The envelope is provided in case the schema is required.
|
||||
*/
|
||||
fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): T
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
@file:JvmName("AMQPSerializerFactories")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
|
||||
fun createSerializerFactoryFactory(): SerializerFactoryFactory = SerializerFactoryFactoryImpl()
|
||||
|
||||
open class SerializerFactoryFactoryImpl : SerializerFactoryFactory {
|
||||
override fun make(context: SerializationContext) =
|
||||
SerializerFactory(context.whitelist, context.deserializationClassLoader)
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
@file:JvmName("AMQPStreams")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.ByteBufferInputStream
|
||||
import net.corda.nodeapi.internal.serialization.ByteBufferOutputStream
|
||||
import net.corda.nodeapi.internal.serialization.serializeOutputStreamPool
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun InputStream.asByteBuffer(): ByteBuffer {
|
||||
return if (this is ByteBufferInputStream) {
|
||||
byteBuffer // BBIS has no other state, so this is perfectly safe.
|
||||
} else {
|
||||
ByteBuffer.wrap(serializeOutputStreamPool.run {
|
||||
copyTo(it)
|
||||
it.toByteArray()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> OutputStream.alsoAsByteBuffer(remaining: Int, task: (ByteBuffer) -> T): T {
|
||||
return if (this is ByteBufferOutputStream) {
|
||||
alsoAsByteBuffer(remaining, task)
|
||||
} else {
|
||||
serializeOutputStreamPool.run {
|
||||
val result = it.alsoAsByteBuffer(remaining, task)
|
||||
it.copyTo(this)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Serialization / deserialization of arrays.
|
||||
*/
|
||||
open class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
companion object {
|
||||
fun make(type: Type, factory: SerializerFactory) = when (type) {
|
||||
Array<Char>::class.java -> CharArraySerializer(factory)
|
||||
else -> ArraySerializer(type, factory)
|
||||
}
|
||||
}
|
||||
|
||||
// because this might be an array of array of primitives (to any recursive depth) and
|
||||
// because we care that the lowest type is unboxed we can't rely on the inbuilt type
|
||||
// id to generate it properly (it will always return [[[Ljava.lang.type -> type[][][]
|
||||
// for example).
|
||||
//
|
||||
// We *need* to retain knowledge for AMQP deserialization weather that lowest primitive
|
||||
// was boxed or unboxed so just infer it recursively.
|
||||
private fun calcTypeName(type: Type): String =
|
||||
if (type.componentType().isArray()) {
|
||||
val typeName = calcTypeName(type.componentType()); "$typeName[]"
|
||||
} else {
|
||||
val arrayType = if (type.asClass()!!.componentType.isPrimitive) "[p]" else "[]"
|
||||
"${type.componentType().typeName}$arrayType"
|
||||
}
|
||||
|
||||
override val typeDescriptor: Symbol by lazy {
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
}
|
||||
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), emptyList())
|
||||
}
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
if (output.writeTypeNotations(typeNotation)) {
|
||||
output.requireSerializer(elementType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
withList {
|
||||
for (entry in obj as Array<*>) {
|
||||
output.writeObjectOrNull(entry, this, elementType, context, debugIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
if (obj is List<*>) {
|
||||
return obj.map { input.readObjectOrNull(it, schemas, elementType, context) }.toArrayOfType(elementType)
|
||||
} else throw NotSerializableException("Expected a List but found $obj")
|
||||
}
|
||||
|
||||
open fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
|
||||
val list = this
|
||||
return java.lang.reflect.Array.newInstance(elementType, this.size).apply {
|
||||
(0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, list[it]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boxed Character arrays required a specialisation to handle the type conversion properly when populating
|
||||
// the array since Kotlin won't allow an implicit cast from Int (as they're stored as 16bit ints) to Char
|
||||
class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array<Char>::class.java, factory) {
|
||||
override fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
|
||||
val list = this
|
||||
return java.lang.reflect.Array.newInstance(elementType, this.size).apply {
|
||||
(0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, (list[it] as Int).toChar()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Specialisation of [ArraySerializer] that handles arrays of unboxed java primitive types
|
||||
abstract class PrimArraySerializer(type: Type, factory: SerializerFactory) : ArraySerializer(type, factory) {
|
||||
companion object {
|
||||
// We don't need to handle the unboxed byte type as that is coercible to a byte array, but
|
||||
// the other 7 primitive types we do
|
||||
private val primTypes: Map<Type, (SerializerFactory) -> PrimArraySerializer> = mapOf(
|
||||
IntArray::class.java to { f -> PrimIntArraySerializer(f) },
|
||||
CharArray::class.java to { f -> PrimCharArraySerializer(f) },
|
||||
BooleanArray::class.java to { f -> PrimBooleanArraySerializer(f) },
|
||||
FloatArray::class.java to { f -> PrimFloatArraySerializer(f) },
|
||||
ShortArray::class.java to { f -> PrimShortArraySerializer(f) },
|
||||
DoubleArray::class.java to { f -> PrimDoubleArraySerializer(f) },
|
||||
LongArray::class.java to { f -> PrimLongArraySerializer(f) }
|
||||
// ByteArray::class.java <-> NOT NEEDED HERE (see comment above)
|
||||
)
|
||||
|
||||
fun make(type: Type, factory: SerializerFactory) = primTypes[type]!!(factory)
|
||||
}
|
||||
|
||||
fun localWriteObject(data: Data, func: () -> Unit) {
|
||||
data.withDescribed(typeNotation.descriptor) { withList { func() } }
|
||||
}
|
||||
}
|
||||
|
||||
class PrimIntArraySerializer(factory: SerializerFactory) : PrimArraySerializer(IntArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
localWriteObject(data) {
|
||||
(obj as IntArray).forEach { output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer(CharArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
localWriteObject(data) {
|
||||
(obj as CharArray).forEach {
|
||||
output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> List<T>.toArrayOfType(type: Type): Any {
|
||||
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
|
||||
val list = this
|
||||
return java.lang.reflect.Array.newInstance(elementType, this.size).apply {
|
||||
val array = this
|
||||
(0..lastIndex).forEach { java.lang.reflect.Array.set(array, it, (list[it] as Int).toChar()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimBooleanArraySerializer(factory: SerializerFactory) : PrimArraySerializer(BooleanArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
localWriteObject(data) {
|
||||
(obj as BooleanArray).forEach { output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimDoubleArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(DoubleArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
localWriteObject(data) {
|
||||
(obj as DoubleArray).forEach { output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimFloatArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(FloatArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int) {
|
||||
localWriteObject(data) {
|
||||
(obj as FloatArray).forEach { output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimShortArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(ShortArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
localWriteObject(data) {
|
||||
(obj as ShortArray).forEach { output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrimLongArraySerializer(factory: SerializerFactory) :
|
||||
PrimArraySerializer(LongArray::class.java, factory) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
localWriteObject(data) {
|
||||
(obj as LongArray).forEach { output.writeObjectOrNull(it, data, elementType, context, debugIndent + 1) }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
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
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashSet
|
||||
|
||||
/**
|
||||
* Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s.
|
||||
*/
|
||||
class CollectionSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = declaredType as? DeserializedParameterizedType
|
||||
?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
|
||||
override val typeDescriptor: Symbol by lazy {
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// 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.
|
||||
return deriveParametrizedType(declaredType, uncheckedCast(declaredClass))
|
||||
} 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), emptyList())
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
if (output.writeTypeNotations(typeNotation)) {
|
||||
output.requireSerializer(declaredType.actualTypeArguments[0])
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
context: SerializationContext,
|
||||
debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
withList {
|
||||
for (entry in obj as Collection<*>) {
|
||||
output.writeObjectOrNull(entry, this, declaredType.actualTypeArguments[0], context, debugIndent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(
|
||||
obj: Any,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// TODO: Can we verify the entries in the list?
|
||||
concreteBuilder((obj as List<*>).map {
|
||||
input.readObjectOrNull(it, schemas, declaredType.actualTypeArguments[0], context)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
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.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
/**
|
||||
* Index into the types list of the parent type of the serializer object, should be the
|
||||
* type that this object proxies for
|
||||
*/
|
||||
const val CORDAPP_TYPE = 0
|
||||
|
||||
/**
|
||||
* Index into the types list of the parent type of the serializer object, should be the
|
||||
* type of the proxy object that we're using to represent the object we're proxying for
|
||||
*/
|
||||
const val PROXY_TYPE = 1
|
||||
|
||||
/**
|
||||
* Wrapper class for user provided serializers
|
||||
*
|
||||
* Through the CorDapp JAR scanner we will have a list of custom serializer types that implement
|
||||
* the toProxy and fromProxy methods. This class takes an instance of one of those objects and
|
||||
* embeds it within a serialization context associated with a serializer factory by creating
|
||||
* and instance of this class and registering that with a [SerializerFactory]
|
||||
*
|
||||
* Proxy serializers should transform an unserializable class into a representation that we can serialize
|
||||
*
|
||||
* @property serializer in instance of a user written serialization proxy, normally scanned and loaded
|
||||
* automatically
|
||||
* @property type the Java [Type] of the class which this serializes, inferred via reflection of the
|
||||
* [serializer]'s super type
|
||||
* @property proxyType the Java [Type] of the class into which instances of [type] are proxied for use by
|
||||
* the underlying serialization engine
|
||||
*
|
||||
* @param factory a [SerializerFactory] belonging to the context this serializer is being instantiated
|
||||
* for
|
||||
*/
|
||||
class CorDappCustomSerializer(
|
||||
private val serializer: SerializationCustomSerializer<*, *>,
|
||||
factory: SerializerFactory) : AMQPSerializer<Any>, SerializerFor {
|
||||
override val revealSubclassesInSchema: Boolean get() = false
|
||||
private val types = serializer::class.supertypes.filter { it.jvmErasure == SerializationCustomSerializer::class }
|
||||
.flatMap { it.arguments }
|
||||
.map { it.type!!.javaType }
|
||||
|
||||
init {
|
||||
if (types.size != 2) {
|
||||
throw NotSerializableException("Unable to determine serializer parent types")
|
||||
}
|
||||
}
|
||||
|
||||
override val type = types[CORDAPP_TYPE]
|
||||
val proxyType = types[PROXY_TYPE]
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(type)}")
|
||||
val descriptor: Descriptor = Descriptor(typeDescriptor)
|
||||
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyType, factory) }
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val proxy = uncheckedCast<SerializationCustomSerializer<*, *>,
|
||||
SerializationCustomSerializer<Any?, Any?>>(serializer).toProxy(obj)
|
||||
|
||||
data.withDescribed(descriptor) {
|
||||
data.withList {
|
||||
proxySerializer.propertySerializers.serializationOrder.forEach {
|
||||
it.getter.writeProperty(proxy, this, output, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
) = uncheckedCast<SerializationCustomSerializer<*, *>, SerializationCustomSerializer<Any?, Any?>>(
|
||||
serializer).fromProxy(uncheckedCast(proxySerializer.readObject(obj, schemas, input, context)))!!
|
||||
|
||||
override fun isSerializerFor(clazz: Class<*>) = clazz == type
|
||||
}
|
||||
|
@ -1,209 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
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
|
||||
|
||||
interface SerializerFor {
|
||||
/**
|
||||
* This method should return true if the custom serializer can serialize an instance of the class passed as the
|
||||
* parameter.
|
||||
*/
|
||||
fun isSerializerFor(clazz: Class<*>): Boolean
|
||||
|
||||
val revealSubclassesInSchema: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for serializers of core platform types that do not conform to the usual serialization rules and thus
|
||||
* cannot be automatically serialized.
|
||||
*/
|
||||
abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
|
||||
/**
|
||||
* This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects
|
||||
* that refer to other custom types etc.
|
||||
*/
|
||||
open val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
|
||||
|
||||
|
||||
protected abstract val descriptor: Descriptor
|
||||
/**
|
||||
* This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization
|
||||
* code path.
|
||||
*/
|
||||
abstract val schemaForDocumentation: Schema
|
||||
|
||||
/**
|
||||
* Whether subclasses using this serializer via inheritance should have a mapping in the schema.
|
||||
*/
|
||||
override val revealSubclassesInSchema: Boolean get() = false
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
data.withDescribed(descriptor) {
|
||||
writeDescribedObject(uncheckedCast(obj), data, type, output, context)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext)
|
||||
|
||||
/**
|
||||
* This custom serializer represents a sort of symbolic link from a subclass to a super class, where the super
|
||||
* class custom serializer is responsible for the "on the wire" format but we want to create a reference to the
|
||||
* subclass in the schema, so that we can distinguish between subclasses.
|
||||
*/
|
||||
// TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer?
|
||||
class SubClass<T : Any>(private val clazz: Class<*>, private val superClassSerializer: CustomSerializer<T>) : CustomSerializer<T>() {
|
||||
// TODO: should this be empty or contain the schema of the super?
|
||||
override val schemaForDocumentation = Schema(emptyList())
|
||||
|
||||
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
|
||||
override val type: Type get() = clazz
|
||||
override val typeDescriptor: Symbol by lazy {
|
||||
Symbol.valueOf("$DESCRIPTOR_DOMAIN:${SerializerFingerPrinter().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)
|
||||
}
|
||||
|
||||
override val descriptor: Descriptor = Descriptor(typeDescriptor)
|
||||
|
||||
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
superClassSerializer.writeDescribedObject(obj, data, type, output, context)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): T {
|
||||
return superClassSerializer.readObject(obj, schemas, input, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional base features for a custom serializer for a particular class [withInheritance] is false
|
||||
* or super class / interfaces [withInheritance] is true
|
||||
*/
|
||||
abstract class CustomSerializerImp<T : Any>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
|
||||
override val type: Type get() = clazz
|
||||
override val typeDescriptor: Symbol = 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional base features for a custom serializer for a particular class, that excludes subclasses.
|
||||
*/
|
||||
abstract class Is<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, false)
|
||||
|
||||
/**
|
||||
* Additional base features for a custom serializer for all implementations of a particular interface or super class.
|
||||
*/
|
||||
abstract class Implements<T : Any>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, true)
|
||||
|
||||
/**
|
||||
* Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be
|
||||
* the serialized form of a proxy class, and the object can be re-created from that proxy on deserialization.
|
||||
*
|
||||
* The proxy class must use only types which are either native AMQP or other types for which there are pre-registered
|
||||
* custom serializers.
|
||||
*/
|
||||
abstract class Proxy<T : Any, P : Any>(clazz: Class<T>,
|
||||
protected val proxyClass: Class<P>,
|
||||
protected val factory: SerializerFactory,
|
||||
withInheritance: Boolean = true) : CustomSerializerImp<T>(clazz, withInheritance) {
|
||||
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
|
||||
|
||||
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
|
||||
|
||||
override val schemaForDocumentation: Schema by lazy {
|
||||
val typeNotations = mutableSetOf<TypeNotation>(
|
||||
CompositeType(
|
||||
nameForType(type),
|
||||
null,
|
||||
emptyList(),
|
||||
descriptor, (proxySerializer.typeNotation as CompositeType).fields))
|
||||
for (additional in additionalSerializers) {
|
||||
typeNotations.addAll(additional.schemaForDocumentation.types)
|
||||
}
|
||||
Schema(typeNotations.toList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement these two methods.
|
||||
*/
|
||||
protected abstract fun toProxy(obj: T): P
|
||||
|
||||
protected abstract fun fromProxy(proxy: P): T
|
||||
|
||||
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
val proxy = toProxy(obj)
|
||||
data.withList {
|
||||
proxySerializer.propertySerializers.serializationOrder.forEach {
|
||||
it.getter.writeProperty(proxy, this, output, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): T {
|
||||
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schemas, input, context))
|
||||
return fromProxy(proxy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom serializer where the on-wire representation is a string. For example, a [Currency] might be represented
|
||||
* as a 3 character currency code, and converted to and from that string. By default, it is assumed that the
|
||||
* [toString] method will generate the string representation and that there is a constructor that takes such a
|
||||
* string as an argument to reconstruct.
|
||||
*
|
||||
* @param clazz The type to be marshalled
|
||||
* @param withInheritance Whether subclasses of the class can also be marshalled.
|
||||
* @param maker A lambda for constructing an instance, that defaults to calling a constructor that expects a string.
|
||||
* @param unmaker A lambda that extracts the string value for an instance, that defaults to the [toString] method.
|
||||
*/
|
||||
abstract class ToString<T : Any>(clazz: Class<T>, withInheritance: Boolean = false,
|
||||
private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` ->
|
||||
{ string -> `constructor`.newInstance(string) }
|
||||
},
|
||||
private val unmaker: (T) -> String = { obj -> obj.toString() })
|
||||
: CustomSerializerImp<T>(clazz, withInheritance) {
|
||||
|
||||
override val schemaForDocumentation = Schema(
|
||||
listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)),
|
||||
SerializerFactory.primitiveTypeName(String::class.java)!!,
|
||||
descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
data.putString(unmaker(obj))
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): T {
|
||||
val proxy = obj as String
|
||||
return maker(proxy)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.serialization.EncodingWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.*
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.amqp.UnsignedInteger
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.InputStream
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.TypeVariable
|
||||
import java.lang.reflect.WildcardType
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
data class ObjectAndEnvelope<out T>(val obj: T, val envelope: Envelope)
|
||||
|
||||
/**
|
||||
* Main entry point for deserializing an AMQP encoded object.
|
||||
*
|
||||
* @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple
|
||||
* instances and threads.
|
||||
*/
|
||||
class DeserializationInput @JvmOverloads constructor(private val serializerFactory: SerializerFactory,
|
||||
private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) {
|
||||
private val objectHistory: MutableList<Any> = mutableListOf()
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T> withDataBytes(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist, task: (ByteBuffer) -> T): T {
|
||||
// Check that the lead bytes match expected header
|
||||
val amqpSequence = amqpMagic.consume(byteSequence)
|
||||
?: throw NotSerializableException("Serialization header does not match.")
|
||||
var stream: InputStream = ByteBufferInputStream(amqpSequence)
|
||||
try {
|
||||
while (true) {
|
||||
when (SectionId.reader.readFrom(stream)) {
|
||||
SectionId.ENCODING -> {
|
||||
val encoding = CordaSerializationEncoding.reader.readFrom(stream)
|
||||
encodingWhitelist.acceptEncoding(encoding) || throw NotSerializableException(encodingNotPermittedFormat.format(encoding))
|
||||
stream = encoding.wrap(stream)
|
||||
}
|
||||
SectionId.DATA_AND_STOP, SectionId.ALT_DATA_AND_STOP -> return task(stream.asByteBuffer())
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun getEnvelope(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist): Envelope {
|
||||
return withDataBytes(byteSequence, encodingWhitelist) { dataBytes ->
|
||||
val data = Data.Factory.create()
|
||||
val expectedSize = dataBytes.remaining()
|
||||
if (data.decode(dataBytes) != expectedSize.toLong()) throw NotSerializableException("Unexpected size of data")
|
||||
Envelope.get(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun getEnvelope(byteSequence: ByteSequence) = getEnvelope(byteSequence, encodingWhitelist)
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>, context: SerializationContext): T =
|
||||
deserialize(bytes, T::class.java, context)
|
||||
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
private fun <R> des(generator: () -> R): R {
|
||||
try {
|
||||
return generator()
|
||||
} catch (nse: NotSerializableException) {
|
||||
throw nse
|
||||
} catch (t: Throwable) {
|
||||
throw NotSerializableException("Unexpected throwable: ${t.message}").apply { initCause(t) }
|
||||
} finally {
|
||||
objectHistory.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main entry point for deserialization of AMQP payloads, and expects a byte sequence involving a header
|
||||
* indicating what version of Corda serialization was used, followed by an [Envelope] which carries the object to
|
||||
* be deserialized and a schema describing the types of the objects.
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>, context: SerializationContext): T =
|
||||
des {
|
||||
val envelope = getEnvelope(bytes, encodingWhitelist)
|
||||
clazz.cast(readObjectOrNull(envelope.obj, SerializationSchemas(envelope.schema, envelope.transformsSchema),
|
||||
clazz, context))
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserializeAndReturnEnvelope(
|
||||
bytes: SerializedBytes<T>,
|
||||
clazz: Class<T>,
|
||||
context: SerializationContext
|
||||
): ObjectAndEnvelope<T> = des {
|
||||
val envelope = getEnvelope(bytes, encodingWhitelist)
|
||||
// Now pick out the obj and schema from the envelope.
|
||||
ObjectAndEnvelope(
|
||||
clazz.cast(readObjectOrNull(
|
||||
envelope.obj,
|
||||
SerializationSchemas(envelope.schema, envelope.transformsSchema),
|
||||
clazz,
|
||||
context)),
|
||||
envelope)
|
||||
}
|
||||
|
||||
internal fun readObjectOrNull(obj: Any?, schema: SerializationSchemas, type: Type, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj == null) null else readObject(obj, schema, type, context)
|
||||
}
|
||||
|
||||
internal fun readObject(obj: Any, schemas: SerializationSchemas, type: Type, context: SerializationContext): 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}' " +
|
||||
"@ $objectIndex")
|
||||
}
|
||||
objectRetrieved
|
||||
} else {
|
||||
val objectRead = when (obj) {
|
||||
is DescribedType -> {
|
||||
// Look up serializer in factory by descriptor
|
||||
val serializer = serializerFactory.get(obj.descriptor, schemas)
|
||||
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, schemas, this, context)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently performs checks aimed at:
|
||||
* * [java.util.List<Command<?>>] and [java.lang.Class<? extends net.corda.core.contracts.Contract>]
|
||||
* * [T : Parent] and [Parent]
|
||||
* * [? extends Parent] and [Parent]
|
||||
*
|
||||
* In the future tighter control might be needed
|
||||
*/
|
||||
private fun Type.materiallyEquivalentTo(that: Type): Boolean =
|
||||
when (that) {
|
||||
is ParameterizedType -> asClass() == that.asClass()
|
||||
is TypeVariable<*> -> isSubClassOf(that.bounds.first())
|
||||
is WildcardType -> isSubClassOf(that.upperBounds.first())
|
||||
else -> false
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import java.lang.reflect.GenericArrayType
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Implementation of [GenericArrayType] that we can actually construct.
|
||||
*/
|
||||
class DeserializedGenericArrayType(private val componentType: Type) : GenericArrayType {
|
||||
override fun getGenericComponentType(): Type = componentType
|
||||
override fun getTypeName(): String = "${componentType.typeName}[]"
|
||||
override fun toString(): String = typeName
|
||||
override fun hashCode(): Int = Objects.hashCode(componentType)
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is GenericArrayType && (componentType == other.genericComponentType)
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.lang.reflect.TypeVariable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Implementation of [ParameterizedType] that we can actually construct, and a parser from the string representation
|
||||
* of the JDK implementation which we use as the textual format in the AMQP schema.
|
||||
*/
|
||||
class DeserializedParameterizedType(private val rawType: Class<*>, private val params: Array<out Type>, private val ownerType: Type? = null) : ParameterizedType {
|
||||
init {
|
||||
if (params.isEmpty()) {
|
||||
throw NotSerializableException("Must be at least one parameter type in a ParameterizedType")
|
||||
}
|
||||
if (params.size != rawType.typeParameters.size) {
|
||||
throw NotSerializableException("Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun boundedType(type: TypeVariable<out Class<out Any>>): Boolean {
|
||||
return !(type.bounds.size == 1 && type.bounds[0] == Object::class.java)
|
||||
}
|
||||
|
||||
private val _typeName: String = makeTypeName()
|
||||
|
||||
private fun makeTypeName(): String {
|
||||
val paramsJoined = params.joinToString(", ") { it.typeName }
|
||||
return "${rawType.name}<$paramsJoined>"
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Maximum depth/nesting of generics before we suspect some DoS attempt.
|
||||
const val MAX_DEPTH: Int = 32
|
||||
|
||||
fun make(name: String, cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader): Type {
|
||||
val paramTypes = ArrayList<Type>()
|
||||
val pos = parseTypeList("$name>", paramTypes, cl)
|
||||
if (pos <= name.length) {
|
||||
throw NotSerializableException("Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.")
|
||||
}
|
||||
if (paramTypes.size != 1) {
|
||||
throw NotSerializableException("Expected only one type, but got $paramTypes")
|
||||
}
|
||||
return paramTypes[0]
|
||||
}
|
||||
|
||||
private fun parseTypeList(params: String, types: MutableList<Type>, cl: ClassLoader, depth: Int = 0): Int {
|
||||
var pos = 0
|
||||
var typeStart = 0
|
||||
var needAType = true
|
||||
var skippingWhitespace = false
|
||||
|
||||
while (pos < params.length) {
|
||||
if (params[pos] == '<') {
|
||||
val typeEnd = pos++
|
||||
val paramTypes = ArrayList<Type>()
|
||||
pos = parseTypeParams(params, pos, paramTypes, cl, depth + 1)
|
||||
types += makeParameterizedType(params.substring(typeStart, typeEnd).trim(), paramTypes, cl)
|
||||
typeStart = pos
|
||||
needAType = false
|
||||
} else if (params[pos] == ',') {
|
||||
val typeEnd = pos++
|
||||
val typeName = params.substring(typeStart, typeEnd).trim()
|
||||
if (!typeName.isEmpty()) {
|
||||
types += makeType(typeName, cl)
|
||||
} else if (needAType) {
|
||||
throw NotSerializableException("Expected a type, not ','")
|
||||
}
|
||||
typeStart = pos
|
||||
needAType = true
|
||||
} else if (params[pos] == '>') {
|
||||
val typeEnd = pos++
|
||||
val typeName = params.substring(typeStart, typeEnd).trim()
|
||||
if (!typeName.isEmpty()) {
|
||||
types += makeType(typeName, cl)
|
||||
} else if (needAType) {
|
||||
throw NotSerializableException("Expected a type, not '>'")
|
||||
}
|
||||
return pos
|
||||
} else {
|
||||
// Skip forwards, checking character types
|
||||
if (pos == typeStart) {
|
||||
skippingWhitespace = false
|
||||
if (params[pos].isWhitespace()) {
|
||||
typeStart = ++pos
|
||||
} else if (!needAType) {
|
||||
throw NotSerializableException("Not expecting a type")
|
||||
} else if (params[pos] == '?') {
|
||||
pos++
|
||||
} else if (!params[pos].isJavaIdentifierStart()) {
|
||||
throw NotSerializableException("Invalid character at start of type: ${params[pos]}")
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
} else {
|
||||
if (params[pos].isWhitespace()) {
|
||||
pos++
|
||||
skippingWhitespace = true
|
||||
} else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) {
|
||||
pos++
|
||||
} else {
|
||||
throw NotSerializableException("Invalid character ${params[pos]} in middle of type $params at idx $pos")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw NotSerializableException("Missing close generics '>'")
|
||||
}
|
||||
|
||||
private fun makeType(typeName: String, cl: ClassLoader): Type {
|
||||
// Not generic
|
||||
return if (typeName == "?") SerializerFactory.AnyType else {
|
||||
Primitives.wrap(SerializerFactory.primitiveType(typeName) ?: Class.forName(typeName, false, cl))
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeParameterizedType(rawTypeName: String, args: MutableList<Type>, cl: ClassLoader): Type {
|
||||
return DeserializedParameterizedType(makeType(rawTypeName, cl) as Class<*>, args.toTypedArray(), null)
|
||||
}
|
||||
|
||||
private fun parseTypeParams(params: String, startPos: Int, paramTypes: MutableList<Type>, cl: ClassLoader, depth: Int): Int {
|
||||
if (depth == MAX_DEPTH) {
|
||||
throw NotSerializableException("Maximum depth of nested generics reached: $depth")
|
||||
}
|
||||
return startPos + parseTypeList(params.substring(startPos), paramTypes, cl, depth)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRawType(): Type = rawType
|
||||
|
||||
override fun getOwnerType(): Type? = ownerType
|
||||
|
||||
override fun getActualTypeArguments(): Array<out Type> = params
|
||||
|
||||
override fun getTypeName(): String = _typeName
|
||||
|
||||
override fun toString(): String = _typeName
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Arrays.hashCode(this.actualTypeArguments) xor Objects.hashCode(this.ownerType) xor Objects.hashCode(this.rawType)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is ParameterizedType) {
|
||||
if (this === other) {
|
||||
true
|
||||
} else {
|
||||
this.ownerType == other.ownerType && this.rawType == other.rawType && Arrays.equals(this.actualTypeArguments, other.actualTypeArguments)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Used whenever a deserialized enums fingerprint doesn't match the fingerprint of the generated
|
||||
* serializer object. I.e. the deserializing code has a different version of the code either newer or
|
||||
* older). The changes will have been documented using the transformation annotations, a copy of which
|
||||
* are encoded as part of the AMQP envelope.
|
||||
*
|
||||
* This function ascertains which version of the enumeration is newer by comparing the length of the
|
||||
* transformations list. Since transformation annotations should only ever be added, never removed even
|
||||
* when seemingly unneeded (such as repeated renaming of a single constant), the longer list will dictate
|
||||
* which is more up to date.
|
||||
*
|
||||
* The list of transforms come from two places, the class as it exists on the current class path and the
|
||||
* class as it exists as it was serialized. In the case of the former we can build the list by using
|
||||
* reflection on the class. In the case of the latter the transforms are retrieved from the AMQP envelope.
|
||||
*
|
||||
* With a set of transforms chosen we calculate the set of all possible constants, then using the
|
||||
* transformation rules we create a mapping between those values and the values that exist on the
|
||||
* current class
|
||||
*
|
||||
* @property type The enum as it exists now, not as it did when it was serialized (either in the past
|
||||
* or future).
|
||||
* @param factory the [SerializerFactory] that is building this serialization object.
|
||||
* @property conversions A mapping between all potential enum constants that could've been assigned to
|
||||
* an instance of the enum as it existed at time of serialisation and those that exist now
|
||||
* @property ordinals Convenience mapping of constant to ordinality
|
||||
*/
|
||||
class EnumEvolutionSerializer(
|
||||
override val type: Type,
|
||||
factory: SerializerFactory,
|
||||
private val conversions: Map<String, String>,
|
||||
private val ordinals: Map<String, Int>) : AMQPSerializer<Any> {
|
||||
override val typeDescriptor = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
|
||||
companion object {
|
||||
private fun MutableMap<String, String>.mapInPlace(f: (String) -> String) {
|
||||
val i = iterator()
|
||||
while (i.hasNext()) {
|
||||
val curr = i.next()
|
||||
curr.setValue(f(curr.value))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an Enum Evolver serializer.
|
||||
*
|
||||
* @param old The description of the enum as it existed at the time of serialisation taken from the
|
||||
* received AMQP header
|
||||
* @param new The Serializer object we built based on the current state of the enum class on our classpath
|
||||
* @param factory the [SerializerFactory] that is building this serialization object.
|
||||
* @param schemas the transforms attached to the class in the AMQP header, i.e. the transforms
|
||||
* known at serialization time
|
||||
*/
|
||||
fun make(old: RestrictedType,
|
||||
new: AMQPSerializer<Any>,
|
||||
factory: SerializerFactory,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||
val wireTransforms = schemas.transforms.types[old.name]
|
||||
?: EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
val localTransforms = TransformsSchema.get(old.name, factory)
|
||||
|
||||
// remember, the longer the list the newer we're assuming the transform set it as we assume
|
||||
// evolution annotations are never removed, only added to
|
||||
val transforms = if (wireTransforms.size > localTransforms.size) wireTransforms else localTransforms
|
||||
|
||||
// if either of these isn't of the cast type then something has gone terribly wrong
|
||||
// elsewhere in the code
|
||||
val defaultRules: List<EnumDefaultSchemaTransform>? = uncheckedCast(transforms[TransformTypes.EnumDefault])
|
||||
val renameRules: List<RenameSchemaTransform>? = uncheckedCast(transforms[TransformTypes.Rename])
|
||||
|
||||
// What values exist on the enum as it exists on the class path
|
||||
val localValues = new.type.asClass()!!.enumConstants.map { it.toString() }
|
||||
|
||||
val conversions: MutableMap<String, String> = localValues
|
||||
.union(defaultRules?.map { it.new }?.toSet() ?: emptySet())
|
||||
.union(renameRules?.map { it.to } ?: emptySet())
|
||||
.associateBy({ it }, { it })
|
||||
.toMutableMap()
|
||||
|
||||
val rules: MutableMap<String, String> = mutableMapOf()
|
||||
rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap())
|
||||
val renameRulesMap = renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()
|
||||
rules.putAll(renameRulesMap)
|
||||
|
||||
// take out set of all possible constants and build a map from those to the
|
||||
// existing constants applying the rename and defaulting rules as defined
|
||||
// in the schema
|
||||
while (conversions.filterNot { it.value in localValues }.isNotEmpty()) {
|
||||
conversions.mapInPlace { rules[it] ?: it }
|
||||
}
|
||||
|
||||
// you'd think this was overkill to get access to the ordinal values for each constant but it's actually
|
||||
// rather tricky when you don't have access to the actual type, so this is a nice way to be able
|
||||
// to precompute and pass to the actual object
|
||||
val ordinals = localValues.mapIndexed { i, s -> Pair(s, i) }.toMap()
|
||||
|
||||
// create a mapping between the ordinal value and the name as it was serialised converted
|
||||
// to the name as it exists. We want to test any new constants have been added to the end
|
||||
// of the enum class
|
||||
val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices
|
||||
.associateBy({ it.value.toInt() }, { conversions[it.name] }))
|
||||
|
||||
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
|
||||
throw NotSerializableException("Constants have been reordered, additions must be appended to the end")
|
||||
}
|
||||
|
||||
return EnumEvolutionSerializer(new.type, factory, conversions, ordinals)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
val enumName = (obj as List<*>)[0] as String
|
||||
|
||||
if (enumName !in conversions) {
|
||||
throw NotSerializableException("No rule to evolve enum constant $type::$enumName")
|
||||
}
|
||||
|
||||
return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!]
|
||||
}
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
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
|
||||
private val typeNotation: TypeNotation
|
||||
override val typeDescriptor = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
|
||||
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, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
val enumName = (obj as List<*>)[0] as String
|
||||
val enumOrd = obj[1] as Int
|
||||
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?
|
||||
|
||||
if (enumName != fromOrd?.name) {
|
||||
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,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
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,70 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
||||
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
* 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 avoid excessive duplication.
|
||||
*/
|
||||
// TODO: make the schema parsing lazy since mostly schemas will have been seen before and we only need it if we
|
||||
// TODO: don't recognise a type descriptor.
|
||||
data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: TransformsSchema) : DescribedType {
|
||||
companion object : DescribedTypeConstructor<Envelope> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.ENVELOPE.amqpDescriptor
|
||||
val DESCRIPTOR_OBJECT = Descriptor(null, DESCRIPTOR)
|
||||
|
||||
// described list should either be two or three elements long
|
||||
private const val ENVELOPE_WITHOUT_TRANSFORMS = 2
|
||||
private const val ENVELOPE_WITH_TRANSFORMS = 3
|
||||
|
||||
private const val BLOB_IDX = 0
|
||||
private const val SCHEMA_IDX = 1
|
||||
private const val TRANSFORMS_SCHEMA_IDX = 2
|
||||
|
||||
fun get(data: Data): Envelope {
|
||||
val describedType = data.`object` as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}, should be $DESCRIPTOR.")
|
||||
}
|
||||
val list = describedType.described as List<*>
|
||||
|
||||
// We need to cope with objects serialised without the transforms header element in the
|
||||
// envelope
|
||||
val transformSchema: Any? = when (list.size) {
|
||||
ENVELOPE_WITHOUT_TRANSFORMS -> null
|
||||
ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX]
|
||||
else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)")
|
||||
}
|
||||
|
||||
return newInstance(listOf(list[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!),
|
||||
TransformsSchema.newInstance(transformSchema)))
|
||||
}
|
||||
|
||||
// This separation of functions is needed as this will be the entry point for the default
|
||||
// AMQP decoder if one is used (see the unit tests).
|
||||
override fun newInstance(described: Any?): Envelope {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
|
||||
// We need to cope with objects serialised without the transforms header element in the
|
||||
// envelope
|
||||
val transformSchema = when (list.size) {
|
||||
ENVELOPE_WITHOUT_TRANSFORMS -> TransformsSchema.newInstance(null)
|
||||
ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema
|
||||
else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)")
|
||||
}
|
||||
|
||||
return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = Envelope::class.java
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(obj, schema, transformsSchema)
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
|
||||
/**
|
||||
* Serializer for deserializing objects whose definition has changed since they
|
||||
* were serialised.
|
||||
*
|
||||
* @property oldReaders A linked map representing the properties of the object as they were serialized. Note
|
||||
* this may contain properties that are no longer needed by the class. These *must* be read however to ensure
|
||||
* any refferenced objects in the object stream are captured properly
|
||||
* @property kotlinConstructor
|
||||
* @property constructorArgs used to hold the properties as sent to the object's constructor. Passed in as a
|
||||
* pre populated array as properties not present on the old constructor must be initialised in the factory
|
||||
*/
|
||||
abstract class EvolutionSerializer(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
protected val oldReaders: Map<String, OldParam>,
|
||||
override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) {
|
||||
|
||||
// explicitly set as empty to indicate it's unused by this type of serializer
|
||||
override val propertySerializers = PropertySerializersEvolution()
|
||||
|
||||
/**
|
||||
* Represents a parameter as would be passed to the constructor of the class as it was
|
||||
* when it was serialised and NOT how that class appears now
|
||||
*
|
||||
* @param resultsIndex index into the constructor argument list where the read property
|
||||
* should be placed
|
||||
* @param property object to read the actual property value
|
||||
*/
|
||||
data class OldParam(var resultsIndex: Int, val property: PropertySerializer) {
|
||||
fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
new: Array<Any?>, context: SerializationContext
|
||||
) = property.readProperty(obj, schemas, input, context).apply {
|
||||
if (resultsIndex >= 0) {
|
||||
new[resultsIndex] = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Unlike the generic deserialization case where we need to locate the primary constructor
|
||||
* for the object (or our best guess) in the case of an object whose structure has changed
|
||||
* since serialisation we need to attempt to locate a constructor that we can use. For example,
|
||||
* its parameters match the serialised members and it will initialise any newly added
|
||||
* elements.
|
||||
*
|
||||
* TODO: Type evolution
|
||||
* TODO: rename annotation
|
||||
*/
|
||||
private fun getEvolverConstructor(type: Type, oldArgs: Map<String, OldParam>): KFunction<Any>? {
|
||||
val clazz: Class<*> = type.asClass()!!
|
||||
|
||||
if (!isConcrete(clazz)) return null
|
||||
|
||||
val oldArgumentSet = oldArgs.map { Pair(it.key as String?, it.value.property.resolvedType) }
|
||||
|
||||
var maxConstructorVersion = Integer.MIN_VALUE
|
||||
var constructor: KFunction<Any>? = null
|
||||
clazz.kotlin.constructors.forEach {
|
||||
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
|
||||
if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) }) &&
|
||||
version > maxConstructorVersion) {
|
||||
constructor = it
|
||||
maxConstructorVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't get an exact match revert to existing behaviour, if the new parameters
|
||||
// are not mandatory (i.e. nullable) things are fine
|
||||
return constructor ?: constructorForDeserialization(type)
|
||||
}
|
||||
|
||||
private fun makeWithConstructor(
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory,
|
||||
constructor: KFunction<Any>,
|
||||
readersAsSerialized: Map<String, OldParam>): AMQPSerializer<Any> {
|
||||
val constructorArgs = arrayOfNulls<Any?>(constructor.parameters.size)
|
||||
|
||||
constructor.parameters.withIndex().forEach {
|
||||
readersAsSerialized[it.value.name!!]?.apply {
|
||||
this.resultsIndex = it.index
|
||||
} ?: if (!it.value.type.isMarkedNullable) {
|
||||
throw NotSerializableException(
|
||||
"New parameter ${it.value.name} is mandatory, should be nullable for evolution to worK")
|
||||
}
|
||||
}
|
||||
return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor, constructorArgs)
|
||||
}
|
||||
|
||||
private fun makeWithSetters(
|
||||
new: ObjectSerializer,
|
||||
factory: SerializerFactory,
|
||||
constructor: KFunction<Any>,
|
||||
readersAsSerialized: Map<String, OldParam>,
|
||||
classProperties: Map<String, PropertyDescriptor>): AMQPSerializer<Any> {
|
||||
val setters = propertiesForSerializationFromSetters(classProperties,
|
||||
new.type,
|
||||
factory).associateBy({ it.getter.name }, { it })
|
||||
return EvolutionSerializerViaSetters(new.type, factory, readersAsSerialized, constructor, setters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a serialization object for deserialization only of objects serialised
|
||||
* as different versions of a class.
|
||||
*
|
||||
* @param old is an object holding the schema that represents the object
|
||||
* as it was serialised and the type descriptor of that type
|
||||
* @param new is the Serializer built for the Class as it exists now, not
|
||||
* how it was serialised and persisted.
|
||||
* @param factory the [SerializerFactory] associated with the serialization
|
||||
* context this serializer is being built for
|
||||
*/
|
||||
fun make(old: CompositeType, new: ObjectSerializer,
|
||||
factory: SerializerFactory): AMQPSerializer<Any> {
|
||||
// The order in which the properties were serialised is important and must be preserved
|
||||
val readersAsSerialized = LinkedHashMap<String, OldParam>()
|
||||
old.fields.forEach {
|
||||
readersAsSerialized[it.name] = try {
|
||||
OldParam(-1, PropertySerializer.make(it.name, EvolutionPropertyReader(),
|
||||
it.getTypeAsClass(factory.classloader), factory))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw NotSerializableException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// cope with the situation where a generic interface was serialised as a type, in such cases
|
||||
// return the synthesised object which is, given the absence of a constructor, a no op
|
||||
val constructor = getEvolverConstructor(new.type, readersAsSerialized) ?: return new
|
||||
|
||||
val classProperties = new.type.asClass()?.propertyDescriptors() ?: emptyMap()
|
||||
|
||||
return if (classProperties.isNotEmpty() && constructor.parameters.isEmpty()) {
|
||||
makeWithSetters(new, factory, constructor, readersAsSerialized, classProperties)
|
||||
} else {
|
||||
makeWithConstructor(new, factory, constructor, readersAsSerialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
|
||||
}
|
||||
}
|
||||
|
||||
class EvolutionSerializerViaConstructor(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
oldReaders: Map<String, EvolutionSerializer.OldParam>,
|
||||
kotlinConstructor: KFunction<Any>?,
|
||||
private val constructorArgs: Array<Any?>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) {
|
||||
/**
|
||||
* Unlike a normal [readObject] call where we simply apply the parameter deserialisers
|
||||
* to the object list of values we need to map that list, which is ordered per the
|
||||
* constructor of the original state of the object, we need to map the new parameter order
|
||||
* of the current constructor onto that list inserting nulls where new parameters are
|
||||
* encountered.
|
||||
*
|
||||
* TODO: Object references
|
||||
*/
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
// *must* read all the parameters in the order they were serialized
|
||||
oldReaders.values.zip(obj).map { it.first.readProperty(it.second, schemas, input, constructorArgs, context) }
|
||||
|
||||
return javaConstructor?.newInstance(*(constructorArgs)) ?: throw NotSerializableException(
|
||||
"Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific instance of an [EvolutionSerializer] where the properties of the object are set via calling
|
||||
* named setter functions on the instantiated object.
|
||||
*/
|
||||
class EvolutionSerializerViaSetters(
|
||||
clazz: Type,
|
||||
factory: SerializerFactory,
|
||||
oldReaders: Map<String, EvolutionSerializer.OldParam>,
|
||||
kotlinConstructor: KFunction<Any>?,
|
||||
private val setters: Map<String, PropertyAccessor>) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) {
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any {
|
||||
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
|
||||
val instance: Any = javaConstructor?.newInstance() ?: throw NotSerializableException(
|
||||
"Failed to instantiate instance of object $clazz")
|
||||
|
||||
// *must* read all the parameters in the order they were serialized
|
||||
oldReaders.values.zip(obj).forEach {
|
||||
// if that property still exists on the new object then set it
|
||||
it.first.property.readProperty(it.second, schemas, input, context).apply {
|
||||
setters[it.first.property.name]?.set(instance, this)
|
||||
}
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances of this type are injected into a [SerializerFactory] at creation time to dictate the
|
||||
* behaviour of evolution within that factory. Under normal circumstances this will simply
|
||||
* be an object that returns an [EvolutionSerializer]. Of course, any implementation that
|
||||
* extends this class can be written to invoke whatever behaviour is desired.
|
||||
*/
|
||||
abstract class EvolutionSerializerGetterBase {
|
||||
abstract fun getEvolutionSerializer(
|
||||
factory: SerializerFactory,
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any>
|
||||
}
|
||||
|
||||
/**
|
||||
* The normal use case for generating an [EvolutionSerializer]'s based on the differences
|
||||
* between the received schema and the class as it exists now on the class path,
|
||||
*/
|
||||
class EvolutionSerializerGetter : EvolutionSerializerGetterBase() {
|
||||
override fun getEvolutionSerializer(factory: SerializerFactory,
|
||||
typeNotation: TypeNotation,
|
||||
newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas): AMQPSerializer<Any> {
|
||||
return factory.serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
|
||||
when (typeNotation) {
|
||||
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, factory)
|
||||
is RestrictedType -> {
|
||||
// The fingerprint of a generic collection can be changed through bug fixes to the
|
||||
// fingerprinting function making it appear as if the class has altered whereas it hasn't.
|
||||
// Given we don't support the evolution of these generic containers, if it appears
|
||||
// one has been changed, simply return the original serializer and associate it with
|
||||
// both the new and old fingerprint
|
||||
if (newSerializer is CollectionSerializer || newSerializer is MapSerializer) {
|
||||
newSerializer
|
||||
} else {
|
||||
EnumEvolutionSerializer.make(typeNotation, newSerializer, factory, schemas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,202 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import com.google.common.hash.Hasher
|
||||
import com.google.common.hash.Hashing
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toBase64
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory]
|
||||
*/
|
||||
interface FingerPrinter {
|
||||
/**
|
||||
* Return a unique identifier for a type, usually this will take into account the constituent elements
|
||||
* of said type such that any modification to any sub element wll generate a different fingerprint
|
||||
*/
|
||||
fun fingerprint(type: Type): String
|
||||
|
||||
/**
|
||||
* If required, associate an instance of the fingerprinter with a specific serializer factory
|
||||
*/
|
||||
fun setOwner(factory: SerializerFactory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the finger printing mechanism used by default
|
||||
*/
|
||||
class SerializerFingerPrinter : FingerPrinter {
|
||||
private var factory: SerializerFactory? = null
|
||||
|
||||
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"
|
||||
private val ANY_TYPE_HASH: String = "Any type = true"
|
||||
private val TYPE_VARIABLE_HASH: String = "Type variable = true"
|
||||
private val WILDCARD_TYPE_HASH: String = "Wild card = true"
|
||||
|
||||
private val logger by lazy { loggerFor<Schema>() }
|
||||
|
||||
override fun setOwner(factory: SerializerFactory) {
|
||||
this.factory = factory
|
||||
}
|
||||
|
||||
/**
|
||||
* The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation.
|
||||
* Thus it only takes into account properties and types and only supports the same object graph subset as the overall
|
||||
* serialization code.
|
||||
*
|
||||
* The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be
|
||||
* different.
|
||||
*/
|
||||
override fun fingerprint(type: Type): String {
|
||||
return fingerprintForType(
|
||||
type, null, HashSet(), Hashing.murmur3_128().newHasher(), debugIndent = 1).hash().asBytes().toBase64()
|
||||
}
|
||||
|
||||
private fun isCollectionOrMap(type: Class<*>) =
|
||||
(Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type))
|
||||
&& !EnumSet::class.java.isAssignableFrom(type)
|
||||
|
||||
internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String {
|
||||
val hasher = Hashing.murmur3_128().newHasher()
|
||||
for (typeDescriptor in typeDescriptors) {
|
||||
hasher.putUnencodedChars(typeDescriptor)
|
||||
}
|
||||
return hasher.hash().asBytes().toBase64()
|
||||
}
|
||||
|
||||
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) {
|
||||
putUnencodedChars(customSerializer.typeDescriptor)
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
// This method concatenates various elements of the types recursively as unencoded strings into the hasher,
|
||||
// effectively creating a unique string for a type which we then hash in the calling function above.
|
||||
private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>,
|
||||
hasher: Hasher, debugIndent: Int = 1): Hasher {
|
||||
// We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
|
||||
// generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
|
||||
// and deserializing (assuming deserialization is occurring in a factory that didn't
|
||||
// serialise the object in the first place (and thus the cache lookup fails). This is also
|
||||
// true of Any, where we need Example<A, B> and Example<?, ?> to have the same fingerprint
|
||||
return if ((type in alreadySeen)
|
||||
&& (type !== SerializerFactory.AnyType)
|
||||
&& (type !is TypeVariable<*>)
|
||||
&& (type !is WildcardType)) {
|
||||
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
|
||||
} else {
|
||||
alreadySeen += type
|
||||
try {
|
||||
when (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!!, debugIndent + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// ... and concatenate the type data for each parameter type.
|
||||
type.actualTypeArguments.fold(startingHash) { orig, paramType ->
|
||||
fingerprintForType(paramType, type, alreadySeen, orig, debugIndent + 1)
|
||||
}
|
||||
}
|
||||
// Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing
|
||||
// the signature of the fingerprinted object. This, however, doesn't work as it breaks bi-
|
||||
// directional fingerprints. That is, fingerprinting a concrete instance of a generic
|
||||
// type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>)
|
||||
//
|
||||
// On serialization Example<Int> is treated as Example<T>, a TypeVariable
|
||||
// On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable
|
||||
// Note: AnyType is a special case of WildcardType used in other parts of the
|
||||
// serializer so both cases need to be dealt with here
|
||||
//
|
||||
// If we treat these types as fundamentally different and alter the fingerprint we will
|
||||
// end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter.
|
||||
is SerializerFactory.AnyType,
|
||||
is WildcardType,
|
||||
is TypeVariable<*> -> {
|
||||
hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH)
|
||||
}
|
||||
is Class<*> -> {
|
||||
if (type.isArray) {
|
||||
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, debugIndent + 1)
|
||||
.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.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!!, debugIndent + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hash the element type + some array hash
|
||||
is GenericArrayType -> {
|
||||
fingerprintForType(type.genericComponentType, contextType, alreadySeen,
|
||||
hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH)
|
||||
}
|
||||
else -> throw NotSerializableException("Don't know how to hash")
|
||||
}
|
||||
} catch (e: NotSerializableException) {
|
||||
val msg = "${e.message} -> $type"
|
||||
logger.error(msg, e)
|
||||
throw NotSerializableException(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fingerprintForObject(
|
||||
type: Type,
|
||||
contextType: Type?,
|
||||
alreadySeen: MutableSet<Type>,
|
||||
hasher: Hasher,
|
||||
factory: SerializerFactory,
|
||||
debugIndent: Int = 0): Hasher {
|
||||
// Hash the class + properties + interfaces
|
||||
val name = type.asClass()?.name
|
||||
?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
|
||||
|
||||
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory)
|
||||
.serializationOrder
|
||||
.fold(hasher.putUnencodedChars(name)) { orig, prop ->
|
||||
fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, debugIndent + 1)
|
||||
.putUnencodedChars(prop.getter.name)
|
||||
.putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
|
||||
}
|
||||
interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, debugIndent + 1) }
|
||||
return hasher
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *>
|
||||
|
||||
/**
|
||||
* Serialization / deserialization of certain supported [Map] types.
|
||||
*/
|
||||
class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
|
||||
override val type: Type = (declaredType as? DeserializedParameterizedType)
|
||||
?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType), factory.classloader)
|
||||
override val typeDescriptor: Symbol = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
|
||||
|
||||
companion object {
|
||||
// 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)) },
|
||||
NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) },
|
||||
// concrete classes for user convenience
|
||||
LinkedHashMap::class.java to { map -> LinkedHashMap(map) },
|
||||
TreeMap::class.java to { map -> TreeMap(map) },
|
||||
EnumMap::class.java to { map ->
|
||||
EnumMap(uncheckedCast<Map<*, *>, Map<EnumJustUsedForCasting, Any>>(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 {
|
||||
declaredClass.checkSupportedMapType()
|
||||
if (supportedTypes.containsKey(declaredClass)) {
|
||||
// Simple case - it is already known to be a map.
|
||||
return deriveParametrizedType(declaredType, uncheckedCast(declaredClass))
|
||||
} 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: MapCreationFunction = findConcreteType(declaredType.rawType as Class<*>)
|
||||
|
||||
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
if (output.writeTypeNotations(typeNotation)) {
|
||||
output.requireSerializer(declaredType.actualTypeArguments[0])
|
||||
output.requireSerializer(declaredType.actualTypeArguments[1])
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
context: SerializationContext,
|
||||
debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
obj.javaClass.checkSupportedMapType()
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
// Write map
|
||||
data.putMap()
|
||||
data.enter()
|
||||
for ((key, value) in obj as Map<*, *>) {
|
||||
output.writeObjectOrNull(key, data, declaredType.actualTypeArguments[0], context, debugIndent)
|
||||
output.writeObjectOrNull(value, data, declaredType.actualTypeArguments[1], context, debugIndent)
|
||||
}
|
||||
data.exit() // exit map
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
|
||||
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schemas, input, it, context) }
|
||||
concreteBuilder(entries.toMap())
|
||||
}
|
||||
|
||||
private fun readEntry(schemas: SerializationSchemas, input: DeserializationInput, entry: Map.Entry<Any?, Any?>,
|
||||
context: SerializationContext
|
||||
) = input.readObjectOrNull(entry.key, schemas, declaredType.actualTypeArguments[0], context) to
|
||||
input.readObjectOrNull(entry.value, schemas, declaredType.actualTypeArguments[1], context)
|
||||
|
||||
// Cannot use * as a bound for EnumMap and EnumSet since * is not an enum. So, we use a sample enum instead.
|
||||
// We don't actually care about the type, we just need to make the compiler happier.
|
||||
internal enum class EnumJustUsedForCasting { NOT_USED }
|
||||
}
|
||||
|
||||
internal fun Class<*>.checkSupportedMapType() {
|
||||
if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) {
|
||||
throw IllegalArgumentException(
|
||||
"Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.")
|
||||
} else if (WeakHashMap::class.java.isAssignableFrom(this)) {
|
||||
throw IllegalArgumentException("Weak references with map types not supported. Suggested fix: "
|
||||
+ "use java.util.LinkedHashMap instead.")
|
||||
} else if (Dictionary::class.java.isAssignableFrom(this)) {
|
||||
throw IllegalArgumentException(
|
||||
"Unable to serialise deprecated type $this. Suggested fix: prefer java.util.map implementations")
|
||||
}
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
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.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
|
||||
/**
|
||||
* Responsible for serializing and deserializing a regular object instance via a series of properties
|
||||
* (matched with a constructor).
|
||||
*/
|
||||
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 }
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
internal open val propertySerializers: PropertySerializers by lazy {
|
||||
propertiesForSerialization(kotlinConstructor, clazz, factory)
|
||||
}
|
||||
|
||||
fun getPropertySerializers() = propertySerializers
|
||||
|
||||
private val typeName = nameForType(clazz)
|
||||
|
||||
override val typeDescriptor = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
|
||||
// We restrict to only those annotated or whitelisted
|
||||
private val interfaces = interfacesForSerialization(clazz, factory)
|
||||
|
||||
internal open val typeNotation: TypeNotation by lazy {
|
||||
CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor), generateFields())
|
||||
}
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
if (output.writeTypeNotations(typeNotation)) {
|
||||
for (iface in interfaces) {
|
||||
output.requireSerializer(iface)
|
||||
}
|
||||
|
||||
propertySerializers.serializationOrder.forEach { property ->
|
||||
property.getter.writeClassInfo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(
|
||||
obj: Any,
|
||||
data: Data,
|
||||
type: Type,
|
||||
output: SerializationOutput,
|
||||
context: SerializationContext,
|
||||
debugIndent: Int) = ifThrowsAppend({ clazz.typeName }
|
||||
) {
|
||||
if (propertySerializers.size != javaConstructor?.parameterCount &&
|
||||
javaConstructor?.parameterCount ?: 0 > 0
|
||||
) {
|
||||
throw NotSerializableException("Serialization constructor for class $type expects "
|
||||
+ "${javaConstructor?.parameterCount} parameters but we have ${propertySerializers.size} "
|
||||
+ "properties to serialize.")
|
||||
}
|
||||
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
// Write list
|
||||
withList {
|
||||
propertySerializers.serializationOrder.forEach { property ->
|
||||
property.getter.writeProperty(obj, this, output, context, debugIndent + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(
|
||||
obj: Any,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
if (obj is List<*>) {
|
||||
if (obj.size > propertySerializers.size) {
|
||||
throw NotSerializableException("Too many properties in described type $typeName")
|
||||
}
|
||||
|
||||
return if (propertySerializers.byConstructor) {
|
||||
readObjectBuildViaConstructor(obj, schemas, input, context)
|
||||
} else {
|
||||
readObjectBuildViaSetters(obj, schemas, input, context)
|
||||
}
|
||||
} else {
|
||||
throw NotSerializableException("Body of described type is unexpected $obj")
|
||||
}
|
||||
}
|
||||
|
||||
private fun readObjectBuildViaConstructor(
|
||||
obj: List<*>,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
logger.trace { "Calling construction based construction for ${clazz.typeName}" }
|
||||
|
||||
return construct(propertySerializers.serializationOrder
|
||||
.zip(obj)
|
||||
.map { Pair(it.first.initialPosition, it.first.getter.readProperty(it.second, schemas, input, context)) }
|
||||
.sortedWith(compareBy({ it.first }))
|
||||
.map { it.second })
|
||||
}
|
||||
|
||||
private fun readObjectBuildViaSetters(
|
||||
obj: List<*>,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
logger.trace { "Calling setter based construction for ${clazz.typeName}" }
|
||||
|
||||
val instance: Any = javaConstructor?.newInstance() ?: throw NotSerializableException(
|
||||
"Failed to instantiate instance of object $clazz")
|
||||
|
||||
// read the properties out of the serialised form, since we're invoking the setters the order we
|
||||
// do it in doesn't matter
|
||||
val propertiesFromBlob = obj
|
||||
.zip(propertySerializers.serializationOrder)
|
||||
.map { it.second.getter.readProperty(it.first, schemas, input, context) }
|
||||
|
||||
// one by one take a property and invoke the setter on the class
|
||||
propertySerializers.serializationOrder.zip(propertiesFromBlob).forEach {
|
||||
it.first.set(instance, it.second)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
private fun generateFields(): List<Field> {
|
||||
return propertySerializers.serializationOrder.map {
|
||||
Field(it.getter.name, it.getter.type, it.getter.requires, it.getter.default, null, it.getter.mandatory, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateProvides(): List<String> = interfaces.map { nameForType(it) }
|
||||
|
||||
fun construct(properties: List<Any?>): Any {
|
||||
logger.trace { "Calling constructor: '$javaConstructor' with properties '$properties'" }
|
||||
|
||||
if (properties.size != javaConstructor?.parameterCount) {
|
||||
throw NotSerializableException("Serialization constructor for class $type expects "
|
||||
+ "${javaConstructor?.parameterCount} parameters but we have ${properties.size} "
|
||||
+ "serialized properties.")
|
||||
}
|
||||
|
||||
return javaConstructor?.newInstance(*properties.toTypedArray())
|
||||
?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Base class for serialization of a property of an object.
|
||||
*/
|
||||
sealed class PropertySerializer(val name: String, val propertyReader: PropertyReader, val resolvedType: Type) {
|
||||
abstract fun writeClassInfo(output: SerializationOutput)
|
||||
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput, context: SerializationContext, debugIndent: Int = 0)
|
||||
abstract fun readProperty(obj: Any?, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext): Any?
|
||||
|
||||
val type: String = generateType()
|
||||
val requires: List<String> = generateRequires()
|
||||
val default: String? = generateDefault()
|
||||
val mandatory: Boolean = generateMandatory()
|
||||
|
||||
private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface == true
|
||||
private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive == true
|
||||
|
||||
private fun generateType(): String {
|
||||
return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType)
|
||||
}
|
||||
|
||||
private fun generateRequires(): List<String> {
|
||||
return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList()
|
||||
}
|
||||
|
||||
private fun generateDefault(): String? =
|
||||
if (isJVMPrimitive) {
|
||||
when (resolvedType) {
|
||||
java.lang.Boolean.TYPE -> "false"
|
||||
java.lang.Character.TYPE -> "�"
|
||||
else -> "0"
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private fun generateMandatory(): Boolean {
|
||||
return isJVMPrimitive || !(propertyReader.isNullable())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun make(name: String, readMethod: PropertyReader, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
return if (SerializerFactory.isPrimitive(resolvedType)) {
|
||||
when (resolvedType) {
|
||||
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
|
||||
else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType)
|
||||
}
|
||||
} else {
|
||||
DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for a complex type (another object).
|
||||
*/
|
||||
class DescribedTypePropertySerializer(
|
||||
name: String,
|
||||
readMethod: PropertyReader,
|
||||
resolvedType: Type,
|
||||
private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
// This is lazy so we don't get an infinite loop when a method returns an instance of the class.
|
||||
private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() }
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
||||
if (resolvedType != Any::class.java) {
|
||||
typeSerializer.writeClassInfo(output)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readProperty(
|
||||
obj: Any?,
|
||||
schemas: SerializationSchemas,
|
||||
input: DeserializationInput,
|
||||
context: SerializationContext): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schemas, resolvedType, context)
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int) = ifThrowsAppend({ nameForDebug }
|
||||
) {
|
||||
output.writeObjectOrNull(propertyReader.read(obj), data, resolvedType, context, debugIndent)
|
||||
}
|
||||
|
||||
private val nameForDebug = "$name(${resolvedType.typeName})"
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for most AMQP primitive type (Int, String, etc).
|
||||
*/
|
||||
class AMQPPrimitivePropertySerializer(
|
||||
name: String,
|
||||
readMethod: PropertyReader,
|
||||
resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
|
||||
input: DeserializationInput, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj is Binary) obj.array else obj
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val value = propertyReader.read(obj)
|
||||
if (value is ByteArray) {
|
||||
data.putObject(Binary(value))
|
||||
} else {
|
||||
data.putObject(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property serializer for the AMQP char type, needed as a specialisation as the underlying
|
||||
* value of the character is stored in numeric UTF-16 form and on deserialization requires explicit
|
||||
* casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs
|
||||
*/
|
||||
class AMQPCharPropertySerializer(name: String, readMethod: PropertyReader) :
|
||||
PropertySerializer(name, readMethod, Character::class.java) {
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schemas: SerializationSchemas,
|
||||
input: DeserializationInput, context: SerializationContext
|
||||
): Any? {
|
||||
return if (obj == null) null else (obj as Short).toChar()
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
val input = propertyReader.read(obj)
|
||||
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,218 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
import kotlin.reflect.jvm.kotlinProperty
|
||||
|
||||
abstract class PropertyReader {
|
||||
abstract fun read(obj: Any?): Any?
|
||||
abstract fun isNullable(): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for those properties of a class that have defined getter functions.
|
||||
*/
|
||||
class PublicPropertyReader(private val readMethod: Method?) : PropertyReader() {
|
||||
init {
|
||||
readMethod?.isAccessible = true
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean {
|
||||
try {
|
||||
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull {
|
||||
it.javaGetter == this
|
||||
}?.returnType?.toString() ?: "?"
|
||||
|
||||
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
// So this used to report as an error, but given we serialise exceptions all the time it
|
||||
// provides for very scary log files so move this to trace level
|
||||
loggerFor<PropertySerializer>().let { logger ->
|
||||
logger.trace("Using kotlin introspection on internal type ${this.declaringClass}")
|
||||
logger.trace("Unexpected internal Kotlin error", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? {
|
||||
return readMethod!!.invoke(obj)
|
||||
}
|
||||
|
||||
override fun isNullable(): Boolean = readMethod?.returnsNullable() ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for those properties of a class that do not have defined getter functions. In which case
|
||||
* we used reflection to remove the unreadable status from that property whilst it's accessed.
|
||||
*/
|
||||
class PrivatePropertyReader(val field: Field, parentType: Type) : PropertyReader() {
|
||||
init {
|
||||
loggerFor<PropertySerializer>().warn("Create property Serializer for private property '${field.name}' not "
|
||||
+ "exposed by a getter on class '$parentType'\n"
|
||||
+ "\tNOTE: This behaviour will be deprecated at some point in the future and a getter required")
|
||||
}
|
||||
|
||||
override fun read(obj: Any?): Any? {
|
||||
field.isAccessible = true
|
||||
val rtn = field.get(obj)
|
||||
field.isAccessible = false
|
||||
return rtn
|
||||
}
|
||||
|
||||
override fun isNullable() = try {
|
||||
field.kotlinProperty?.returnType?.isMarkedNullable ?: false
|
||||
} catch (e: kotlin.reflect.jvm.internal.KotlinReflectionInternalError) {
|
||||
// This might happen for some types, e.g. kotlin.Throwable? - the root cause of the issue
|
||||
// is: https://youtrack.jetbrains.com/issue/KT-13077
|
||||
// TODO: Revisit this when Kotlin issue is fixed.
|
||||
|
||||
// So this used to report as an error, but given we serialise exceptions all the time it
|
||||
// provides for very scary log files so move this to trace level
|
||||
loggerFor<PropertySerializer>().let { logger ->
|
||||
logger.trace("Using kotlin introspection on internal type $field")
|
||||
logger.trace("Unexpected internal Kotlin error", e)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special instance of a [PropertyReader] for use only by [EvolutionSerializer]s to make
|
||||
* it explicit that no properties are ever actually read from an object as the evolution
|
||||
* serializer should only be accessing the already serialized form.
|
||||
*/
|
||||
class EvolutionPropertyReader : PropertyReader() {
|
||||
override fun read(obj: Any?): Any? {
|
||||
throw UnsupportedOperationException("It should be impossible for an evolution serializer to "
|
||||
+ "be reading from an object")
|
||||
}
|
||||
|
||||
override fun isNullable() = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a generic interface to a serializable property of an object.
|
||||
*
|
||||
* @property initialPosition where in the constructor used for serialization the property occurs.
|
||||
* @property getter a [PropertySerializer] wrapping access to the property. This will either be a
|
||||
* method invocation on the getter or, if not publicly accessible, reflection based by temporally
|
||||
* making the property accessible.
|
||||
*/
|
||||
abstract class PropertyAccessor(
|
||||
val initialPosition: Int,
|
||||
open val getter: PropertySerializer) {
|
||||
companion object : Comparator<PropertyAccessor> {
|
||||
override fun compare(p0: PropertyAccessor?, p1: PropertyAccessor?): Int {
|
||||
return p0?.getter?.name?.compareTo(p1?.getter?.name ?: "") ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to control how the property is set on the object.
|
||||
*/
|
||||
abstract fun set(instance: Any, obj: Any?)
|
||||
|
||||
override fun toString(): String {
|
||||
return "${getter.name}($initialPosition)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of [PropertyAccessor] representing a property of an object that
|
||||
* is serialized and deserialized via JavaBean getter and setter style methods.
|
||||
*/
|
||||
class PropertyAccessorGetterSetter(
|
||||
initialPosition: Int,
|
||||
getter: PropertySerializer,
|
||||
private val setter: Method) : PropertyAccessor(initialPosition, getter) {
|
||||
init {
|
||||
/**
|
||||
* Play nicely with Java interop, public methods aren't marked as accessible
|
||||
*/
|
||||
setter.isAccessible = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the setter on the underlying object passing in the serialized value.
|
||||
*/
|
||||
override fun set(instance: Any, obj: Any?) {
|
||||
setter.invoke(instance, *listOf(obj).toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of [PropertyAccessor] representing a property of an object that
|
||||
* is serialized via a JavaBean getter but deserialized using the constructor
|
||||
* of the object the property belongs to.
|
||||
*/
|
||||
class PropertyAccessorConstructor(
|
||||
initialPosition: Int,
|
||||
override val getter: PropertySerializer) : PropertyAccessor(initialPosition, getter) {
|
||||
/**
|
||||
* Because the property should be being set on the obejct through the constructor any
|
||||
* calls to the explicit setter should be an error.
|
||||
*/
|
||||
override fun set(instance: Any, obj: Any?) {
|
||||
NotSerializableException("Attempting to access a setter on an object being instantiated " +
|
||||
"via its constructor.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of [PropertyAccessor]s that represent the serialized form
|
||||
* of an object.
|
||||
*
|
||||
* @property serializationOrder a list of [PropertyAccessor]. For deterministic serialization
|
||||
* should be sorted.
|
||||
* @property size how many properties are being serialized.
|
||||
* @property byConstructor are the properties of the class represented by this set of properties populated
|
||||
* on deserialization via the object's constructor or the corresponding setter functions. Should be
|
||||
* overridden and set appropriately by child types.
|
||||
*/
|
||||
abstract class PropertySerializers(
|
||||
val serializationOrder: List<PropertyAccessor>) {
|
||||
companion object {
|
||||
fun make(serializationOrder: List<PropertyAccessor>) =
|
||||
when (serializationOrder.firstOrNull()) {
|
||||
is PropertyAccessorConstructor -> PropertySerializersConstructor(serializationOrder)
|
||||
is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder)
|
||||
null -> PropertySerializersNoProperties()
|
||||
else -> {
|
||||
throw NotSerializableException("Unknown Property Accessor type, cannot create set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val size get() = serializationOrder.size
|
||||
abstract val byConstructor: Boolean
|
||||
}
|
||||
|
||||
class PropertySerializersNoProperties : PropertySerializers(emptyList()) {
|
||||
override val byConstructor get() = true
|
||||
}
|
||||
|
||||
class PropertySerializersConstructor(
|
||||
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
|
||||
override val byConstructor get() = true
|
||||
}
|
||||
|
||||
class PropertySerializersSetter(
|
||||
serializationOrder: List<PropertyAccessor>) : PropertySerializers(serializationOrder) {
|
||||
override val byConstructor get() = false
|
||||
}
|
||||
|
||||
class PropertySerializersEvolution : PropertySerializers(emptyList()) {
|
||||
override val byConstructor get() = false
|
||||
}
|
||||
|
||||
|
@ -1,306 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
|
||||
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.DescribedTypeConstructor
|
||||
import java.io.NotSerializableException
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
|
||||
|
||||
const val DESCRIPTOR_DOMAIN: String = "net.corda"
|
||||
val amqpMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(1, 0))
|
||||
|
||||
/**
|
||||
* This and the classes below are OO representations of the AMQP XML schema described in the specification. Their
|
||||
* [toString] representations generate the associated XML form.
|
||||
*/
|
||||
data class Schema(val types: List<TypeNotation>) : DescribedType {
|
||||
companion object : DescribedTypeConstructor<Schema> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.SCHEMA.amqpDescriptor
|
||||
|
||||
fun get(obj: Any): Schema {
|
||||
val describedType = obj as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
val list = describedType.described as List<*>
|
||||
return newInstance(listOf((list[0] as List<*>).map { TypeNotation.get(it!!) }))
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = Schema::class.java
|
||||
|
||||
override fun newInstance(described: Any?): Schema {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
return Schema(uncheckedCast(list[0]))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(types)
|
||||
|
||||
override fun toString(): String = types.joinToString("\n")
|
||||
}
|
||||
|
||||
data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : DescribedType {
|
||||
constructor(name: String?) : this(Symbol.valueOf(name))
|
||||
|
||||
companion object : DescribedTypeConstructor<Descriptor> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.OBJECT_DESCRIPTOR.amqpDescriptor
|
||||
|
||||
fun get(obj: Any): Descriptor {
|
||||
val describedType = obj as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
return newInstance(describedType.described)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = Descriptor::class.java
|
||||
|
||||
override fun newInstance(described: Any?): Descriptor {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
return Descriptor(list[0] as? Symbol, list[1] as? UnsignedLong)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, code)
|
||||
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder("<descriptor")
|
||||
if (name != null) {
|
||||
sb.append(" name=\"$name\"")
|
||||
}
|
||||
if (code != null) {
|
||||
val code = String.format("0x%08x:0x%08x", code.toLong().shr(32), code.toLong().and(0xffff))
|
||||
sb.append(" code=\"$code\"")
|
||||
}
|
||||
sb.append("/>")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
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 = AMQPDescriptorRegistry.FIELD.amqpDescriptor
|
||||
|
||||
fun get(obj: Any): Field {
|
||||
val describedType = obj as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
return newInstance(describedType.described)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = Field::class.java
|
||||
|
||||
override fun newInstance(described: Any?): Field {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
return Field(list[0] as String, list[1] as String, uncheckedCast(list[2]), list[3] as? String, list[4] as? String, list[5] as Boolean, list[6] as Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, type, requires, default, label, mandatory, multiple)
|
||||
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder("<field name=\"$name\" type=\"$type\" mandatory=\"$mandatory\" multiple=\"$multiple\"")
|
||||
if (requires.isNotEmpty()) {
|
||||
sb.append(" requires=\"")
|
||||
sb.append(requires.joinToString(","))
|
||||
sb.append("\"")
|
||||
}
|
||||
if (default != null) {
|
||||
sb.append(" default=\"$default\"")
|
||||
}
|
||||
if (!label.isNullOrBlank()) {
|
||||
sb.append(" label=\"$label\"")
|
||||
}
|
||||
sb.append("/>")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TypeNotation : DescribedType {
|
||||
companion object {
|
||||
fun get(obj: Any): TypeNotation {
|
||||
val describedType = obj as DescribedType
|
||||
return when (describedType.descriptor) {
|
||||
CompositeType.DESCRIPTOR -> CompositeType.get(describedType)
|
||||
RestrictedType.DESCRIPTOR -> RestrictedType.get(describedType)
|
||||
else -> throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract val name: String
|
||||
abstract val label: String?
|
||||
abstract val provides: List<String>
|
||||
abstract val descriptor: Descriptor
|
||||
}
|
||||
|
||||
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 = AMQPDescriptorRegistry.COMPOSITE_TYPE.amqpDescriptor
|
||||
|
||||
fun get(describedType: DescribedType): CompositeType {
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
val list = describedType.described as List<*>
|
||||
return newInstance(listOf(list[0], list[1], list[2], Descriptor.get(list[3]!!), (list[4] as List<*>).map { Field.get(it!!) }))
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = CompositeType::class.java
|
||||
|
||||
override fun newInstance(described: Any?): CompositeType {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
return CompositeType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as Descriptor, uncheckedCast(list[4]))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, label, provides, descriptor, fields)
|
||||
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder("<type class=\"composite\" name=\"$name\"")
|
||||
if (!label.isNullOrBlank()) {
|
||||
sb.append(" label=\"$label\"")
|
||||
}
|
||||
if (provides.isNotEmpty()) {
|
||||
sb.append(" provides=\"")
|
||||
sb.append(provides.joinToString(","))
|
||||
sb.append("\"")
|
||||
}
|
||||
sb.append(">\n")
|
||||
sb.append(" $descriptor\n")
|
||||
for (field in fields) {
|
||||
sb.append(" $field\n")
|
||||
}
|
||||
sb.append("</type>")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
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 = AMQPDescriptorRegistry.RESTRICTED_TYPE.amqpDescriptor
|
||||
|
||||
fun get(describedType: DescribedType): RestrictedType {
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
val list = describedType.described as List<*>
|
||||
return newInstance(listOf(list[0], list[1], list[2], list[3], Descriptor.get(list[4]!!), (list[5] as List<*>).map { Choice.get(it!!) }))
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = RestrictedType::class.java
|
||||
|
||||
override fun newInstance(described: Any?): RestrictedType {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
return RestrictedType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as String, list[4] as Descriptor, uncheckedCast(list[5]))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, label, provides, source, descriptor, choices)
|
||||
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder("<type class=\"restricted\" name=\"$name\"")
|
||||
if (!label.isNullOrBlank()) {
|
||||
sb.append(" label=\"$label\"")
|
||||
}
|
||||
sb.append(" source=\"$source\"")
|
||||
if (provides.isNotEmpty()) {
|
||||
sb.append(" provides=\"")
|
||||
sb.append(provides.joinToString(","))
|
||||
sb.append("\"")
|
||||
}
|
||||
sb.append(">\n")
|
||||
sb.append(" $descriptor\n")
|
||||
choices.forEach {
|
||||
sb.append(" $it\n")
|
||||
}
|
||||
sb.append("</type>")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
data class Choice(val name: String, val value: String) : DescribedType {
|
||||
companion object : DescribedTypeConstructor<Choice> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.CHOICE.amqpDescriptor
|
||||
|
||||
fun get(obj: Any): Choice {
|
||||
val describedType = obj as DescribedType
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
return newInstance(describedType.described)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = Choice::class.java
|
||||
|
||||
override fun newInstance(described: Any?): Choice {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
return Choice(list[0] as String, list[1] as String)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, value)
|
||||
|
||||
override fun toString(): String {
|
||||
return "<choice name=\"$name\" value=\"$value\"/>"
|
||||
}
|
||||
}
|
||||
|
||||
data class ReferencedObject(private val refCounter: Int) : DescribedType {
|
||||
companion object : DescribedTypeConstructor<ReferencedObject> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.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/>"
|
||||
}
|
||||
|
||||
|
@ -1,580 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.ConstructorForDeserialization
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.*
|
||||
import java.lang.reflect.Field
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KParameter
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
/**
|
||||
* Code for finding the constructor we will use for deserialization.
|
||||
*
|
||||
* If there's only one constructor, it selects that. If there are two and one is the default, it selects the other.
|
||||
* Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is
|
||||
* annotated with [@CordaConstructor]. It will report an error if more than one constructor is annotated.
|
||||
*/
|
||||
internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||
val clazz: Class<*> = type.asClass()!!
|
||||
if (isConcrete(clazz)) {
|
||||
var preferredCandidate: KFunction<Any>? = clazz.kotlin.primaryConstructor
|
||||
var annotatedCount = 0
|
||||
val kotlinConstructors = clazz.kotlin.constructors
|
||||
val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() }
|
||||
|
||||
for (kotlinConstructor in kotlinConstructors) {
|
||||
if (preferredCandidate == null && kotlinConstructors.size == 1) {
|
||||
preferredCandidate = kotlinConstructor
|
||||
} else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) {
|
||||
preferredCandidate = kotlinConstructor
|
||||
} else if (kotlinConstructor.findAnnotation<ConstructorForDeserialization>() != null) {
|
||||
if (annotatedCount++ > 0) {
|
||||
throw NotSerializableException("More than one constructor for $clazz is annotated with @CordaConstructor.")
|
||||
}
|
||||
preferredCandidate = kotlinConstructor
|
||||
}
|
||||
}
|
||||
|
||||
return preferredCandidate?.apply { isAccessible = true }
|
||||
?: throw NotSerializableException("No constructor for deserialization found for $clazz.")
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the properties to be used during serialization by attempting to find those that match the parameters
|
||||
* to the deserialization constructor, if the class is concrete. If it is abstract, or an interface, then use all
|
||||
* the properties.
|
||||
*
|
||||
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters
|
||||
* have names accessible via reflection.
|
||||
*/
|
||||
internal fun <T : Any> propertiesForSerialization(
|
||||
kotlinConstructor: KFunction<T>?,
|
||||
type: Type,
|
||||
factory: SerializerFactory) = PropertySerializers.make(
|
||||
if (kotlinConstructor != null) {
|
||||
propertiesForSerializationFromConstructor(kotlinConstructor, type, factory)
|
||||
} else {
|
||||
propertiesForSerializationFromAbstract(type.asClass()!!, type, factory)
|
||||
}.sortedWith(PropertyAccessor))
|
||||
|
||||
|
||||
fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
|
||||
|
||||
/**
|
||||
* Encapsulates the property of a class and its potential getter and setter methods.
|
||||
*
|
||||
* @property field a property of a class.
|
||||
* @property setter the method of a class that sets the field. Determined by locating
|
||||
* a function called setXyz on the class for the property named in field as xyz.
|
||||
* @property getter the method of a class that returns a fields value. Determined by
|
||||
* locating a function named getXyz for the property named in field as xyz.
|
||||
*/
|
||||
data class PropertyDescriptor(var field: Field?, var setter: Method?, var getter: Method?, var iser: Method?) {
|
||||
override fun toString() = StringBuilder("").apply {
|
||||
appendln("Property - ${field?.name ?: "null field"}\n")
|
||||
appendln(" getter - ${getter?.name ?: "no getter"}")
|
||||
appendln(" setter - ${setter?.name ?: "no setter"}")
|
||||
appendln(" iser - ${iser?.name ?: "no isXYZ defined"}")
|
||||
}.toString()
|
||||
|
||||
constructor() : this(null, null, null, null)
|
||||
|
||||
fun preferredGetter(): Method? = getter ?: iser
|
||||
}
|
||||
|
||||
object PropertyDescriptorsRegex {
|
||||
// match an uppercase letter that also has a corresponding lower case equivalent
|
||||
val re = Regex("(?<type>get|set|is)(?<var>\\p{Lu}.*)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Collate the properties of a class and match them with their getter and setter
|
||||
* methods as per a JavaBean.
|
||||
*
|
||||
* for a property
|
||||
* exampleProperty
|
||||
*
|
||||
* We look for methods
|
||||
* setExampleProperty
|
||||
* getExampleProperty
|
||||
* isExampleProperty
|
||||
*
|
||||
* Where setExampleProperty must return a type compatible with exampleProperty, getExampleProperty must
|
||||
* take a single parameter of a type compatible with exampleProperty and isExampleProperty must
|
||||
* return a boolean
|
||||
*/
|
||||
fun Class<out Any?>.propertyDescriptors(): Map<String, PropertyDescriptor> {
|
||||
val classProperties = mutableMapOf<String, PropertyDescriptor>()
|
||||
|
||||
var clazz: Class<out Any?>? = this
|
||||
|
||||
do {
|
||||
clazz!!.declaredFields.forEach { property ->
|
||||
classProperties.computeIfAbsent(property.name) {
|
||||
PropertyDescriptor()
|
||||
}.apply {
|
||||
this.field = property
|
||||
}
|
||||
}
|
||||
clazz = clazz.superclass
|
||||
} while (clazz != null)
|
||||
|
||||
//
|
||||
// Running as two loops rather than one as we need to ensure we have captured all of the properties
|
||||
// before looking for interacting methods and need to cope with the class hierarchy introducing
|
||||
// new properties / methods
|
||||
//
|
||||
clazz = this
|
||||
do {
|
||||
// Note: It is possible for a class to have multiple instances of a function where the types
|
||||
// differ. For example:
|
||||
// interface I<out T> { val a: T }
|
||||
// class D(override val a: String) : I<String>
|
||||
// instances of D will have both
|
||||
// getA - returning a String (java.lang.String) and
|
||||
// getA - returning an Object (java.lang.Object)
|
||||
// In this instance we take the most derived object
|
||||
//
|
||||
// In addition, only getters that take zero parameters and setters that take a single
|
||||
// parameter will be considered
|
||||
clazz!!.declaredMethods?.map { func ->
|
||||
if (!Modifier.isPublic(func.modifiers)) return@map
|
||||
if (func.name == "getClass") return@map
|
||||
|
||||
PropertyDescriptorsRegex.re.find(func.name)?.apply {
|
||||
// matching means we have an func getX where the property could be x or X
|
||||
// so having pre-loaded all of the properties we try to match to either case. If that
|
||||
// fails the getter doesn't refer to a property directly, but may refer to a constructor
|
||||
// parameter that shadows a property
|
||||
val properties =
|
||||
classProperties[groups[2]!!.value] ?: classProperties[groups[2]!!.value.decapitalize()] ?:
|
||||
// take into account those constructor properties that don't directly map to a named
|
||||
// property which are, by default, already added to the map
|
||||
classProperties.computeIfAbsent(groups[2]!!.value) { PropertyDescriptor() }
|
||||
|
||||
properties.apply {
|
||||
when (groups[1]!!.value) {
|
||||
"set" -> {
|
||||
if (func.parameterCount == 1) {
|
||||
if (setter == null) setter = func
|
||||
else if (TypeToken.of(setter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) {
|
||||
setter = func
|
||||
}
|
||||
}
|
||||
}
|
||||
"get" -> {
|
||||
if (func.parameterCount == 0) {
|
||||
if (getter == null) getter = func
|
||||
else if (TypeToken.of(getter!!.genericReturnType).isSupertypeOf(func.genericReturnType)) {
|
||||
getter = func
|
||||
}
|
||||
}
|
||||
}
|
||||
"is" -> {
|
||||
if (func.parameterCount == 0) {
|
||||
val rtnType = TypeToken.of(func.genericReturnType)
|
||||
if ((rtnType == TypeToken.of(Boolean::class.java))
|
||||
|| (rtnType == TypeToken.of(Boolean::class.javaObjectType))) {
|
||||
if (iser == null) iser = func
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clazz = clazz.superclass
|
||||
} while (clazz != null)
|
||||
|
||||
return classProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* From a constructor, determine which properties of a class are to be serialized.
|
||||
*
|
||||
* @param kotlinConstructor The constructor to be used to instantiate instances of the class
|
||||
* @param type The class's [Type]
|
||||
* @param factory The factory generating the serializer wrapping this function.
|
||||
*/
|
||||
internal fun <T : Any> propertiesForSerializationFromConstructor(
|
||||
kotlinConstructor: KFunction<T>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
|
||||
|
||||
val classProperties = clazz.propertyDescriptors()
|
||||
|
||||
// Annoyingly there isn't a better way to ascertain that the constructor for the class
|
||||
// has a synthetic parameter inserted to capture the reference to the outer class. You'd
|
||||
// think you could inspect the parameter and check the isSynthetic flag but that is always
|
||||
// false so given the naming convention is specified by the standard we can just check for
|
||||
// this
|
||||
if (kotlinConstructor.javaConstructor?.parameterCount ?: 0 > 0 &&
|
||||
kotlinConstructor.javaConstructor?.parameters?.get(0)?.name == "this$0"
|
||||
) {
|
||||
throw SyntheticParameterException(type)
|
||||
}
|
||||
|
||||
if (classProperties.isNotEmpty() && kotlinConstructor.parameters.isEmpty()) {
|
||||
return propertiesForSerializationFromSetters(classProperties, type, factory)
|
||||
}
|
||||
|
||||
return mutableListOf<PropertyAccessor>().apply {
|
||||
kotlinConstructor.parameters.withIndex().forEach { param ->
|
||||
// name cannot be null, if it is then this is a synthetic field and we will have bailed
|
||||
// out prior to this
|
||||
val name = param.value.name!!
|
||||
|
||||
// We will already have disambiguated getA for property A or a but we still need to cope
|
||||
// with the case we don't know the case of A when the parameter doesn't match a property
|
||||
// but has a getter
|
||||
val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()]
|
||||
?: throw NotSerializableException(
|
||||
"Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"")
|
||||
|
||||
// If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't
|
||||
// *for *know* we switch to a reflection based method
|
||||
val propertyReader = if (matchingProperty.getter != null) {
|
||||
val getter = matchingProperty.getter ?: throw NotSerializableException(
|
||||
"Property has no getter method for - \"$name\" - of \"$clazz\". If using Java and the parameter name"
|
||||
+ "looks anonymous, check that you have the -parameters option specified in the "
|
||||
+ "Java compiler. Alternately, provide a proxy serializer "
|
||||
+ "(SerializationCustomSerializer) if recompiling isn't an option.")
|
||||
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) {
|
||||
throw NotSerializableException(
|
||||
"Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " +
|
||||
"parameter type \"${param.value.type.javaType}\"")
|
||||
}
|
||||
|
||||
Pair(PublicPropertyReader(getter), returnType)
|
||||
} else {
|
||||
val field = classProperties[name]!!.field
|
||||
?: throw NotSerializableException("No property matching constructor parameter named - \"$name\" - " +
|
||||
"of \"$clazz\". If using Java, check that you have the -parameters option specified " +
|
||||
"in the Java compiler. Alternately, provide a proxy serializer " +
|
||||
"(SerializationCustomSerializer) if recompiling isn't an option")
|
||||
|
||||
Pair(PrivatePropertyReader(field, type), field.genericType)
|
||||
}
|
||||
|
||||
this += PropertyAccessorConstructor(
|
||||
param.index,
|
||||
PropertySerializer.make(name, propertyReader.first, propertyReader.second, factory))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we determine a class has a constructor that takes no parameters then check for pairs of getters / setters
|
||||
* and use those
|
||||
*/
|
||||
fun propertiesForSerializationFromSetters(
|
||||
properties: Map<String, PropertyDescriptor>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
return mutableListOf<PropertyAccessorGetterSetter>().apply {
|
||||
var idx = 0
|
||||
|
||||
properties.forEach { property ->
|
||||
val getter: Method? = property.value.preferredGetter()
|
||||
val setter: Method? = property.value.setter
|
||||
|
||||
if (getter == null || setter == null) return@forEach
|
||||
|
||||
if (setter.parameterCount != 1) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes too many arguments")
|
||||
}
|
||||
|
||||
val setterType = setter.genericParameterTypes[0]!!
|
||||
|
||||
if ((property.value.field != null) &&
|
||||
(!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType)))) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes parameter of type $setterType yet underlying type is " +
|
||||
"${property.value.field?.genericType!!}")
|
||||
}
|
||||
|
||||
// Make sure the getter returns the same type (within inheritance bounds) the setter accepts.
|
||||
if (!(TypeToken.of(getter.genericReturnType).isSupertypeOf(setterType))) {
|
||||
throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " +
|
||||
"takes parameter of type $setterType yet the defined getter returns a value of type " +
|
||||
"${getter.returnType} [${getter.genericReturnType}]")
|
||||
}
|
||||
this += PropertyAccessorGetterSetter(
|
||||
idx++,
|
||||
PropertySerializer.make(property.key, PublicPropertyReader(getter),
|
||||
resolveTypeVariables(getter.genericReturnType, type), factory),
|
||||
setter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructorParamTakesReturnTypeOfGetter(
|
||||
getterReturnType: Type,
|
||||
rawGetterReturnType: Type,
|
||||
param: KParameter): Boolean {
|
||||
val paramToken = TypeToken.of(param.type.javaType)
|
||||
val rawParamType = TypeToken.of(paramToken.rawType)
|
||||
|
||||
return paramToken.isSupertypeOf(getterReturnType)
|
||||
|| paramToken.isSupertypeOf(rawGetterReturnType)
|
||||
// cope with the case where the constructor parameter is a generic type (T etc) but we
|
||||
// can discover it's raw type. When bounded this wil be the bounding type, unbounded
|
||||
// generics this will be object
|
||||
|| rawParamType.isSupertypeOf(getterReturnType)
|
||||
|| rawParamType.isSupertypeOf(rawGetterReturnType)
|
||||
}
|
||||
|
||||
private fun propertiesForSerializationFromAbstract(
|
||||
clazz: Class<*>,
|
||||
type: Type,
|
||||
factory: SerializerFactory): List<PropertyAccessor> {
|
||||
val properties = clazz.propertyDescriptors()
|
||||
|
||||
return mutableListOf<PropertyAccessorConstructor>().apply {
|
||||
properties.toList().withIndex().forEach {
|
||||
val getter = it.value.second.getter ?: return@forEach
|
||||
if (it.value.second.field == null) return@forEach
|
||||
|
||||
val returnType = resolveTypeVariables(getter.genericReturnType, type)
|
||||
this += PropertyAccessorConstructor(
|
||||
it.index,
|
||||
PropertySerializer.make(it.value.first, PublicPropertyReader(getter), returnType, factory))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List<Type> {
|
||||
val interfaces = LinkedHashSet<Type>()
|
||||
exploreType(type, interfaces, serializerFactory)
|
||||
return interfaces.toList()
|
||||
}
|
||||
|
||||
private fun exploreType(type: Type?, interfaces: MutableSet<Type>, serializerFactory: SerializerFactory) {
|
||||
val clazz = type?.asClass()
|
||||
if (clazz != null) {
|
||||
if (clazz.isInterface) {
|
||||
if (serializerFactory.whitelist.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting.
|
||||
else interfaces += type
|
||||
}
|
||||
for (newInterface in clazz.genericInterfaces) {
|
||||
if (newInterface !in interfaces) {
|
||||
exploreType(resolveTypeVariables(newInterface, type), interfaces, serializerFactory)
|
||||
}
|
||||
}
|
||||
val superClass = clazz.genericSuperclass ?: return
|
||||
exploreType(resolveTypeVariables(superClass, type), interfaces, serializerFactory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension helper for writing described objects.
|
||||
*/
|
||||
fun Data.withDescribed(descriptor: Descriptor, block: Data.() -> Unit) {
|
||||
// Write described
|
||||
putDescribed()
|
||||
enter()
|
||||
// Write descriptor
|
||||
putObject(descriptor.code ?: descriptor.name)
|
||||
block()
|
||||
exit() // exit described
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension helper for writing lists.
|
||||
*/
|
||||
fun Data.withList(block: Data.() -> Unit) {
|
||||
// Write list
|
||||
putList()
|
||||
enter()
|
||||
block()
|
||||
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
|
||||
return if (resolvedType is TypeVariable<*>) {
|
||||
val bounds = resolvedType.bounds
|
||||
return if (bounds.isEmpty()) {
|
||||
SerializerFactory.AnyType
|
||||
} else if (bounds.size == 1) {
|
||||
resolveTypeVariables(bounds[0], contextType)
|
||||
} else throw NotSerializableException("Got bounded type $actualType but only support single bound.")
|
||||
} else {
|
||||
resolvedType
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Type.asClass(): Class<*>? {
|
||||
return when {
|
||||
this is Class<*> -> this
|
||||
this is ParameterizedType -> this.rawType.asClass()
|
||||
this is GenericArrayType -> this.genericComponentType.asClass()?.arrayClass()
|
||||
this is TypeVariable<*> -> this.bounds.first().asClass()
|
||||
this is WildcardType -> this.upperBounds.first().asClass()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Type.asArray(): Type? {
|
||||
return when {
|
||||
this is Class<*> -> this.arrayClass()
|
||||
this is ParameterizedType -> DeserializedGenericArrayType(this)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Class<*>.arrayClass(): Class<*> = java.lang.reflect.Array.newInstance(this, 0).javaClass
|
||||
|
||||
internal fun Type.isArray(): Boolean = (this is Class<*> && this.isArray) || (this is GenericArrayType)
|
||||
|
||||
internal fun Type.componentType(): Type {
|
||||
check(this.isArray()) { "$this is not an array type." }
|
||||
return (this as? Class<*>)?.componentType ?: (this as GenericArrayType).genericComponentType
|
||||
}
|
||||
|
||||
internal fun Class<*>.asParameterizedType(): ParameterizedType {
|
||||
return DeserializedParameterizedType(this, this.typeParameters)
|
||||
}
|
||||
|
||||
internal fun Type.asParameterizedType(): ParameterizedType {
|
||||
return when (this) {
|
||||
is Class<*> -> this.asParameterizedType()
|
||||
is ParameterizedType -> this
|
||||
else -> throw NotSerializableException("Don't know how to convert to ParameterizedType")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Type.isSubClassOf(type: Type): Boolean {
|
||||
return TypeToken.of(this).isSubtypeOf(TypeToken.of(type).rawType)
|
||||
}
|
||||
|
||||
// ByteArrays, primitives 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common properties that are to be used in the [SerializationContext.properties] to alter serialization behavior/content
|
||||
*/
|
||||
internal enum class CommonPropertyNames {
|
||||
IncludeInternalInfo,
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function which helps tracking the path in the object graph when exceptions are thrown.
|
||||
* Since there might be a chain of nested calls it is useful to record which part of the graph caused an issue.
|
||||
* Path information is added to the message of the exception being thrown.
|
||||
*/
|
||||
internal inline fun <T> ifThrowsAppend(strToAppendFn: () -> String, block: () -> T): T {
|
||||
try {
|
||||
return block()
|
||||
} catch (th: Throwable) {
|
||||
th.setMessage("${strToAppendFn()} -> ${th.message}")
|
||||
throw th
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not a public property so will have to use reflection
|
||||
*/
|
||||
private fun Throwable.setMessage(newMsg: String) {
|
||||
val detailMessageField = Throwable::class.java.getDeclaredField("detailMessage")
|
||||
detailMessageField.isAccessible = true
|
||||
detailMessageField.set(this, newMsg)
|
||||
}
|
||||
|
||||
fun ClassWhitelist.requireWhitelisted(type: Type) {
|
||||
if (!this.isWhitelisted(type.asClass()!!)) {
|
||||
throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.")
|
||||
}
|
||||
}
|
||||
|
||||
fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = (hasListed(clazz) || hasAnnotationInHierarchy(clazz))
|
||||
fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = !(this.isWhitelisted(clazz))
|
||||
|
||||
// Recursively check the class, interfaces and superclasses for our annotation.
|
||||
fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean {
|
||||
return type.isAnnotationPresent(CordaSerializable::class.java)
|
||||
|| type.interfaces.any { hasAnnotationInHierarchy(it) }
|
||||
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
|
||||
}
|
||||
|
||||
/**
|
||||
* By default use Kotlin reflection and grab the objectInstance. However, that doesn't play nicely with nested
|
||||
* private objects. Even setting the accessibility override (setAccessible) still causes an
|
||||
* [IllegalAccessException] when attempting to retrieve the value of the INSTANCE field.
|
||||
*
|
||||
* Whichever reference to the class Kotlin reflection uses, override (set from setAccessible) on that field
|
||||
* isn't set even when it was explicitly set as accessible before calling into the kotlin reflection routines.
|
||||
*
|
||||
* For example
|
||||
*
|
||||
* clazz.getDeclaredField("INSTANCE")?.apply {
|
||||
* isAccessible = true
|
||||
* kotlin.objectInstance // This throws as the INSTANCE field isn't accessible
|
||||
* }
|
||||
*
|
||||
* Therefore default back to good old java reflection and simply look for the INSTANCE field as we are never going
|
||||
* to serialize a companion object.
|
||||
*
|
||||
* As such, if objectInstance fails access, revert to Java reflection and try that
|
||||
*/
|
||||
fun Class<*>.objectInstance() =
|
||||
try {
|
||||
this.kotlin.objectInstance
|
||||
} catch (e: IllegalAccessException) {
|
||||
// Check it really is an object (i.e. it has no constructor)
|
||||
if (constructors.isNotEmpty()) null
|
||||
else {
|
||||
try {
|
||||
this.getDeclaredField("INSTANCE")?.let { field ->
|
||||
// and must be marked as both static and final (>0 means they're set)
|
||||
if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null
|
||||
else {
|
||||
val accessibility = field.isAccessible
|
||||
field.isAccessible = true
|
||||
val obj = field.get(null)
|
||||
field.isAccessible = accessibility
|
||||
obj
|
||||
}
|
||||
}
|
||||
} catch (e: NoSuchFieldException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationEncoding
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.nodeapi.internal.serialization.CordaSerializationEncoding
|
||||
import net.corda.nodeapi.internal.serialization.SectionId
|
||||
import net.corda.nodeapi.internal.serialization.byteArrayOutput
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
import kotlin.collections.LinkedHashSet
|
||||
|
||||
data class BytesAndSchemas<T : Any>(
|
||||
val obj: SerializedBytes<T>,
|
||||
val schema: Schema,
|
||||
val transformsSchema: TransformsSchema)
|
||||
|
||||
/**
|
||||
* Main entry point for serializing an object to AMQP.
|
||||
*
|
||||
* @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple
|
||||
* instances and threads.
|
||||
*/
|
||||
open class SerializationOutput @JvmOverloads constructor(
|
||||
internal val serializerFactory: SerializerFactory,
|
||||
private val encoding: SerializationEncoding? = null
|
||||
) {
|
||||
private val objectHistory: MutableMap<Any, Int> = IdentityHashMap()
|
||||
private val serializerHistory: MutableSet<AMQPSerializer<*>> = 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 constructed the serialized form.
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
try {
|
||||
return _serialize(obj, context)
|
||||
} finally {
|
||||
andFinally()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> serializeAndReturnSchema(obj: T, context: SerializationContext): BytesAndSchemas<T> {
|
||||
try {
|
||||
val blob = _serialize(obj, context)
|
||||
val schema = Schema(schemaHistory.toList())
|
||||
return BytesAndSchemas(blob, schema, TransformsSchema.build(schema, serializerFactory))
|
||||
} finally {
|
||||
andFinally()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun andFinally() {
|
||||
objectHistory.clear()
|
||||
serializerHistory.clear()
|
||||
schemaHistory.clear()
|
||||
}
|
||||
|
||||
internal fun <T : Any> _serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
val data = Data.Factory.create()
|
||||
data.withDescribed(Envelope.DESCRIPTOR_OBJECT) {
|
||||
withList {
|
||||
writeObject(obj, this, context)
|
||||
val schema = Schema(schemaHistory.toList())
|
||||
writeSchema(schema, this)
|
||||
writeTransformSchema(TransformsSchema.build(schema, serializerFactory), this)
|
||||
}
|
||||
}
|
||||
return SerializedBytes(byteArrayOutput {
|
||||
var stream: OutputStream = it
|
||||
try {
|
||||
amqpMagic.writeTo(stream)
|
||||
if (encoding != null) {
|
||||
SectionId.ENCODING.writeTo(stream)
|
||||
(encoding as CordaSerializationEncoding).writeTo(stream)
|
||||
stream = encoding.wrap(stream)
|
||||
}
|
||||
SectionId.DATA_AND_STOP.writeTo(stream)
|
||||
stream.alsoAsByteBuffer(data.encodedSize().toInt(), data::encode)
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
internal fun writeObject(obj: Any, data: Data, context: SerializationContext) {
|
||||
writeObject(obj, data, obj.javaClass, context)
|
||||
}
|
||||
|
||||
open fun writeSchema(schema: Schema, data: Data) {
|
||||
data.putObject(schema)
|
||||
}
|
||||
|
||||
open fun writeTransformSchema(transformsSchema: TransformsSchema, data: Data) {
|
||||
data.putObject(transformsSchema)
|
||||
}
|
||||
|
||||
internal fun writeObjectOrNull(obj: Any?, data: Data, type: Type, context: SerializationContext, debugIndent: Int) {
|
||||
if (obj == null) {
|
||||
data.putNull()
|
||||
} else {
|
||||
writeObject(obj, data, if (type == SerializerFactory.AnyType) obj.javaClass else type, context, debugIndent)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun writeObject(obj: Any, data: Data, type: Type, context: SerializationContext, debugIndent: Int = 0) {
|
||||
val serializer = serializerFactory.get(obj.javaClass, type)
|
||||
if (serializer !in serializerHistory) {
|
||||
serializerHistory.add(serializer)
|
||||
serializer.writeClassInfo(this)
|
||||
}
|
||||
|
||||
val retrievedRefCount = objectHistory[obj]
|
||||
if (retrievedRefCount == null) {
|
||||
serializer.writeObject(obj, data, type, this, context, debugIndent)
|
||||
// Important to do it after serialization such that dependent object will have preceding reference numbers
|
||||
// assigned to them first as they will be first read from the stream on receiving end.
|
||||
// Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content
|
||||
if (suitableForObjectReference(obj.javaClass)) {
|
||||
objectHistory[obj] = objectHistory.size
|
||||
}
|
||||
} else {
|
||||
data.writeReferencedObject(ReferencedObject(retrievedRefCount))
|
||||
}
|
||||
}
|
||||
|
||||
internal open fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean {
|
||||
return schemaHistory.addAll(typeNotation)
|
||||
}
|
||||
|
||||
internal open fun requireSerializer(type: Type) {
|
||||
if (type != SerializerFactory.AnyType && type != Object::class.java) {
|
||||
val serializer = serializerFactory.get(null, type)
|
||||
if (serializer !in serializerHistory) {
|
||||
serializerHistory.add(serializer)
|
||||
serializer.writeClassInfo(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,431 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeResolver
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.*
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
data class SerializationSchemas(val schema: Schema, val transforms: TransformsSchema)
|
||||
data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typeDescriptor: Any)
|
||||
|
||||
/**
|
||||
* Factory of serializers designed to be shared across threads and invocations.
|
||||
*
|
||||
* @property evolutionSerializerGetter controls how evolution serializers are generated by the factory. The normal
|
||||
* use case is an [EvolutionSerializer] type is returned. However, in some scenarios, primarily testing, this
|
||||
* can be altered to fit the requirements of the test.
|
||||
*/
|
||||
// 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: allow definition of well known types that are left out of the schema.
|
||||
// TODO: migrate some core types to unsigned integer descriptor
|
||||
// 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
|
||||
open class SerializerFactory(
|
||||
val whitelist: ClassWhitelist,
|
||||
val classCarpenter: ClassCarpenter,
|
||||
private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
|
||||
val fingerPrinter: FingerPrinter = SerializerFingerPrinter(),
|
||||
private val serializersByType: MutableMap<Type, AMQPSerializer<Any>>,
|
||||
val serializersByDescriptor: MutableMap<Any, AMQPSerializer<Any>>,
|
||||
private val customSerializers: MutableList<SerializerFor>,
|
||||
val transformsCache: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
|
||||
constructor(whitelist: ClassWhitelist,
|
||||
classCarpenter: ClassCarpenter,
|
||||
evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
|
||||
fingerPrinter: FingerPrinter = SerializerFingerPrinter()
|
||||
) : this(whitelist, classCarpenter, evolutionSerializerGetter, fingerPrinter,
|
||||
serializersByType = ConcurrentHashMap(),
|
||||
serializersByDescriptor = ConcurrentHashMap(),
|
||||
customSerializers = CopyOnWriteArrayList(),
|
||||
transformsCache = ConcurrentHashMap())
|
||||
constructor(whitelist: ClassWhitelist,
|
||||
classLoader: ClassLoader,
|
||||
evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
|
||||
fingerPrinter: FingerPrinter = SerializerFingerPrinter()
|
||||
) : this(whitelist, ClassCarpenterImpl(classLoader, whitelist), evolutionSerializerGetter, fingerPrinter,
|
||||
serializersByType = ConcurrentHashMap(),
|
||||
serializersByDescriptor = ConcurrentHashMap(),
|
||||
customSerializers = CopyOnWriteArrayList(),
|
||||
transformsCache = ConcurrentHashMap())
|
||||
|
||||
init {
|
||||
fingerPrinter.setOwner(this)
|
||||
}
|
||||
|
||||
val classloader: ClassLoader
|
||||
get() = classCarpenter.classloader
|
||||
|
||||
private fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: AMQPSerializer<Any>,
|
||||
schemas: SerializationSchemas) = evolutionSerializerGetter.getEvolutionSerializer(this, typeNotation, newSerializer, schemas)
|
||||
|
||||
/**
|
||||
* Look up, and manufacture if necessary, a serializer for the given type.
|
||||
*
|
||||
* @param actualClass Will be null if there isn't an actual object instance available (e.g. for
|
||||
* restricted type processing).
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
|
||||
val declaredClass = declaredType.asClass() ?: throw NotSerializableException(
|
||||
"Declared types of $declaredType are not supported.")
|
||||
|
||||
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
|
||||
|
||||
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))) &&
|
||||
!EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> {
|
||||
val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass)
|
||||
serializersByType.computeIfAbsent(declaredTypeAmended) {
|
||||
CollectionSerializer(declaredTypeAmended, this)
|
||||
}
|
||||
}
|
||||
// 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(declaredTypeAmended) {
|
||||
makeMapSerializer(declaredTypeAmended)
|
||||
}
|
||||
}
|
||||
Enum::class.java.isAssignableFrom(actualClass
|
||||
?: declaredClass) -> serializersByType.computeIfAbsent(actualClass ?: declaredClass) {
|
||||
whitelist.requireWhitelisted(actualType)
|
||||
EnumSerializer(actualType, actualClass ?: declaredClass, this)
|
||||
}
|
||||
else -> {
|
||||
makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
|
||||
}
|
||||
}
|
||||
|
||||
serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer)
|
||||
|
||||
return serializer
|
||||
}
|
||||
|
||||
/**
|
||||
* Try and infer concrete types for any generics type variables for the actual class encountered,
|
||||
* based on the declared type.
|
||||
*/
|
||||
// TODO: test GenericArrayType
|
||||
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()
|
||||
}
|
||||
is TypeVariable<*> -> actualClass
|
||||
is WildcardType -> actualClass
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
|
||||
* type, which must be a [ParameterizedType].
|
||||
*/
|
||||
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: ParameterizedType): Type? {
|
||||
if (actualClass == null || declaredClass == actualClass) {
|
||||
return null
|
||||
} else if (declaredClass.isAssignableFrom(actualClass)) {
|
||||
return if (actualClass.typeParameters.isNotEmpty()) {
|
||||
// The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them
|
||||
// Search for declared type in the inheritance hierarchy and then see if that fills in all the variables
|
||||
val implementationChain: List<Type>? = findPathToDeclared(actualClass, declaredType, mutableListOf())
|
||||
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 newResolved = resolved.resolveType(chainEntry)
|
||||
TypeResolver().where(chainEntry, newResolved)
|
||||
}
|
||||
// The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything.
|
||||
val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters)
|
||||
val resolvedType = resolver.resolveType(endType)
|
||||
resolvedType
|
||||
} else throw NotSerializableException("No inheritance path between actual $actualClass and declared $declaredType.")
|
||||
} else actualClass
|
||||
} else throw NotSerializableException("Found object of type $actualClass in a property expecting $declaredType")
|
||||
}
|
||||
|
||||
// Stop when reach declared type or return null if we don't find it.
|
||||
private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: MutableList<Type>): List<Type>? {
|
||||
chain.add(startingType)
|
||||
val startingClass = startingType.asClass()
|
||||
if (startingClass == declaredType.asClass()) {
|
||||
// We're done...
|
||||
return chain
|
||||
}
|
||||
// Now explore potential options of superclass and all interfaces
|
||||
val superClass = startingClass?.genericSuperclass
|
||||
val superClassChain = if (superClass != null) {
|
||||
val resolved = TypeResolver().where(startingClass.asParameterizedType(), startingType.asParameterizedType()).resolveType(superClass)
|
||||
findPathToDeclared(resolved, declaredType, ArrayList(chain))
|
||||
} else null
|
||||
if (superClassChain != null) return superClassChain
|
||||
for (iface in startingClass?.genericInterfaces ?: emptyArray()) {
|
||||
val resolved = TypeResolver().where(startingClass!!.asParameterizedType(), startingType.asParameterizedType()).resolveType(iface)
|
||||
return findPathToDeclared(resolved, declaredType, ArrayList(chain)) ?: continue
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
|
||||
* contained in the [Schema].
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun get(typeDescriptor: Any, schema: SerializationSchemas): AMQPSerializer<Any> {
|
||||
return serializersByDescriptor[typeDescriptor] ?: {
|
||||
processSchema(FactorySchemaAndDescriptor(schema, typeDescriptor))
|
||||
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
|
||||
"Could not find type matching descriptor $typeDescriptor.")
|
||||
}()
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
|
||||
* that expects to find getters and a constructor with a parameter for each property.
|
||||
*/
|
||||
open fun register(customSerializer: CustomSerializer<out Any>) {
|
||||
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
|
||||
customSerializers += customSerializer
|
||||
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
|
||||
for (additional in customSerializer.additionalSerializers) {
|
||||
register(additional)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerExternal(customSerializer: CorDappCustomSerializer) {
|
||||
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
|
||||
customSerializers += customSerializer
|
||||
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over an AMQP schema, for each type ascertain whether it's on ClassPath of [classloader] and,
|
||||
* if not, use the [ClassCarpenter] to generate a class to use in its place.
|
||||
*/
|
||||
private fun processSchema(schemaAndDescriptor: FactorySchemaAndDescriptor, sentinel: Boolean = false) {
|
||||
val metaSchema = CarpenterMetaSchema.newInstance()
|
||||
for (typeNotation in schemaAndDescriptor.schemas.schema.types) {
|
||||
try {
|
||||
val serialiser = processSchemaEntry(typeNotation)
|
||||
// if we just successfully built a serializer for the type but the type fingerprint
|
||||
// doesn't match that of the serialised object then we are dealing with different
|
||||
// instance of the class, as such we need to build an EvolutionSerializer
|
||||
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
|
||||
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
if (sentinel) throw e
|
||||
metaSchema.buildFor(typeNotation, classloader)
|
||||
}
|
||||
}
|
||||
|
||||
if (metaSchema.isNotEmpty()) {
|
||||
runCarpentry(schemaAndDescriptor, metaSchema)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCarpentry(schemaAndDescriptor: FactorySchemaAndDescriptor, metaSchema: CarpenterMetaSchema) {
|
||||
val mc = MetaCarpenter(metaSchema, classCarpenter)
|
||||
try {
|
||||
mc.build()
|
||||
} catch (e: MetaCarpenterException) {
|
||||
// preserve the actual message locally
|
||||
loggerFor<SerializerFactory>().apply {
|
||||
error("${e.message} [hint: enable trace debugging for the stack trace]")
|
||||
trace("", e)
|
||||
}
|
||||
|
||||
// prevent carpenter exceptions escaping into the world, convert things into a nice
|
||||
// NotSerializableException for when this escapes over the wire
|
||||
throw NotSerializableException(e.name)
|
||||
}
|
||||
processSchema(schemaAndDescriptor, true)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
return get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
|
||||
}
|
||||
|
||||
private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) {
|
||||
if (clazz.isSynthetic) {
|
||||
// Explicitly ban synthetic classes, we have no way of recreating them when deserializing. This also
|
||||
// captures Lambda expressions and other anonymous functions
|
||||
throw NotSerializableException(type.typeName)
|
||||
} else if (isPrimitive(clazz)) {
|
||||
AMQPPrimitiveSerializer(clazz)
|
||||
} else {
|
||||
findCustomSerializer(clazz, declaredType) ?: run {
|
||||
if (type.isArray()) {
|
||||
// Don't need to check the whitelist since each element will come back through the whitelisting process.
|
||||
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
|
||||
else ArraySerializer.make(type, this)
|
||||
} else {
|
||||
val singleton = clazz.objectInstance()
|
||||
if (singleton != null) {
|
||||
whitelist.requireWhitelisted(clazz)
|
||||
SingletonSerializer(clazz, singleton, this)
|
||||
} else {
|
||||
whitelist.requireWhitelisted(type)
|
||||
ObjectSerializer(type, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
for (customSerializer in customSerializers) {
|
||||
if (customSerializer.isSerializerFor(clazz)) {
|
||||
val declaredSuperClass = declaredType.asClass()?.superclass
|
||||
return if (declaredSuperClass == null
|
||||
|| !customSerializer.isSerializerFor(declaredSuperClass)
|
||||
|| !customSerializer.revealSubclassesInSchema) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
customSerializer as? AMQPSerializer<Any>
|
||||
} else {
|
||||
// Make a subclass serializer for the subclass and return that...
|
||||
CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer<Any> {
|
||||
val rawType = declaredType.rawType as Class<*>
|
||||
rawType.checkSupportedMapType()
|
||||
return MapSerializer(declaredType, this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isPrimitive(type: Type): Boolean = primitiveTypeName(type) != null
|
||||
|
||||
fun primitiveTypeName(type: Type): String? {
|
||||
val clazz = type as? Class<*> ?: return null
|
||||
return primitiveTypeNames[Primitives.unwrap(clazz)]
|
||||
}
|
||||
|
||||
fun primitiveType(type: String): Class<*>? {
|
||||
return namesOfPrimitiveTypes[type]
|
||||
}
|
||||
|
||||
private val primitiveTypeNames: Map<Class<*>, String> = mapOf(
|
||||
Character::class.java to "char",
|
||||
Char::class.java to "char",
|
||||
Boolean::class.java to "boolean",
|
||||
Byte::class.java to "byte",
|
||||
UnsignedByte::class.java to "ubyte",
|
||||
Short::class.java to "short",
|
||||
UnsignedShort::class.java to "ushort",
|
||||
Int::class.java to "int",
|
||||
UnsignedInteger::class.java to "uint",
|
||||
Long::class.java to "long",
|
||||
UnsignedLong::class.java to "ulong",
|
||||
Float::class.java to "float",
|
||||
Double::class.java to "double",
|
||||
Decimal32::class.java to "decimal32",
|
||||
Decimal64::class.java to "decimal62",
|
||||
Decimal128::class.java to "decimal128",
|
||||
Date::class.java to "timestamp",
|
||||
UUID::class.java to "uuid",
|
||||
ByteArray::class.java to "binary",
|
||||
String::class.java to "string",
|
||||
Symbol::class.java to "symbol")
|
||||
|
||||
private val namesOfPrimitiveTypes: Map<String, Class<*>> = primitiveTypeNames.map { it.value to it.key }.toMap()
|
||||
|
||||
fun nameForType(type: Type): String = when (type) {
|
||||
is Class<*> -> {
|
||||
primitiveTypeName(type) ?: if (type.isArray) {
|
||||
"${nameForType(type.componentType)}${if (type.componentType.isPrimitive) "[p]" else "[]"}"
|
||||
} else type.name
|
||||
}
|
||||
is ParameterizedType -> {
|
||||
"${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
|
||||
}
|
||||
is GenericArrayType -> "${nameForType(type.genericComponentType)}[]"
|
||||
is WildcardType -> "?"
|
||||
is TypeVariable<*> -> "?"
|
||||
else -> throw NotSerializableException("Unable to render type $type to a string.")
|
||||
}
|
||||
|
||||
private fun typeForName(name: String, classloader: ClassLoader): Type {
|
||||
return if (name.endsWith("[]")) {
|
||||
val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader)
|
||||
if (elementType is ParameterizedType || elementType is GenericArrayType) {
|
||||
DeserializedGenericArrayType(elementType)
|
||||
} else if (elementType is Class<*>) {
|
||||
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
|
||||
} else {
|
||||
throw NotSerializableException("Not able to deserialize array type: $name")
|
||||
}
|
||||
} else if (name.endsWith("[p]")) {
|
||||
// There is no need to handle the ByteArray case as that type is coercible automatically
|
||||
// to the binary type and is thus handled by the main serializer and doesn't need a
|
||||
// special case for a primitive array of bytes
|
||||
when (name) {
|
||||
"int[p]" -> IntArray::class.java
|
||||
"char[p]" -> CharArray::class.java
|
||||
"boolean[p]" -> BooleanArray::class.java
|
||||
"float[p]" -> FloatArray::class.java
|
||||
"double[p]" -> DoubleArray::class.java
|
||||
"short[p]" -> ShortArray::class.java
|
||||
"long[p]" -> LongArray::class.java
|
||||
else -> throw NotSerializableException("Not able to deserialize array type: $name")
|
||||
}
|
||||
} else {
|
||||
DeserializedParameterizedType.make(name, classloader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AnyType : WildcardType {
|
||||
override fun getUpperBounds(): Array<Type> = arrayOf(Object::class.java)
|
||||
|
||||
override fun getLowerBounds(): Array<Type> = emptyArray()
|
||||
|
||||
override fun toString(): String = "?"
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A custom serializer that transports nothing on the wire (except a boolean "false", since AMQP does not support
|
||||
* absolutely nothing, or null as a described type) when we have a singleton within the node that we just
|
||||
* 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 = Symbol.valueOf(
|
||||
"$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
|
||||
|
||||
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), emptyList())
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) {
|
||||
output.writeTypeNotations(typeNotation)
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext, debugIndent: Int
|
||||
) {
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
data.putBoolean(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput, context: SerializationContext
|
||||
): Any {
|
||||
return singleton
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
|
||||
import net.corda.core.serialization.CordaSerializationTransformRename
|
||||
import net.corda.core.serialization.CordaSerializationTransformRenames
|
||||
|
||||
/**
|
||||
* Utility class that defines an instance of a transform we support.
|
||||
*
|
||||
* @property type The transform annotation.
|
||||
* @property enum Maps the annotaiton onto a transform type, we expect there are multiple annotations that
|
||||
* would map to a single transform type.
|
||||
* @property getAnnotations Anonymous function that should return a list of Annotations encapsualted by the parent annotation
|
||||
* that reference the transform. Notionally this allows the code that extracts transforms to work on single instances
|
||||
* of a transform or a meta list of them.
|
||||
*/
|
||||
data class SupportedTransform(
|
||||
val type: Class<out Annotation>,
|
||||
val enum: TransformTypes,
|
||||
val getAnnotations: (Annotation) -> List<Annotation>)
|
||||
|
||||
/**
|
||||
* Extract from an annotated class the list of annotations that refer to a particular
|
||||
* transformation type when that class has multiple transforms wrapped in an
|
||||
* outer annotation
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val wrapperExtract = { x: Annotation ->
|
||||
(x::class.java.getDeclaredMethod("value").invoke(x) as Array<Annotation>).toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract from an annotated class the list of annotations that refer to a particular
|
||||
* transformation type when that class has a single decorator applied
|
||||
*/
|
||||
private val singleExtract = { x: Annotation -> listOf(x) }
|
||||
|
||||
// Transform annotation used to test the handling of transforms the de-serialising node doesn't understand. At
|
||||
// some point test cases will have been created with this transform applied.
|
||||
// @Target(AnnotationTarget.CLASS)
|
||||
// @Retention(AnnotationRetention.RUNTIME)
|
||||
// annotation class UnknownTransformAnnotation(val a: Int, val b: Int, val c: Int)
|
||||
|
||||
/**
|
||||
* Utility list of all transforms we support that simplifies our generation code.
|
||||
*
|
||||
* NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation
|
||||
* when many instances are repeated.
|
||||
*/
|
||||
val supportedTransforms = listOf(
|
||||
SupportedTransform(
|
||||
CordaSerializationTransformEnumDefaults::class.java,
|
||||
TransformTypes.EnumDefault,
|
||||
wrapperExtract
|
||||
),
|
||||
SupportedTransform(
|
||||
CordaSerializationTransformEnumDefault::class.java,
|
||||
TransformTypes.EnumDefault,
|
||||
singleExtract
|
||||
),
|
||||
SupportedTransform(
|
||||
CordaSerializationTransformRenames::class.java,
|
||||
TransformTypes.Rename,
|
||||
wrapperExtract
|
||||
),
|
||||
SupportedTransform(
|
||||
CordaSerializationTransformRename::class.java,
|
||||
TransformTypes.Rename,
|
||||
singleExtract
|
||||
)
|
||||
//,SupportedTransform(
|
||||
// UnknownTransformAnnotation::class.java,
|
||||
// TransformTypes.UnknownTest,
|
||||
// singleExtract)
|
||||
)
|
@ -1,134 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
|
||||
import net.corda.core.serialization.CordaSerializationTransformRename
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
||||
import java.io.NotSerializableException
|
||||
|
||||
/**
|
||||
* Enumerated type that represents each transform that can be applied to a class. Used as the key type in
|
||||
* the [TransformsSchema] map for each class.
|
||||
*
|
||||
* @property build should be a function that takes a transform [Annotation] (currently one of
|
||||
* [CordaSerializationTransformRename] or [CordaSerializationTransformEnumDefaults])
|
||||
* and constructs an instance of the corresponding [Transform] type
|
||||
*
|
||||
* DO NOT REORDER THE CONSTANTS!!! Please append any new entries to the end
|
||||
*/
|
||||
// TODO: it would be awesome to auto build this list by scanning for transform annotations themselves
|
||||
// TODO: annotated with some annotation
|
||||
enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType {
|
||||
/**
|
||||
* Placeholder entry for future transforms where a node receives a transform we've subsequently
|
||||
* added and thus the de-serialising node doesn't know about that transform.
|
||||
*/
|
||||
Unknown({ UnknownTransform() }) {
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
override fun getDescribed(): Any = ordinal
|
||||
override fun validate(list: List<Transform>, constants: Map<String, Int>) {}
|
||||
},
|
||||
EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) {
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
override fun getDescribed(): Any = ordinal
|
||||
|
||||
/**
|
||||
* Validates a list of constant additions to an enumerated type. To be valid a default (the value
|
||||
* that should be used when we cannot use the new value) must refer to a constant that exists in the
|
||||
* enum class as it exists now and it cannot refer to itself.
|
||||
*
|
||||
* @param list The list of transforms representing new constants and the mapping from that constant to an
|
||||
* existing value
|
||||
* @param constants The list of enum constants on the type the transforms are being applied to
|
||||
*/
|
||||
override fun validate(list: List<Transform>, constants: Map<String, Int>) {
|
||||
uncheckedCast<List<Transform>, List<EnumDefaultSchemaTransform>>(list).forEach {
|
||||
if (!constants.contains(it.new)) {
|
||||
throw NotSerializableException("Unknown enum constant ${it.new}")
|
||||
}
|
||||
|
||||
if (!constants.contains(it.old)) {
|
||||
throw NotSerializableException(
|
||||
"Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " +
|
||||
"doesn't exist in constant set $constants")
|
||||
}
|
||||
|
||||
if (it.old == it.new) {
|
||||
throw NotSerializableException("Enum extension ${it.new} cannot default to itself")
|
||||
}
|
||||
|
||||
if (constants[it.old]!! >= constants[it.new]!!) {
|
||||
throw NotSerializableException(
|
||||
"Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " +
|
||||
"defaults to ${it.old}[${constants[it.old]}] which is greater")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) {
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
override fun getDescribed(): Any = ordinal
|
||||
|
||||
/**
|
||||
* Validates a list of rename transforms is valid. Such a list isn't valid if we detect a cyclic chain,
|
||||
* that is a constant is renamed to something that used to exist in the enum. We do this for both
|
||||
* the same constant (i.e. C -> D -> C) and multiple constants (C->D, B->C)
|
||||
*
|
||||
* @param list The list of transforms representing the renamed constants and the mapping between their new
|
||||
* and old values
|
||||
* @param constants The list of enum constants on the type the transforms are being applied to
|
||||
*/
|
||||
override fun validate(list: List<Transform>, constants: Map<String, Int>) {
|
||||
object : Any() {
|
||||
val from: MutableSet<String> = mutableSetOf()
|
||||
val to: MutableSet<String> = mutableSetOf()
|
||||
}.apply {
|
||||
@Suppress("UNCHECKED_CAST") (list as List<RenameSchemaTransform>).forEach { rename ->
|
||||
if (rename.to in this.to || rename.from in this.from) {
|
||||
throw NotSerializableException("Cyclic renames are not allowed (${rename.to})")
|
||||
}
|
||||
|
||||
this.to.add(rename.from)
|
||||
this.from.add(rename.to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Transform used to test the unknown handler, leave this at as the final constant, uncomment
|
||||
// when regenerating test cases - if Java had a pre-processor this would be much neater
|
||||
//
|
||||
//,UnknownTest({ a -> UnknownTestTransform((a as UnknownTransformAnnotation).a, a.b, a.c)}) {
|
||||
// override fun getDescriptor(): Any = DESCRIPTOR
|
||||
// override fun getDescribed(): Any = ordinal
|
||||
//}
|
||||
;
|
||||
|
||||
abstract fun validate(list: List<Transform>, constants: Map<String, Int>)
|
||||
|
||||
companion object : DescribedTypeConstructor<TransformTypes> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor
|
||||
|
||||
/**
|
||||
* Used to construct an instance of the object from the serialised bytes
|
||||
*
|
||||
* @param obj the serialised byte object from the AMQP serialised stream
|
||||
*/
|
||||
override fun newInstance(obj: Any?): TransformTypes {
|
||||
val describedType = obj as DescribedType
|
||||
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
|
||||
return try {
|
||||
values()[describedType.described as Int]
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
values()[0]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = TransformTypes::class.java
|
||||
}
|
||||
}
|
@ -1,336 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
|
||||
import net.corda.core.serialization.CordaSerializationTransformRename
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.codec.DescribedTypeConstructor
|
||||
import java.io.NotSerializableException
|
||||
import java.util.*
|
||||
|
||||
// NOTE: We are effectively going to replicate the annotations, we need to do this because
|
||||
// we can't instantiate instances of those annotation classes and this code needs to
|
||||
// work at the de-serialising end
|
||||
/**
|
||||
* Base class for representations of specific types of transforms as applied to a type within the
|
||||
* Corda serialisation framework
|
||||
*/
|
||||
abstract class Transform : DescribedType {
|
||||
companion object : DescribedTypeConstructor<Transform> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT.amqpDescriptor
|
||||
|
||||
/**
|
||||
* @param obj: a serialized instance of a described type, should be one of the
|
||||
* descendants of this class
|
||||
*/
|
||||
private fun checkDescribed(obj: Any?): Any? {
|
||||
val describedType = obj as DescribedType
|
||||
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
|
||||
return describedType.described
|
||||
}
|
||||
|
||||
/**
|
||||
* From an encoded descendant return an instance of the specific type. Transforms are encoded into
|
||||
* the schema as a list of class name and parameters.Using the class name (list element 0)
|
||||
* create the appropriate class instance
|
||||
*
|
||||
* For future proofing any unknown transform types are not treated as errors, rather we
|
||||
* simply create a placeholder object so we can ignore it
|
||||
*
|
||||
* @param obj: a serialized instance of a described type, should be one of the
|
||||
* descendants of this class
|
||||
*/
|
||||
override fun newInstance(obj: Any?): Transform {
|
||||
val described = Transform.checkDescribed(obj) as List<*>
|
||||
return when (described[0]) {
|
||||
EnumDefaultSchemaTransform.typeName -> EnumDefaultSchemaTransform.newInstance(described)
|
||||
RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described)
|
||||
else -> UnknownTransform()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = Transform::class.java
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
/**
|
||||
* Return a string representation of a transform in terms of key / value pairs, used
|
||||
* by the serializer to encode arbitrary transforms
|
||||
*/
|
||||
abstract fun params(): String
|
||||
|
||||
abstract val name: String
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform type placeholder that allows for backward compatibility. Should a noce recieve
|
||||
* a transform type it doesn't recognise, we can will use this as a placeholder
|
||||
*/
|
||||
class UnknownTransform : Transform() {
|
||||
companion object : DescribedTypeConstructor<UnknownTransform> {
|
||||
const val typeName = "UnknownTransform"
|
||||
|
||||
override fun newInstance(obj: Any?) = UnknownTransform()
|
||||
|
||||
override fun getTypeClass(): Class<*> = UnknownTransform::class.java
|
||||
}
|
||||
|
||||
override fun getDescribed(): Any = emptyList<Any>()
|
||||
override fun params() = ""
|
||||
|
||||
override val name: String get() = typeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the unit testing framework
|
||||
*/
|
||||
class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
|
||||
companion object : DescribedTypeConstructor<UnknownTestTransform> {
|
||||
const val typeName = "UnknownTest"
|
||||
|
||||
override fun newInstance(obj: Any?): UnknownTestTransform {
|
||||
val described = obj as List<*>
|
||||
return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = UnknownTransform::class.java
|
||||
}
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, a, b, c)
|
||||
override fun params() = ""
|
||||
|
||||
override val name: String get() = typeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to be used on an Enumerated Type whenever a new element is added
|
||||
*
|
||||
* @property old The value the [new] instance should default to when not available
|
||||
* @property new the value (as a String) that has been added
|
||||
*/
|
||||
class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() {
|
||||
companion object : DescribedTypeConstructor<EnumDefaultSchemaTransform> {
|
||||
/**
|
||||
* Value encoded into the schema that identifies a transform as this type
|
||||
*/
|
||||
const val typeName = "EnumDefault"
|
||||
|
||||
override fun newInstance(obj: Any?): EnumDefaultSchemaTransform {
|
||||
val described = obj as List<*>
|
||||
val old = described[1] as? String ?: throw IllegalStateException("Was expecting \"old\" as a String")
|
||||
val new = described[2] as? String ?: throw IllegalStateException("Was expecting \"new\" as a String")
|
||||
return EnumDefaultSchemaTransform(old, new)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = EnumDefaultSchemaTransform::class.java
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
constructor (annotation: CordaSerializationTransformEnumDefault) : this(annotation.old, annotation.new)
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, old, new)
|
||||
override fun params() = "old=${old.esc()} new=${new.esc()}"
|
||||
|
||||
override fun equals(other: Any?) = (
|
||||
(other is EnumDefaultSchemaTransform && other.new == new && other.old == old) || super.equals(other))
|
||||
|
||||
override fun hashCode() = (17 * new.hashCode()) + old.hashCode()
|
||||
|
||||
override val name: String get() = typeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform applied to either a class or enum where a property is renamed
|
||||
*
|
||||
* @property from the name of the property or constant prior to being changed, i.e. what it was
|
||||
* @property to the new name of the property or constant after the change has been made, i.e. what it is now
|
||||
*/
|
||||
class RenameSchemaTransform(val from: String, val to: String) : Transform() {
|
||||
companion object : DescribedTypeConstructor<RenameSchemaTransform> {
|
||||
/**
|
||||
* Value encoded into the schema that identifies a transform as this type
|
||||
*/
|
||||
const val typeName = "Rename"
|
||||
|
||||
override fun newInstance(obj: Any?): RenameSchemaTransform {
|
||||
val described = obj as List<*>
|
||||
val from = described[1] as? String ?: throw IllegalStateException("Was expecting \"from\" as a String")
|
||||
val to = described[2] as? String ?: throw IllegalStateException("Was expecting \"to\" as a String")
|
||||
return RenameSchemaTransform(from, to)
|
||||
}
|
||||
|
||||
override fun getTypeClass(): Class<*> = RenameSchemaTransform::class.java
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
constructor (annotation: CordaSerializationTransformRename) : this(annotation.from, annotation.to)
|
||||
|
||||
override fun getDescribed(): Any = listOf(name, from, to)
|
||||
|
||||
override fun params() = "from=${from.esc()} to=${to.esc()}"
|
||||
|
||||
override fun equals(other: Any?) = (
|
||||
(other is RenameSchemaTransform && other.from == from && other.to == to) || super.equals(other))
|
||||
|
||||
override fun hashCode() = (11 * from.hashCode()) + to.hashCode()
|
||||
|
||||
override val name: String get() = typeName
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the set of all transforms that can be a applied to all classes represented as part of
|
||||
* an AMQP schema. It forms a part of the AMQP envelope alongside the [Schema] and the serialized bytes
|
||||
*
|
||||
* @property types maps class names to a map of transformation types. In turn those transformation types
|
||||
* are each a list of instances o that transform.
|
||||
*/
|
||||
data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, MutableList<Transform>>>) : DescribedType {
|
||||
companion object : DescribedTypeConstructor<TransformsSchema> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_SCHEMA.amqpDescriptor
|
||||
|
||||
/**
|
||||
* Takes a class name and either returns a cached instance of the TransformSet for it or, on a cache miss,
|
||||
* instantiates the transform set before inserting into the cache and returning it.
|
||||
*
|
||||
* @param name fully qualified class name to lookup transforms for
|
||||
* @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own
|
||||
* class loader and this dictates which classes we can and cannot see
|
||||
*/
|
||||
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
|
||||
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
try {
|
||||
val clazz = sf.classloader.loadClass(name)
|
||||
|
||||
supportedTransforms.forEach { transform ->
|
||||
clazz.getAnnotation(transform.type)?.let { list ->
|
||||
transform.getAnnotations(list).forEach { annotation ->
|
||||
val t = transform.enum.build(annotation)
|
||||
|
||||
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
|
||||
// ignore them it feels like a good thing to alert the user to since this is
|
||||
// more than likely a typo in their code so best make it an actual error
|
||||
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }.any { t == it }) {
|
||||
throw NotSerializableException(
|
||||
"Repeated unique transformation annotation of type ${t.name}")
|
||||
}
|
||||
|
||||
transforms[transform.enum]!!.add(t)
|
||||
}
|
||||
|
||||
transform.enum.validate(
|
||||
transforms[transform.enum] ?: emptyList(),
|
||||
clazz.enumConstants.mapIndexed { i, s -> Pair(s.toString(), i) }.toMap())
|
||||
}
|
||||
}
|
||||
} catch (_: ClassNotFoundException) {
|
||||
// if we can't load the class we'll end up caching an empty list which is fine as that
|
||||
// list, on lookup, won't be included in the schema because it's empty
|
||||
}
|
||||
|
||||
transforms
|
||||
}
|
||||
|
||||
private fun getAndAdd(
|
||||
type: String,
|
||||
sf: SerializerFactory,
|
||||
map: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
|
||||
get(type, sf).apply {
|
||||
if (isNotEmpty()) {
|
||||
map[type] = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a schema for encoding, takes all of the types being transmitted and inspects each
|
||||
* one for any transform annotations. If there are any build up a set that can be
|
||||
* encoded into the AMQP [Envelope]
|
||||
*
|
||||
* @param schema should be a [Schema] generated for a serialised data structure
|
||||
* @param sf should be provided by the same serialization context that generated the schema
|
||||
*/
|
||||
fun build(schema: Schema, sf: SerializerFactory) = TransformsSchema(
|
||||
mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>().apply {
|
||||
schema.types.forEach { type -> getAndAdd(type.name, sf, this) }
|
||||
})
|
||||
|
||||
override fun getTypeClass(): Class<*> = TransformsSchema::class.java
|
||||
|
||||
/**
|
||||
* Constructs an instance of the object from the serialised form of an instance
|
||||
* of this object
|
||||
*/
|
||||
override fun newInstance(described: Any?): TransformsSchema {
|
||||
val rtn = mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>()
|
||||
val describedType = described as? DescribedType ?: return TransformsSchema(rtn)
|
||||
|
||||
if (describedType.descriptor != DESCRIPTOR) {
|
||||
throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.")
|
||||
}
|
||||
|
||||
val map = describedType.described as? Map<*, *>
|
||||
?: throw NotSerializableException("Transform schema must be encoded as a map")
|
||||
|
||||
map.forEach { type ->
|
||||
val fingerprint = type.key as? String
|
||||
?: throw NotSerializableException("Fingerprint must be encoded as a string")
|
||||
|
||||
rtn[fingerprint] = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
|
||||
(type.value as Map<*, *>).forEach { transformType, transforms ->
|
||||
val transform = TransformTypes.newInstance(transformType)
|
||||
|
||||
rtn[fingerprint]!![transform] = mutableListOf()
|
||||
(transforms as List<*>).forEach {
|
||||
rtn[fingerprint]!![TransformTypes.newInstance(transformType)]?.add(Transform.newInstance(it))
|
||||
?: throw NotSerializableException("De-serialization error with transform for class "
|
||||
+ "${type.key} ${transform.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TransformsSchema(rtn)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
|
||||
override fun getDescribed(): Any = types
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override fun toString(): String {
|
||||
data class Indent(val indent: String) {
|
||||
@Suppress("UNUSED") constructor(i: Indent) : this(" ${i.indent}")
|
||||
|
||||
override fun toString() = indent
|
||||
}
|
||||
|
||||
val sb = StringBuilder("")
|
||||
val indent = Indent("")
|
||||
|
||||
sb.appendln("$indent<type-transforms>")
|
||||
types.forEach { type ->
|
||||
val indent = Indent(indent)
|
||||
sb.appendln("$indent<type name=${type.key.esc()}>")
|
||||
type.value.forEach { transform ->
|
||||
val indent = Indent(indent)
|
||||
sb.appendln("$indent<transforms type=${transform.key.name.esc()}>")
|
||||
transform.value.forEach {
|
||||
val indent = Indent(indent)
|
||||
sb.appendln("$indent<transform ${it.params()} />")
|
||||
}
|
||||
sb.appendln("$indent</transforms>")
|
||||
}
|
||||
sb.appendln("$indent</type>")
|
||||
}
|
||||
sb.appendln("$indent</type-transforms>")
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.esc() = "\"$this\""
|
@ -1,11 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* A serializer for [BigDecimal], utilising the string based helper. [BigDecimal] seems to have no import/export
|
||||
* features that are precision independent other than via a string. The format of the string is discussed in the
|
||||
* documentation for [BigDecimal.toString].
|
||||
*/
|
||||
object BigDecimalSerializer : CustomSerializer.ToString<BigDecimal>(BigDecimal::class.java)
|
@ -1,11 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* A serializer for [BigInteger], utilising the string based helper. [BigInteger] seems to have no import/export
|
||||
* features that are precision independent other than via a string. The format of the string is discussed in the
|
||||
* documentation for [BigInteger.toString].
|
||||
*/
|
||||
object BigIntegerSerializer : CustomSerializer.ToString<BigInteger>(BigInteger::class.java)
|
@ -1,17 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A serializer that writes out a [BitSet] as an integer number of bits, plus the necessary number of bytes to encode that
|
||||
* many bits.
|
||||
*/
|
||||
class BitSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<BitSet, BitSetSerializer.BitSetProxy>(BitSet::class.java, BitSetProxy::class.java, factory) {
|
||||
override fun toProxy(obj: BitSet): BitSetProxy = BitSetProxy(obj.toByteArray())
|
||||
|
||||
override fun fromProxy(proxy: BitSetProxy): BitSet = BitSet.valueOf(proxy.bytes)
|
||||
|
||||
data class BitSetProxy(val bytes: ByteArray)
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.io.NotSerializableException
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
|
||||
class CertPathSerializer(factory: SerializerFactory)
|
||||
: CustomSerializer.Proxy<CertPath, CertPathSerializer.CertPathProxy>(CertPath::class.java, CertPathProxy::class.java, factory) {
|
||||
override fun toProxy(obj: CertPath): CertPathProxy = CertPathProxy(obj.type, obj.encoded)
|
||||
|
||||
override fun fromProxy(proxy: CertPathProxy): CertPath {
|
||||
try {
|
||||
val cf = CertificateFactory.getInstance(proxy.type)
|
||||
return cf.generateCertPath(proxy.encoded.inputStream())
|
||||
} catch (ce: CertificateException) {
|
||||
val nse = NotSerializableException("java.security.cert.CertPath: $type")
|
||||
nse.initCause(ce)
|
||||
throw nse
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class CertPathProxy(val type: String, val encoded: ByteArray)
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
|
||||
/**
|
||||
* A serializer for [Class] that uses [ClassProxy] proxy object to write out
|
||||
*/
|
||||
class ClassSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Class<*>, ClassSerializer.ClassProxy>(Class::class.java, ClassProxy::class.java, factory) {
|
||||
override fun toProxy(obj: Class<*>): ClassProxy = ClassProxy(obj.name)
|
||||
|
||||
override fun fromProxy(proxy: ClassProxy): Class<*> = Class.forName(proxy.className, true, factory.classloader)
|
||||
|
||||
data class ClassProxy(val className: String)
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.nodeapi.internal.serialization.GeneratedAttachment
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
|
||||
/**
|
||||
* A serializer for [ContractAttachment] that uses a proxy object to write out the full attachment eagerly.
|
||||
* @param factory the serializerFactory
|
||||
*/
|
||||
class ContractAttachmentSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<ContractAttachment,
|
||||
ContractAttachmentSerializer.ContractAttachmentProxy>(ContractAttachment::class.java,
|
||||
ContractAttachmentProxy::class.java, factory) {
|
||||
override fun toProxy(obj: ContractAttachment): ContractAttachmentProxy {
|
||||
val bytes = try {
|
||||
obj.attachment.open().readFully()
|
||||
} catch (e: Exception) {
|
||||
throw MissingAttachmentsException(listOf(obj.id))
|
||||
}
|
||||
return ContractAttachmentProxy(GeneratedAttachment(bytes), obj.contract, obj.additionalContracts, obj.uploader)
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ContractAttachmentProxy): ContractAttachment {
|
||||
return ContractAttachment(proxy.attachment, proxy.contract, proxy.contracts, proxy.uploader)
|
||||
}
|
||||
|
||||
data class ContractAttachmentProxy(val attachment: Attachment, val contract: ContractClassName, val contracts: Set<ContractClassName>, val uploader: String?)
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A custom serializer for the [Currency] class, utilizing the currency code string representation.
|
||||
*/
|
||||
object CurrencySerializer : CustomSerializer.ToString<Currency>(Currency::class.java,
|
||||
withInheritance = false,
|
||||
maker = { Currency.getInstance(it) },
|
||||
unmaker = { it.currencyCode })
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* A serializer for [Duration] that uses a proxy object to write out the seconds and the nanos.
|
||||
*/
|
||||
class DurationSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Duration, DurationSerializer.DurationProxy>(Duration::class.java, DurationProxy::class.java, factory) {
|
||||
override fun toProxy(obj: Duration): DurationProxy = DurationProxy(obj.seconds, obj.nano)
|
||||
|
||||
override fun fromProxy(proxy: DurationProxy): Duration = Duration.ofSeconds(proxy.seconds, proxy.nanos.toLong())
|
||||
|
||||
data class DurationProxy(val seconds: Long, val nanos: Int)
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.MapSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A serializer that writes out an [EnumSet] as a type, plus list of instances in the set.
|
||||
*/
|
||||
class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<EnumSet<*>, EnumSetSerializer.EnumSetProxy>(EnumSet::class.java, EnumSetProxy::class.java, factory) {
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(ClassSerializer(factory))
|
||||
|
||||
override fun toProxy(obj: EnumSet<*>): EnumSetProxy = EnumSetProxy(elementType(obj), obj.toList())
|
||||
|
||||
private fun elementType(set: EnumSet<*>): Class<*> {
|
||||
return if (set.isEmpty()) {
|
||||
EnumSet.complementOf(uncheckedCast<EnumSet<*>, EnumSet<MapSerializer.EnumJustUsedForCasting>>(set)).first().javaClass
|
||||
} else {
|
||||
set.first().javaClass
|
||||
}
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: EnumSetProxy): EnumSet<*> {
|
||||
return if (proxy.elements.isEmpty()) {
|
||||
EnumSet.noneOf(uncheckedCast<Class<*>, Class<MapSerializer.EnumJustUsedForCasting>>(proxy.clazz))
|
||||
} else {
|
||||
EnumSet.copyOf(uncheckedCast<List<Any>, List<MapSerializer.EnumJustUsedForCasting>>(proxy.elements))
|
||||
}
|
||||
}
|
||||
|
||||
data class EnumSetProxy(val clazz: Class<*>, val elements: List<Any>)
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A serializer that writes out the content of an input stream as bytes and deserializes into a [ByteArrayInputStream].
|
||||
*/
|
||||
object InputStreamSerializer : CustomSerializer.Implements<InputStream>(InputStream::class.java) {
|
||||
override val revealSubclassesInSchema: Boolean = true
|
||||
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: InputStream, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
val startingSize = maxOf(4096, obj.available() + 1)
|
||||
var buffer = ByteArray(startingSize)
|
||||
var pos = 0
|
||||
while (true) {
|
||||
val numberOfBytesRead = obj.read(buffer, pos, buffer.size - pos)
|
||||
if (numberOfBytesRead != -1) {
|
||||
pos += numberOfBytesRead
|
||||
// If the buffer is now full, resize it.
|
||||
if (pos == buffer.size) {
|
||||
buffer = buffer.copyOf(buffer.size + maxOf(4096, obj.available() + 1))
|
||||
}
|
||||
} else {
|
||||
data.putBinary(Binary(buffer, 0, pos))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
) : InputStream {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||
return bits.inputStream()
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A serializer for [Instant] that uses a proxy object to write out the seconds since the epoch and the nanos.
|
||||
*/
|
||||
class InstantSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Instant, InstantSerializer.InstantProxy>(Instant::class.java, InstantProxy::class.java, factory) {
|
||||
override fun toProxy(obj: Instant): InstantProxy = InstantProxy(obj.epochSecond, obj.nano)
|
||||
|
||||
override fun fromProxy(proxy: InstantProxy): Instant = Instant.ofEpochSecond(proxy.epochSeconds, proxy.nanos.toLong())
|
||||
|
||||
data class InstantProxy(val epochSeconds: Long, val nanos: Int)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* A serializer for [LocalDate] that uses a proxy object to write out the year, month and day.
|
||||
*/
|
||||
class LocalDateSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<LocalDate, LocalDateSerializer.LocalDateProxy>(LocalDate::class.java, LocalDateProxy::class.java, factory) {
|
||||
override fun toProxy(obj: LocalDate): LocalDateProxy = LocalDateProxy(obj.year, obj.monthValue.toByte(), obj.dayOfMonth.toByte())
|
||||
|
||||
override fun fromProxy(proxy: LocalDateProxy): LocalDate = LocalDate.of(proxy.year, proxy.month.toInt(), proxy.day.toInt())
|
||||
|
||||
data class LocalDateProxy(val year: Int, val month: Byte, val day: Byte)
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
/**
|
||||
* A serializer for [LocalDateTime] that uses a proxy object to write out the date and time.
|
||||
*/
|
||||
class LocalDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<LocalDateTime, LocalDateTimeSerializer.LocalDateTimeProxy>(LocalDateTime::class.java, LocalDateTimeProxy::class.java, factory) {
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(LocalDateSerializer(factory), LocalTimeSerializer(factory))
|
||||
|
||||
override fun toProxy(obj: LocalDateTime): LocalDateTimeProxy = LocalDateTimeProxy(obj.toLocalDate(), obj.toLocalTime())
|
||||
|
||||
override fun fromProxy(proxy: LocalDateTimeProxy): LocalDateTime = LocalDateTime.of(proxy.date, proxy.time)
|
||||
|
||||
data class LocalDateTimeProxy(val date: LocalDate, val time: LocalTime)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.LocalTime
|
||||
|
||||
/**
|
||||
* A serializer for [LocalTime] that uses a proxy object to write out the hours, minutes, seconds and the nanos.
|
||||
*/
|
||||
class LocalTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<LocalTime, LocalTimeSerializer.LocalTimeProxy>(LocalTime::class.java, LocalTimeProxy::class.java, factory) {
|
||||
override fun toProxy(obj: LocalTime): LocalTimeProxy = LocalTimeProxy(obj.hour.toByte(), obj.minute.toByte(), obj.second.toByte(), obj.nano)
|
||||
|
||||
override fun fromProxy(proxy: LocalTimeProxy): LocalTime = LocalTime.of(proxy.hour.toInt(), proxy.minute.toInt(), proxy.second.toInt(), proxy.nano)
|
||||
|
||||
data class LocalTimeProxy(val hour: Byte, val minute: Byte, val second: Byte, val nano: Int)
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.MonthDay
|
||||
|
||||
/**
|
||||
* A serializer for [MonthDay] that uses a proxy object to write out the integer form.
|
||||
*/
|
||||
class MonthDaySerializer(factory: SerializerFactory)
|
||||
: CustomSerializer.Proxy<MonthDay, MonthDaySerializer.MonthDayProxy>(
|
||||
MonthDay::class.java, MonthDayProxy::class.java, factory
|
||||
) {
|
||||
override fun toProxy(obj: MonthDay): MonthDayProxy = MonthDayProxy(obj.monthValue.toByte(), obj.dayOfMonth.toByte())
|
||||
|
||||
override fun fromProxy(proxy: MonthDayProxy): MonthDay = MonthDay.of(proxy.month.toInt(), proxy.day.toInt())
|
||||
|
||||
data class MonthDayProxy(val month: Byte, val day: Byte)
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
/**
|
||||
* A serializer for [OffsetDateTime] that uses a proxy object to write out the date and zone offset.
|
||||
*/
|
||||
class OffsetDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<OffsetDateTime, OffsetDateTimeSerializer.OffsetDateTimeProxy>(OffsetDateTime::class.java, OffsetDateTimeProxy::class.java, factory) {
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(LocalDateTimeSerializer(factory), ZoneIdSerializer(factory))
|
||||
|
||||
override fun toProxy(obj: OffsetDateTime): OffsetDateTimeProxy = OffsetDateTimeProxy(obj.toLocalDateTime(), obj.offset)
|
||||
|
||||
override fun fromProxy(proxy: OffsetDateTimeProxy): OffsetDateTime = OffsetDateTime.of(proxy.dateTime, proxy.offset)
|
||||
|
||||
data class OffsetDateTimeProxy(val dateTime: LocalDateTime, val offset: ZoneOffset)
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.LocalTime
|
||||
import java.time.OffsetTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
/**
|
||||
* A serializer for [OffsetTime] that uses a proxy object to write out the time and zone offset.
|
||||
*/
|
||||
class OffsetTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<OffsetTime, OffsetTimeSerializer.OffsetTimeProxy>(OffsetTime::class.java, OffsetTimeProxy::class.java, factory) {
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(LocalTimeSerializer(factory), ZoneIdSerializer(factory))
|
||||
|
||||
override fun toProxy(obj: OffsetTime): OffsetTimeProxy = OffsetTimeProxy(obj.toLocalTime(), obj.offset)
|
||||
|
||||
override fun fromProxy(proxy: OffsetTimeProxy): OffsetTime = OffsetTime.of(proxy.time, proxy.offset)
|
||||
|
||||
data class OffsetTimeProxy(val time: LocalTime, val offset: ZoneOffset)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.OpaqueBytesSubSequence
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
|
||||
/**
|
||||
* A serializer for [OpaqueBytesSubSequence] that uses a proxy object to write out only content included into sequence
|
||||
* to save on network bandwidth
|
||||
* Uses [OpaqueBytes] as a proxy
|
||||
*/
|
||||
class OpaqueBytesSubSequenceSerializer(factory: SerializerFactory) :
|
||||
CustomSerializer.Proxy<OpaqueBytesSubSequence, OpaqueBytes>(OpaqueBytesSubSequence::class.java, OpaqueBytes::class.java, factory) {
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
|
||||
override fun toProxy(obj: OpaqueBytesSubSequence): OpaqueBytes = OpaqueBytes(obj.copyBytes())
|
||||
override fun fromProxy(proxy: OpaqueBytes): OpaqueBytesSubSequence = OpaqueBytesSubSequence(proxy.bytes, proxy.offset, proxy.size)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.Period
|
||||
|
||||
/**
|
||||
* A serializer for [Period] that uses a proxy object to write out the integer form.
|
||||
*/
|
||||
class PeriodSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Period, PeriodSerializer.PeriodProxy>(Period::class.java, PeriodProxy::class.java, factory) {
|
||||
override fun toProxy(obj: Period): PeriodProxy = PeriodProxy(obj.years, obj.months, obj.days)
|
||||
|
||||
override fun fromProxy(proxy: PeriodProxy): Period = Period.of(proxy.years, proxy.months, proxy.days)
|
||||
|
||||
data class PeriodProxy(val years: Int, val months: Int, val days: Int)
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Storage
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import net.corda.nodeapi.internal.serialization.checkUseCase
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.security.PrivateKey
|
||||
import java.util.*
|
||||
|
||||
object PrivateKeySerializer : CustomSerializer.Implements<PrivateKey>(PrivateKey::class.java) {
|
||||
|
||||
private val allowedUseCases = EnumSet.of(Storage, Checkpoint)
|
||||
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: PrivateKey, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
checkUseCase(allowedUseCases)
|
||||
output.writeObject(obj.encoded, data, clazz, context)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): PrivateKey {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||
return Crypto.decodePrivateKey(bits)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A serializer that writes out a public key in X.509 format.
|
||||
*/
|
||||
object PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::class.java) {
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
|
||||
|
||||
override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
// TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser.
|
||||
output.writeObject(obj.encoded, data, clazz, context)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): PublicKey {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||
return Crypto.decodePublicKey(bits)
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import net.corda.serialization.internal.amqp.CustomSerializer
|
||||
import net.corda.serialization.internal.amqp.SerializerFactory
|
||||
import rx.Notification
|
||||
|
||||
class RxNotificationSerializer(
|
||||
|
@ -1,9 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
|
||||
/**
|
||||
* A serializer for [SimpleString].
|
||||
*/
|
||||
object SimpleStringSerializer : CustomSerializer.ToString<SimpleString>(SimpleString::class.java)
|
@ -1,8 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
|
||||
/**
|
||||
* A serializer for [StringBuffer].
|
||||
*/
|
||||
object StringBufferSerializer : CustomSerializer.ToString<StringBuffer>(StringBuffer::class.java)
|
@ -1,92 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.CordaThrowable
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
|
||||
class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
override val revealSubclassesInSchema: Boolean = true
|
||||
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(StackTraceElementSerializer(factory))
|
||||
|
||||
override fun toProxy(obj: Throwable): ThrowableProxy {
|
||||
val extraProperties: MutableMap<String, Any?> = LinkedHashMap()
|
||||
val message = if (obj is CordaThrowable) {
|
||||
// Try and find a constructor
|
||||
try {
|
||||
val constructor = constructorForDeserialization(obj.javaClass)
|
||||
propertiesForSerializationFromConstructor(constructor!!, obj.javaClass, factory).forEach { property ->
|
||||
extraProperties[property.getter.name] = property.getter.propertyReader.read(obj)
|
||||
}
|
||||
} catch (e: NotSerializableException) {
|
||||
logger.warn("Unexpected exception", e)
|
||||
}
|
||||
obj.originalMessage
|
||||
} else {
|
||||
obj.message
|
||||
}
|
||||
val stackTraceToInclude = if (shouldIncludeInternalInfo()) obj.stackTrace else emptyArray()
|
||||
return ThrowableProxy(obj.javaClass.name, message, stackTraceToInclude, obj.cause, obj.suppressed, extraProperties)
|
||||
}
|
||||
|
||||
private fun shouldIncludeInternalInfo(): Boolean {
|
||||
val currentContext = SerializationFactory.currentFactory?.currentContext
|
||||
val includeInternalInfo = currentContext?.properties?.get(CommonPropertyNames.IncludeInternalInfo)
|
||||
return true == includeInternalInfo
|
||||
}
|
||||
|
||||
override fun fromProxy(proxy: ThrowableProxy): Throwable {
|
||||
try {
|
||||
// TODO: This will need reworking when we have multiple class loaders
|
||||
val clazz = Class.forName(proxy.exceptionClass, false, factory.classloader)
|
||||
// If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties
|
||||
// Otherwise we just make a CordaRuntimeException
|
||||
if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) {
|
||||
val constructor = constructorForDeserialization(clazz)!!
|
||||
val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap())
|
||||
(throwable as CordaThrowable).apply {
|
||||
if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass
|
||||
this.setMessage(proxy.message)
|
||||
this.setCause(proxy.cause)
|
||||
this.addSuppressed(proxy.suppressed)
|
||||
}
|
||||
return (throwable as Throwable).apply {
|
||||
this.stackTrace = proxy.stackTrace
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Unexpected exception de-serializing throwable: ${proxy.exceptionClass}. Converting to CordaRuntimeException.", e)
|
||||
}
|
||||
// If the criteria are not met or we experience an exception constructing the exception, we fall back to our own unchecked exception.
|
||||
return CordaRuntimeException(proxy.exceptionClass, null, null).apply {
|
||||
this.setMessage(proxy.message)
|
||||
this.setCause(proxy.cause)
|
||||
this.stackTrace = proxy.stackTrace
|
||||
this.addSuppressed(proxy.suppressed)
|
||||
}
|
||||
}
|
||||
|
||||
class ThrowableProxy(
|
||||
val exceptionClass: String,
|
||||
val message: String?,
|
||||
val stackTrace: Array<StackTraceElement>,
|
||||
val cause: Throwable?,
|
||||
val suppressed: Array<Throwable>,
|
||||
val additionalProperties: Map<String, Any?>)
|
||||
}
|
||||
|
||||
class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<StackTraceElement, StackTraceElementSerializer.StackTraceElementProxy>(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
|
||||
override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber)
|
||||
|
||||
override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber)
|
||||
|
||||
data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int)
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.security.cert.X509CRL
|
||||
|
||||
object X509CRLSerializer : CustomSerializer.Implements<X509CRL>(X509CRL::class.java) {
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(
|
||||
type.toString(),
|
||||
"",
|
||||
listOf(type.toString()),
|
||||
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
|
||||
descriptor,
|
||||
emptyList()
|
||||
)))
|
||||
|
||||
override fun writeDescribedObject(obj: X509CRL, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
output.writeObject(obj.encoded, data, clazz, context)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): X509CRL {
|
||||
val bytes = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||
return X509CertificateFactory().delegate.generateCRL(bytes.inputStream()) as X509CRL
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.serialization.amqp.*
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Type
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object X509CertificateSerializer : CustomSerializer.Implements<X509Certificate>(X509Certificate::class.java) {
|
||||
override val schemaForDocumentation = Schema(listOf(RestrictedType(
|
||||
type.toString(),
|
||||
"",
|
||||
listOf(type.toString()),
|
||||
SerializerFactory.primitiveTypeName(ByteArray::class.java)!!,
|
||||
descriptor,
|
||||
emptyList()
|
||||
)))
|
||||
|
||||
override fun writeDescribedObject(obj: X509Certificate, data: Data, type: Type, output: SerializationOutput,
|
||||
context: SerializationContext
|
||||
) {
|
||||
output.writeObject(obj.encoded, data, clazz, context)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput,
|
||||
context: SerializationContext
|
||||
): X509Certificate {
|
||||
val bits = input.readObject(obj, schemas, ByteArray::class.java, context) as ByteArray
|
||||
return X509CertificateFactory().generateCertificate(bits.inputStream())
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.YearMonth
|
||||
|
||||
/**
|
||||
* A serializer for [YearMonth] that uses a proxy object to write out the integer form.
|
||||
*/
|
||||
class YearMonthSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<YearMonth, YearMonthSerializer.YearMonthProxy>(YearMonth::class.java, YearMonthProxy::class.java, factory) {
|
||||
override fun toProxy(obj: YearMonth): YearMonthProxy = YearMonthProxy(obj.year, obj.monthValue.toByte())
|
||||
|
||||
override fun fromProxy(proxy: YearMonthProxy): YearMonth = YearMonth.of(proxy.year, proxy.month.toInt())
|
||||
|
||||
data class YearMonthProxy(val year: Int, val month: Byte)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.Year
|
||||
|
||||
/**
|
||||
* A serializer for [Year] that uses a proxy object to write out the integer form.
|
||||
*/
|
||||
class YearSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Year, YearSerializer.YearProxy>(Year::class.java, YearProxy::class.java, factory) {
|
||||
override fun toProxy(obj: Year): YearProxy = YearProxy(obj.value)
|
||||
|
||||
override fun fromProxy(proxy: YearProxy): Year = Year.of(proxy.year)
|
||||
|
||||
data class YearProxy(val year: Int)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.time.ZoneId
|
||||
|
||||
/**
|
||||
* A serializer for [ZoneId] that uses a proxy object to write out the string form.
|
||||
*/
|
||||
class ZoneIdSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<ZoneId, ZoneIdSerializer.ZoneIdProxy>(ZoneId::class.java, ZoneIdProxy::class.java, factory) {
|
||||
override val revealSubclassesInSchema: Boolean = true
|
||||
|
||||
override fun toProxy(obj: ZoneId): ZoneIdProxy = ZoneIdProxy(obj.id)
|
||||
|
||||
override fun fromProxy(proxy: ZoneIdProxy): ZoneId = ZoneId.of(proxy.id)
|
||||
|
||||
data class ZoneIdProxy(val id: String)
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.lang.reflect.Method
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* A serializer for [ZonedDateTime] that uses a proxy object to write out the date, time, offset and zone.
|
||||
*/
|
||||
class ZonedDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<ZonedDateTime, ZonedDateTimeSerializer.ZonedDateTimeProxy>(ZonedDateTime::class.java, ZonedDateTimeProxy::class.java, factory) {
|
||||
// Java deserialization of `ZonedDateTime` uses a private method. We will resolve this somewhat statically
|
||||
// so that any change to internals of `ZonedDateTime` is detected early.
|
||||
companion object {
|
||||
val ofLenient: Method = ZonedDateTime::class.java.getDeclaredMethod("ofLenient", LocalDateTime::class.java, ZoneOffset::class.java, ZoneId::class.java)
|
||||
|
||||
init {
|
||||
ofLenient.isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(LocalDateTimeSerializer(factory), ZoneIdSerializer(factory))
|
||||
|
||||
override fun toProxy(obj: ZonedDateTime): ZonedDateTimeProxy = ZonedDateTimeProxy(obj.toLocalDateTime(), obj.offset, obj.zone)
|
||||
|
||||
override fun fromProxy(proxy: ZonedDateTimeProxy): ZonedDateTime = ofLenient.invoke(null, proxy.dateTime, proxy.offset, proxy.zone) as ZonedDateTime
|
||||
|
||||
data class ZonedDateTimeProxy(val dateTime: LocalDateTime, val offset: ZoneOffset, val zone: ZoneId)
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
@file:JvmName("AMQPSchemaExtensions")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CompositeType
|
||||
import net.corda.nodeapi.internal.serialization.amqp.RestrictedType
|
||||
import net.corda.nodeapi.internal.serialization.amqp.Field as AMQPField
|
||||
import net.corda.nodeapi.internal.serialization.amqp.Schema as AMQPSchema
|
||||
|
||||
fun AMQPSchema.carpenterSchema(classloader: ClassLoader): CarpenterMetaSchema {
|
||||
val rtn = CarpenterMetaSchema.newInstance()
|
||||
|
||||
types.filterIsInstance<CompositeType>().forEach {
|
||||
it.carpenterSchema(classloader, carpenterSchemas = rtn)
|
||||
}
|
||||
|
||||
return rtn
|
||||
}
|
||||
|
||||
/**
|
||||
* if we can load the class then we MUST know about all of it's composite elements
|
||||
*/
|
||||
private fun CompositeType.validatePropertyTypes(classloader: ClassLoader) {
|
||||
fields.forEach {
|
||||
if (!it.validateType(classloader)) throw UncarpentableException(name, it.name, it.type)
|
||||
}
|
||||
}
|
||||
|
||||
fun AMQPField.typeAsString() = if (type == "*") requires[0] else type
|
||||
|
||||
/**
|
||||
* based upon this AMQP schema either
|
||||
* a) add the corresponding carpenter schema to the [carpenterSchemas] param
|
||||
* b) add the class to the dependency tree in [carpenterSchemas] if it cannot be instantiated
|
||||
* at this time
|
||||
*
|
||||
* @param classloader the class loader provided by the [SerializationContext]
|
||||
* @param carpenterSchemas structure that holds the dependency tree and list of classes that
|
||||
* need constructing
|
||||
* @param force by default a schema is not added to [carpenterSchemas] if it already exists
|
||||
* on the class path. For testing purposes schema generation can be forced
|
||||
*/
|
||||
fun CompositeType.carpenterSchema(classloader: ClassLoader,
|
||||
carpenterSchemas: CarpenterMetaSchema,
|
||||
force: Boolean = false) {
|
||||
if (classloader.exists(name)) {
|
||||
validatePropertyTypes(classloader)
|
||||
if (!force) return
|
||||
}
|
||||
|
||||
val providesList = mutableListOf<Class<*>>()
|
||||
var isInterface = false
|
||||
var isCreatable = true
|
||||
|
||||
provides.forEach {
|
||||
if (name == it) {
|
||||
isInterface = true
|
||||
return@forEach
|
||||
}
|
||||
|
||||
try {
|
||||
providesList.add(classloader.loadClass(it))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
carpenterSchemas.addDepPair(this, name, it)
|
||||
isCreatable = false
|
||||
}
|
||||
}
|
||||
|
||||
val m: MutableMap<String, Field> = mutableMapOf()
|
||||
|
||||
fields.forEach {
|
||||
try {
|
||||
m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classloader))
|
||||
} catch (e: ClassNotFoundException) {
|
||||
carpenterSchemas.addDepPair(this, name, it.typeAsString())
|
||||
isCreatable = false
|
||||
}
|
||||
}
|
||||
|
||||
if (isCreatable) {
|
||||
carpenterSchemas.carpenterSchemas.add(CarpenterSchemaFactory.newInstance(
|
||||
name = name,
|
||||
fields = m,
|
||||
interfaces = providesList,
|
||||
isInterface = isInterface))
|
||||
}
|
||||
}
|
||||
|
||||
// This is potentially problematic as we're assuming the only type of restriction we will be
|
||||
// carpenting for, an enum, but actually trying to split out RestrictedType into something
|
||||
// more polymorphic is hard. Additionally, to conform to AMQP we're really serialising
|
||||
// this as a list so...
|
||||
fun RestrictedType.carpenterSchema(carpenterSchemas: CarpenterMetaSchema) {
|
||||
val m: MutableMap<String, Field> = mutableMapOf()
|
||||
|
||||
choices.forEach { m[it.name] = EnumField() }
|
||||
|
||||
carpenterSchemas.carpenterSchemas.add(EnumSchema(name = name, fields = m))
|
||||
}
|
||||
|
||||
// map a pair of (typename, mandatory) to the corresponding class type
|
||||
// where the mandatory AMQP flag maps to the types nullability
|
||||
val typeStrToType: Map<Pair<String, Boolean>, Class<out Any?>> = mapOf(
|
||||
Pair("int", true) to Int::class.javaPrimitiveType!!,
|
||||
Pair("int", false) to Integer::class.javaObjectType,
|
||||
Pair("short", true) to Short::class.javaPrimitiveType!!,
|
||||
Pair("short", false) to Short::class.javaObjectType,
|
||||
Pair("long", true) to Long::class.javaPrimitiveType!!,
|
||||
Pair("long", false) to Long::class.javaObjectType,
|
||||
Pair("char", true) to Char::class.javaPrimitiveType!!,
|
||||
Pair("char", false) to java.lang.Character::class.java,
|
||||
Pair("boolean", true) to Boolean::class.javaPrimitiveType!!,
|
||||
Pair("boolean", false) to Boolean::class.javaObjectType,
|
||||
Pair("double", true) to Double::class.javaPrimitiveType!!,
|
||||
Pair("double", false) to Double::class.javaObjectType,
|
||||
Pair("float", true) to Float::class.javaPrimitiveType!!,
|
||||
Pair("float", false) to Float::class.javaObjectType,
|
||||
Pair("byte", true) to Byte::class.javaPrimitiveType!!,
|
||||
Pair("byte", false) to Byte::class.javaObjectType
|
||||
)
|
||||
|
||||
fun String.stripGenerics(): String = if (this.endsWith('>')) {
|
||||
this.substring(0, this.indexOf('<'))
|
||||
} else this
|
||||
|
||||
fun AMQPField.getTypeAsClass(classloader: ClassLoader) = (typeStrToType[Pair(type, mandatory)] ?: when (type) {
|
||||
"string" -> String::class.java
|
||||
"binary" -> ByteArray::class.java
|
||||
"*" -> if (requires.isEmpty()) Any::class.java else {
|
||||
classloader.loadClass(requires[0].stripGenerics())
|
||||
}
|
||||
else -> classloader.loadClass(type.stripGenerics())
|
||||
})!!
|
||||
|
||||
fun AMQPField.validateType(classloader: ClassLoader) =
|
||||
when (type) {
|
||||
"byte", "int", "string", "short", "long", "char", "boolean", "double", "float" -> true
|
||||
"*" -> classloader.exists(requires[0])
|
||||
else -> classloader.exists(type)
|
||||
}
|
||||
|
||||
private fun ClassLoader.exists(clazz: String) = run {
|
||||
try {
|
||||
this.loadClass(clazz); true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -1,459 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import com.google.common.base.MoreObjects
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.Type
|
||||
import java.lang.Character.isJavaIdentifierPart
|
||||
import java.lang.Character.isJavaIdentifierStart
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Any object that implements this interface is expected to expose its own fields via the [get] method, exactly
|
||||
* as if `this.class.getMethod("get" + name.capitalize()).invoke(this)` had been called. It is intended as a more
|
||||
* convenient alternative to reflection.
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface SimpleFieldAccess {
|
||||
operator fun get(name: String): Any?
|
||||
}
|
||||
|
||||
class CarpenterClassLoader(parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) :
|
||||
ClassLoader(parentClassLoader) {
|
||||
fun load(name: String, bytes: ByteArray): Class<*> = defineClass(name, bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
class InterfaceMismatchNonGetterException(val clazz: Class<*>, val method: Method) : InterfaceMismatchException(
|
||||
"Requested interfaces must consist only of methods that start with 'get': ${clazz.name}.${method.name}")
|
||||
|
||||
class InterfaceMismatchMissingAMQPFieldException(val clazz: Class<*>, val field: String) : InterfaceMismatchException(
|
||||
"Interface ${clazz.name} requires a field named $field but that isn't found in the schema or any superclass schemas")
|
||||
|
||||
/**
|
||||
* Which version of the java runtime are we constructing objects against
|
||||
*/
|
||||
private const val TARGET_VERSION = V1_8
|
||||
|
||||
private val jlEnum: String = Type.getInternalName(Enum::class.java)
|
||||
private val jlString: String = Type.getInternalName(String::class.java)
|
||||
private val jlObject: String = Type.getInternalName(Object::class.java)
|
||||
private val jlClass: String = Type.getInternalName(Class::class.java)
|
||||
private val moreObjects: String = Type.getInternalName(MoreObjects::class.java)
|
||||
private val toStringHelper: String = Type.getInternalName(MoreObjects.ToStringHelper::class.java)
|
||||
|
||||
// Allow us to create alternative ClassCarpenters.
|
||||
interface ClassCarpenter {
|
||||
val whitelist: ClassWhitelist
|
||||
val classloader: ClassLoader
|
||||
fun build(schema: Schema): Class<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* A class carpenter generates JVM bytecodes for a class given a schema and then loads it into a sub-classloader.
|
||||
* The generated classes have getters, a toString method and implement a simple property access interface. The
|
||||
* resulting class can then be accessed via reflection APIs, or cast to one of the requested interfaces.
|
||||
*
|
||||
* Additional interfaces may be requested if they consist purely of get methods and the schema matches.
|
||||
*
|
||||
* # Discussion
|
||||
*
|
||||
* This class may initially appear pointless: why create a class at runtime that simply holds data and which
|
||||
* you cannot compile against? The purpose is to enable the synthesis of data classes based on (AMQP) schemas
|
||||
* when the app that originally defined them is not available on the classpath. Whilst the getters and setters
|
||||
* are not usable directly, many existing reflection based frameworks like JSON/XML processors, Swing property
|
||||
* editor sheets, Groovy and so on can work with the JavaBean ("POJO") format. Feeding these objects to such
|
||||
* frameworks can often be useful. The generic property access interface is helpful if you want to write code
|
||||
* that accesses these schemas but don't want to actually define/depend on the classes themselves.
|
||||
*
|
||||
* # Usage notes
|
||||
*
|
||||
* This class is not thread safe.
|
||||
*
|
||||
* The generated class has private final fields and getters for each field. The constructor has one parameter
|
||||
* for each field. In this sense it is like a Kotlin data class.
|
||||
*
|
||||
* The generated class implements [SimpleFieldAccess]. The get method takes the name of the field, not the name
|
||||
* of a getter i.e. use .get("someVar") not .get("getSomeVar") or in Kotlin you can use square brackets syntax.
|
||||
*
|
||||
* The generated class implements toString() using Google Guava to simplify formatting. Make sure it's on the
|
||||
* classpath of the generated classes.
|
||||
*
|
||||
* Generated classes can refer to each other as long as they're defined in the right order. They can also
|
||||
* inherit from each other. When inheritance is used the constructor requires parameters in order of superclasses
|
||||
* first, child class last.
|
||||
*
|
||||
* You cannot create boxed primitive fields with this class: fields are always of primitive type.
|
||||
*
|
||||
* Nullability information is not emitted.
|
||||
*
|
||||
* Each [ClassCarpenter] defines its own classloader and thus, its own namespace. If you create multiple
|
||||
* carpenters, you can load the same schema with the same name and get two different classes, whose objects
|
||||
* will not be interoperable.
|
||||
*
|
||||
* Equals/hashCode methods are not yet supported.
|
||||
*/
|
||||
class ClassCarpenterImpl(cl: ClassLoader, override val whitelist: ClassWhitelist) : ClassCarpenter {
|
||||
constructor(whitelist: ClassWhitelist) : this(Thread.currentThread().contextClassLoader, whitelist)
|
||||
|
||||
// TODO: Generics.
|
||||
// TODO: Sandbox the generated code when a security manager is in use.
|
||||
// TODO: Generate equals/hashCode.
|
||||
// TODO: Support annotations.
|
||||
// TODO: isFoo getter patterns for booleans (this is what Kotlin generates)
|
||||
|
||||
override val classloader = CarpenterClassLoader(cl)
|
||||
|
||||
private val _loaded = HashMap<String, Class<*>>()
|
||||
|
||||
/** Returns a snapshot of the currently loaded classes as a map of full class name (package names+dots) -> class object */
|
||||
val loaded: Map<String, Class<*>> = HashMap(_loaded)
|
||||
|
||||
/**
|
||||
* Generate bytecode for the given schema and load into the JVM. The returned class object can be used to
|
||||
* construct instances of the generated class.
|
||||
*
|
||||
* @throws DuplicateNameException if the schema's name is already taken in this namespace (you can create a
|
||||
* new ClassCarpenter if you're OK with ambiguous names)
|
||||
*/
|
||||
override fun build(schema: Schema): Class<*> {
|
||||
validateSchema(schema)
|
||||
// Walk up the inheritance hierarchy and then start walking back down once we either hit the top, or
|
||||
// find a class we haven't generated yet.
|
||||
val hierarchy = ArrayList<Schema>()
|
||||
hierarchy += schema
|
||||
var cursor = schema.superclass
|
||||
while (cursor != null && cursor.name !in _loaded) {
|
||||
hierarchy += cursor
|
||||
cursor = cursor.superclass
|
||||
}
|
||||
|
||||
hierarchy.reversed().forEach {
|
||||
when (it) {
|
||||
is InterfaceSchema -> generateInterface(it)
|
||||
is ClassSchema -> generateClass(it)
|
||||
is EnumSchema -> generateEnum(it)
|
||||
}
|
||||
}
|
||||
|
||||
assert(schema.name in _loaded)
|
||||
|
||||
return _loaded[schema.name]!!
|
||||
}
|
||||
|
||||
private fun generateEnum(enumSchema: Schema): Class<*> {
|
||||
return generate(enumSchema) { cw, schema ->
|
||||
cw.apply {
|
||||
visit(TARGET_VERSION, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, schema.jvmName,
|
||||
"L$jlEnum<L${schema.jvmName};>;", jlEnum, null)
|
||||
|
||||
if (schema.flags.cordaSerializable()) {
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
}
|
||||
|
||||
generateFields(schema)
|
||||
generateStaticEnumConstructor(schema)
|
||||
generateEnumConstructor()
|
||||
generateEnumValues(schema)
|
||||
generateEnumValueOf(schema)
|
||||
}.visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateInterface(interfaceSchema: Schema): Class<*> {
|
||||
return generate(interfaceSchema) { cw, schema ->
|
||||
val interfaces = schema.interfaces.map { Type.getInternalName(it) }.toTypedArray()
|
||||
|
||||
cw.apply {
|
||||
visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null,
|
||||
jlObject, interfaces)
|
||||
|
||||
if (schema.flags.cordaSerializable()) {
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
}
|
||||
generateAbstractGetters(schema)
|
||||
}.visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateClass(classSchema: Schema): Class<*> {
|
||||
return generate(classSchema) { cw, schema ->
|
||||
val superName = schema.superclass?.jvmName ?: jlObject
|
||||
val interfaces = schema.interfaces.map { Type.getInternalName(it) }.toMutableList()
|
||||
|
||||
if (SimpleFieldAccess::class.java !in schema.interfaces
|
||||
&& schema.flags.cordaSerializable()
|
||||
&& schema.flags.simpleFieldAccess()) {
|
||||
interfaces.add(Type.getInternalName(SimpleFieldAccess::class.java))
|
||||
}
|
||||
|
||||
cw.apply {
|
||||
visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName,
|
||||
interfaces.toTypedArray())
|
||||
|
||||
if (schema.flags.cordaSerializable()) {
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
}
|
||||
generateFields(schema)
|
||||
generateClassConstructor(schema)
|
||||
generateGetters(schema)
|
||||
if (schema.superclass == null) {
|
||||
generateGetMethod() // From SimplePropertyAccess
|
||||
}
|
||||
generateToString(schema)
|
||||
}.visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun generate(schema: Schema, generator: (ClassWriter, Schema) -> Unit): Class<*> {
|
||||
// Lazy: we could compute max locals/max stack ourselves, it'd be faster.
|
||||
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS)
|
||||
|
||||
generator(cw, schema)
|
||||
|
||||
val clazz = classloader.load(schema.name, cw.toByteArray())
|
||||
_loaded[schema.name] = clazz
|
||||
return clazz
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateFields(schema: Schema) {
|
||||
schema.generateFields(this)
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateToString(schema: Schema) {
|
||||
with(visitMethod(ACC_PUBLIC, "toString", "()L$jlString;", null, null)) {
|
||||
visitCode()
|
||||
// com.google.common.base.MoreObjects.toStringHelper("TypeName")
|
||||
visitLdcInsn(schema.name.split('.').last())
|
||||
visitMethodInsn(INVOKESTATIC, moreObjects, "toStringHelper",
|
||||
"(L$jlString;)L$toStringHelper;", false)
|
||||
// Call the add() methods.
|
||||
for ((name, field) in schema.fieldsIncludingSuperclasses().entries) {
|
||||
visitLdcInsn(name)
|
||||
visitVarInsn(ALOAD, 0) // this
|
||||
visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name])
|
||||
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(L$jlString;${field.type})L$toStringHelper;", false)
|
||||
}
|
||||
// call toString() on the builder and return.
|
||||
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()L$jlString;", false)
|
||||
visitInsn(ARETURN)
|
||||
visitMaxs(0, 0)
|
||||
visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateGetMethod() {
|
||||
val ourJvmName = Type.getInternalName(ClassCarpenterImpl::class.java)
|
||||
with(visitMethod(ACC_PUBLIC, "get", "(L$jlString;)L$jlObject;", null, null)) {
|
||||
visitCode()
|
||||
visitVarInsn(ALOAD, 0) // Load 'this'
|
||||
visitVarInsn(ALOAD, 1) // Load the name argument
|
||||
// Using this generic helper method is slow, as it relies on reflection. A faster way would be
|
||||
// to use a tableswitch opcode, or just push back on the user and ask them to use actual reflection
|
||||
// or MethodHandles (super fast reflection) to access the object instead.
|
||||
visitMethodInsn(INVOKESTATIC, ourJvmName, "getField", "(L$jlObject;L$jlString;)L$jlObject;", false)
|
||||
visitInsn(ARETURN)
|
||||
visitMaxs(0, 0)
|
||||
visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateGetters(schema: Schema) {
|
||||
for ((name, type) in schema.fields) {
|
||||
visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null).apply {
|
||||
(type as ClassField).addNullabilityAnnotation(this)
|
||||
visitCode()
|
||||
visitVarInsn(ALOAD, 0) // Load 'this'
|
||||
visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor)
|
||||
when (type.field) {
|
||||
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
|
||||
java.lang.Character.TYPE -> visitInsn(IRETURN)
|
||||
java.lang.Long.TYPE -> visitInsn(LRETURN)
|
||||
java.lang.Double.TYPE -> visitInsn(DRETURN)
|
||||
java.lang.Float.TYPE -> visitInsn(FRETURN)
|
||||
else -> visitInsn(ARETURN)
|
||||
}
|
||||
visitMaxs(0, 0)
|
||||
}.visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateAbstractGetters(schema: Schema) {
|
||||
for ((name, field) in schema.fields) {
|
||||
val opcodes = ACC_ABSTRACT + ACC_PUBLIC
|
||||
// abstract method doesn't have any implementation so just end
|
||||
visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null).visitEnd()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateStaticEnumConstructor(schema: Schema) {
|
||||
visitMethod(ACC_STATIC, "<clinit>", "()V", null, null).apply {
|
||||
visitCode()
|
||||
visitIntInsn(BIPUSH, schema.fields.size)
|
||||
visitTypeInsn(ANEWARRAY, schema.jvmName)
|
||||
visitInsn(DUP)
|
||||
|
||||
var idx = 0
|
||||
schema.fields.forEach {
|
||||
visitInsn(DUP)
|
||||
visitIntInsn(BIPUSH, idx)
|
||||
visitTypeInsn(NEW, schema.jvmName)
|
||||
visitInsn(DUP)
|
||||
visitLdcInsn(it.key)
|
||||
visitIntInsn(BIPUSH, idx++)
|
||||
visitMethodInsn(INVOKESPECIAL, schema.jvmName, "<init>", "(L$jlString;I)V", false)
|
||||
visitInsn(DUP)
|
||||
visitFieldInsn(PUTSTATIC, schema.jvmName, it.key, "L${schema.jvmName};")
|
||||
visitInsn(AASTORE)
|
||||
}
|
||||
|
||||
visitFieldInsn(PUTSTATIC, schema.jvmName, "\$VALUES", schema.asArray)
|
||||
visitInsn(RETURN)
|
||||
|
||||
visitMaxs(0, 0)
|
||||
}.visitEnd()
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateEnumValues(schema: Schema) {
|
||||
visitMethod(ACC_PUBLIC + ACC_STATIC, "values", "()${schema.asArray}", null, null).apply {
|
||||
visitCode()
|
||||
visitFieldInsn(GETSTATIC, schema.jvmName, "\$VALUES", schema.asArray)
|
||||
visitMethodInsn(INVOKEVIRTUAL, schema.asArray, "clone", "()L$jlObject;", false)
|
||||
visitTypeInsn(CHECKCAST, schema.asArray)
|
||||
visitInsn(ARETURN)
|
||||
visitMaxs(0, 0)
|
||||
}.visitEnd()
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateEnumValueOf(schema: Schema) {
|
||||
visitMethod(ACC_PUBLIC + ACC_STATIC, "valueOf", "(L$jlString;)L${schema.jvmName};", null, null).apply {
|
||||
visitCode()
|
||||
visitLdcInsn(Type.getType("L${schema.jvmName};"))
|
||||
visitVarInsn(ALOAD, 0)
|
||||
visitMethodInsn(INVOKESTATIC, jlEnum, "valueOf", "(L$jlClass;L$jlString;)L$jlEnum;", true)
|
||||
visitTypeInsn(CHECKCAST, schema.jvmName)
|
||||
visitInsn(ARETURN)
|
||||
visitMaxs(0, 0)
|
||||
}.visitEnd()
|
||||
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateEnumConstructor() {
|
||||
visitMethod(ACC_PROTECTED, "<init>", "(L$jlString;I)V", "()V", null).apply {
|
||||
visitParameter("\$enum\$name", ACC_SYNTHETIC)
|
||||
visitParameter("\$enum\$ordinal", ACC_SYNTHETIC)
|
||||
|
||||
visitCode()
|
||||
|
||||
visitVarInsn(ALOAD, 0) // this
|
||||
visitVarInsn(ALOAD, 1)
|
||||
visitVarInsn(ILOAD, 2)
|
||||
visitMethodInsn(INVOKESPECIAL, jlEnum, "<init>", "(L$jlString;I)V", false)
|
||||
visitInsn(RETURN)
|
||||
|
||||
visitMaxs(0, 0)
|
||||
}.visitEnd()
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateClassConstructor(schema: Schema) {
|
||||
visitMethod(
|
||||
ACC_PUBLIC,
|
||||
"<init>",
|
||||
"(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V",
|
||||
null,
|
||||
null).apply {
|
||||
var idx = 0
|
||||
|
||||
schema.fields.values.forEach { it.visitParameter(this, idx++) }
|
||||
|
||||
visitCode()
|
||||
|
||||
// Calculate the super call.
|
||||
val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap()
|
||||
visitVarInsn(ALOAD, 0)
|
||||
val sc = schema.superclass
|
||||
if (sc == null) {
|
||||
visitMethodInsn(INVOKESPECIAL, jlObject, "<init>", "()V", false)
|
||||
} else {
|
||||
var slot = 1
|
||||
superclassFields.values.forEach { slot += load(slot, it) }
|
||||
val superDesc = sc.descriptorsIncludingSuperclasses().values.joinToString("")
|
||||
visitMethodInsn(INVOKESPECIAL, sc.jvmName, "<init>", "($superDesc)V", false)
|
||||
}
|
||||
|
||||
// Assign the fields from parameters.
|
||||
var slot = 1 + superclassFields.size
|
||||
for ((name, field) in schema.fields) {
|
||||
(field as ClassField).nullTest(this, slot)
|
||||
|
||||
visitVarInsn(ALOAD, 0) // Load 'this' onto the stack
|
||||
slot += load(slot, field) // Load the contents of the parameter onto the stack.
|
||||
visitFieldInsn(PUTFIELD, schema.jvmName, name, field.descriptor)
|
||||
}
|
||||
visitInsn(RETURN)
|
||||
visitMaxs(0, 0)
|
||||
}.visitEnd()
|
||||
}
|
||||
|
||||
private fun MethodVisitor.load(slot: Int, type: Field): Int {
|
||||
when (type.field) {
|
||||
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
|
||||
java.lang.Character.TYPE -> visitVarInsn(ILOAD, slot)
|
||||
java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot)
|
||||
java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot)
|
||||
java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot)
|
||||
else -> visitVarInsn(ALOAD, slot)
|
||||
}
|
||||
return when (type.field) {
|
||||
java.lang.Long.TYPE, java.lang.Double.TYPE -> 2
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a sub element isn't whitelist we will not build a class containing that type as a member. Since, by
|
||||
* default, classes created by the [ClassCarpenter] are annotated as [CordaSerializable] we will always
|
||||
* be able to carpent classes generated from our AMQP library as, at a base level, we will either be able to
|
||||
* create the lowest level in the meta hierarchy because either all members are jvm primitives or
|
||||
* whitelisted classes
|
||||
*/
|
||||
private fun validateSchema(schema: Schema) {
|
||||
if (schema.name in _loaded) throw DuplicateNameException(schema.name)
|
||||
fun isJavaName(n: String) = n.isNotBlank() && isJavaIdentifierStart(n.first()) && n.all(::isJavaIdentifierPart)
|
||||
require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" }
|
||||
schema.fields.forEach {
|
||||
require(isJavaName(it.key)) { "Not a valid Java name: $it" }
|
||||
}
|
||||
|
||||
// Now check each interface we've been asked to implement, as the JVM will unfortunately only catch the
|
||||
// fact that we didn't implement the interface we said we would at the moment the missing method is
|
||||
// actually called, which is a bit too dynamic for my tastes.
|
||||
val allFields = schema.fieldsIncludingSuperclasses()
|
||||
for (itf in schema.interfaces) {
|
||||
itf.methods.forEach {
|
||||
val fieldNameFromItf = when {
|
||||
it.name.startsWith("get") -> it.name.substring(3).decapitalize()
|
||||
else -> throw InterfaceMismatchNonGetterException(itf, it)
|
||||
}
|
||||
|
||||
// If we're trying to carpent a class that prior to serialisation / deserialization
|
||||
// was made by a carpenter then we can ignore this (it will implement a plain get
|
||||
// method from SimpleFieldAccess).
|
||||
if (fieldNameFromItf.isEmpty() && SimpleFieldAccess::class.java in schema.interfaces) return@forEach
|
||||
|
||||
if ((schema is ClassSchema) and (fieldNameFromItf !in allFields)) {
|
||||
throw InterfaceMismatchMissingAMQPFieldException(itf, fieldNameFromItf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Suppress("UNUSED")
|
||||
fun getField(obj: Any, name: String): Any? = obj.javaClass.getMethod("get" + name.capitalize()).invoke(obj)
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
/**
|
||||
* The general exception type thrown by the [ClassCarpenter]
|
||||
*/
|
||||
abstract class ClassCarpenterException(msg: String) : CordaRuntimeException(msg)
|
||||
|
||||
/**
|
||||
* Thrown by the [ClassCarpenter] when trying to build
|
||||
*/
|
||||
abstract class InterfaceMismatchException(msg: String) : ClassCarpenterException(msg)
|
||||
|
||||
class DuplicateNameException(val name: String) : ClassCarpenterException(
|
||||
"An attempt was made to register two classes with the name '$name' within the same ClassCarpenter namespace.")
|
||||
|
||||
class NullablePrimitiveException(val name: String, val field: Class<out Any>) : ClassCarpenterException(
|
||||
"Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable")
|
||||
|
||||
class UncarpentableException(name: String, field: String, type: String) :
|
||||
ClassCarpenterException("Class $name is loadable yet contains field $field of unknown type $type")
|
||||
|
||||
/**
|
||||
* A meta exception used by the [MetaCarpenter] to wrap any exceptions generated during the build
|
||||
* process and associate those with the current schema being processed. This makes for cleaner external
|
||||
* error hand
|
||||
*
|
||||
* @property name The name of the schema, and thus the class being created, when the error was occured
|
||||
* @property e The [ClassCarpenterException] this is wrapping
|
||||
*/
|
||||
class MetaCarpenterException(val name: String, val e: ClassCarpenterException) : CordaRuntimeException(
|
||||
"Whilst processing class '$name' - ${e.message}")
|
@ -1,113 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.CompositeType
|
||||
import net.corda.nodeapi.internal.serialization.amqp.RestrictedType
|
||||
import net.corda.nodeapi.internal.serialization.amqp.TypeNotation
|
||||
|
||||
/**
|
||||
* Generated from an AMQP schema this class represents the classes unknown to the deserializer and that thusly
|
||||
* require carpenting up in bytecode form. This is a multi step process as carpenting one object may be dependent
|
||||
* upon the creation of others, this information is tracked in the dependency tree represented by
|
||||
* [dependencies] and [dependsOn]. Creatable classes are stored in [carpenterSchemas].
|
||||
*
|
||||
* The state of this class after initial generation is expected to mutate as classes are built by the carpenter
|
||||
* enabling the resolution of dependencies and thus new carpenter schemas added whilst those already
|
||||
* carpented schemas are removed.
|
||||
*
|
||||
* @property carpenterSchemas The list of carpentable classes
|
||||
* @property dependencies Maps a class to a list of classes that depend on it being built first
|
||||
* @property dependsOn Maps a class to a list of classes it depends on being built before it
|
||||
*
|
||||
* Once a class is constructed we can quickly check for resolution by first looking at all of its dependents in the
|
||||
* [dependencies] map. This will give us a list of classes that depended on that class being carpented. We can then
|
||||
* in turn look up all of those classes in the [dependsOn] list, remove their dependency on the newly created class,
|
||||
* and if that list is reduced to zero know we can now generate a [Schema] for them and carpent them up
|
||||
*/
|
||||
data class CarpenterMetaSchema(
|
||||
val carpenterSchemas: MutableList<Schema>,
|
||||
val dependencies: MutableMap<String, Pair<TypeNotation, MutableList<String>>>,
|
||||
val dependsOn: MutableMap<String, MutableList<String>>) {
|
||||
companion object CarpenterSchemaConstructor {
|
||||
fun newInstance(): CarpenterMetaSchema {
|
||||
return CarpenterMetaSchema(mutableListOf(), mutableMapOf(), mutableMapOf())
|
||||
}
|
||||
}
|
||||
|
||||
fun addDepPair(type: TypeNotation, dependant: String, dependee: String) {
|
||||
dependsOn.computeIfAbsent(dependee, { mutableListOf() }).add(dependant)
|
||||
dependencies.computeIfAbsent(dependant, { Pair(type, mutableListOf()) }).second.add(dependee)
|
||||
}
|
||||
|
||||
val size
|
||||
get() = carpenterSchemas.size
|
||||
|
||||
fun isEmpty() = carpenterSchemas.isEmpty()
|
||||
fun isNotEmpty() = carpenterSchemas.isNotEmpty()
|
||||
|
||||
// We could make this an abstract method on TypeNotation but that
|
||||
// would mean the amqp package being "more" infected with carpenter
|
||||
// specific bits.
|
||||
fun buildFor(target: TypeNotation, cl: ClassLoader) = when (target) {
|
||||
is RestrictedType -> target.carpenterSchema(this)
|
||||
is CompositeType -> target.carpenterSchema(cl, this, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a dependency tree of [CarpenterMetaSchema] and reduce it to zero by carpenting those classes that
|
||||
* require it. As classes are carpented check for dependency resolution, if now free generate a [Schema] for
|
||||
* that class and add it to the list of classes ([CarpenterMetaSchema.carpenterSchemas]) that require
|
||||
* carpenting
|
||||
*
|
||||
* @property cc a reference to the actual class carpenter we're using to constuct classes
|
||||
* @property objects a list of carpented classes loaded into the carpenters class loader
|
||||
*/
|
||||
abstract class MetaCarpenterBase(val schemas: CarpenterMetaSchema, val cc: ClassCarpenter) {
|
||||
val objects = mutableMapOf<String, Class<*>>()
|
||||
|
||||
fun step(newObject: Schema) {
|
||||
objects[newObject.name] = cc.build(newObject)
|
||||
|
||||
// go over the list of everything that had a dependency on the newly
|
||||
// carpented class existing and remove it from their dependency list, If that
|
||||
// list is now empty we have no impediment to carpenting that class up
|
||||
schemas.dependsOn.remove(newObject.name)?.forEach { dependent ->
|
||||
assert(newObject.name in schemas.dependencies[dependent]!!.second)
|
||||
|
||||
schemas.dependencies[dependent]?.second?.remove(newObject.name)
|
||||
|
||||
// we're out of blockers so we can now create the type
|
||||
if (schemas.dependencies[dependent]?.second?.isEmpty() == true) {
|
||||
(schemas.dependencies.remove(dependent)?.first as CompositeType).carpenterSchema(
|
||||
classloader = cc.classloader,
|
||||
carpenterSchemas = schemas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun build()
|
||||
|
||||
val classloader: ClassLoader
|
||||
get() = cc.classloader
|
||||
}
|
||||
|
||||
class MetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) {
|
||||
override fun build() {
|
||||
while (schemas.carpenterSchemas.isNotEmpty()) {
|
||||
val newObject = schemas.carpenterSchemas.removeAt(0)
|
||||
try {
|
||||
step(newObject)
|
||||
} catch (e: ClassCarpenterException) {
|
||||
throw MetaCarpenterException(newObject.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestMetaCarpenter(schemas: CarpenterMetaSchema, cc: ClassCarpenter) : MetaCarpenterBase(schemas, cc) {
|
||||
override fun build() {
|
||||
if (schemas.carpenterSchemas.isEmpty()) return
|
||||
step(schemas.carpenterSchemas.removeAt(0))
|
||||
}
|
||||
}
|
||||
|
@ -1,128 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
import java.util.*
|
||||
|
||||
enum class SchemaFlags {
|
||||
SimpleFieldAccess, CordaSerializable
|
||||
}
|
||||
|
||||
/**
|
||||
* A Schema is the representation of an object the Carpenter can construct
|
||||
*
|
||||
* Known Sub Classes
|
||||
* - [ClassSchema]
|
||||
* - [InterfaceSchema]
|
||||
* - [EnumSchema]
|
||||
*/
|
||||
abstract class Schema(
|
||||
val name: String,
|
||||
var fields: Map<String, Field>,
|
||||
val superclass: Schema? = null,
|
||||
val interfaces: List<Class<*>> = emptyList(),
|
||||
updater: (String, Field) -> Unit) {
|
||||
private fun Map<String, Field>.descriptors() = LinkedHashMap(this.mapValues { it.value.descriptor })
|
||||
|
||||
var flags: EnumMap<SchemaFlags, Boolean> = EnumMap(SchemaFlags::class.java)
|
||||
|
||||
init {
|
||||
fields.forEach { updater(it.key, it.value) }
|
||||
|
||||
// Fix the order up front if the user didn't, inject the name into the field as it's
|
||||
// neater when iterating
|
||||
fields = LinkedHashMap(fields)
|
||||
}
|
||||
|
||||
fun fieldsIncludingSuperclasses(): Map<String, Field> =
|
||||
(superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields)
|
||||
|
||||
fun descriptorsIncludingSuperclasses(): Map<String, String?> =
|
||||
(superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors()
|
||||
|
||||
abstract fun generateFields(cw: ClassWriter)
|
||||
|
||||
val jvmName: String
|
||||
get() = name.replace(".", "/")
|
||||
|
||||
val asArray: String
|
||||
get() = "[L$jvmName;"
|
||||
|
||||
fun unsetCordaSerializable() {
|
||||
flags.replace(SchemaFlags.CordaSerializable, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun EnumMap<SchemaFlags, Boolean>.cordaSerializable(): Boolean {
|
||||
return this.getOrDefault(SchemaFlags.CordaSerializable, true) == true
|
||||
}
|
||||
|
||||
fun EnumMap<SchemaFlags, Boolean>.simpleFieldAccess(): Boolean {
|
||||
return this.getOrDefault(SchemaFlags.SimpleFieldAccess, true) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a concrete object.
|
||||
*/
|
||||
class ClassSchema(
|
||||
name: String,
|
||||
fields: Map<String, Field>,
|
||||
superclass: Schema? = null,
|
||||
interfaces: List<Class<*>> = emptyList()
|
||||
) : Schema(name, fields, superclass, interfaces, { newName, field -> field.name = newName }) {
|
||||
override fun generateFields(cw: ClassWriter) {
|
||||
cw.apply { fields.forEach { it.value.generateField(this) } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an interface. Carpented interfaces can be used within [ClassSchema]s
|
||||
* if that class should be implementing that interface.
|
||||
*/
|
||||
class InterfaceSchema(
|
||||
name: String,
|
||||
fields: Map<String, Field>,
|
||||
superclass: Schema? = null,
|
||||
interfaces: List<Class<*>> = emptyList()
|
||||
) : Schema(name, fields, superclass, interfaces, { newName, field -> field.name = newName }) {
|
||||
override fun generateFields(cw: ClassWriter) {
|
||||
cw.apply { fields.forEach { it.value.generateField(this) } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an enumerated type.
|
||||
*/
|
||||
class EnumSchema(
|
||||
name: String,
|
||||
fields: Map<String, Field>
|
||||
) : Schema(name, fields, null, emptyList(), { fieldName, field ->
|
||||
(field as EnumField).name = fieldName
|
||||
field.descriptor = "L${name.replace(".", "/")};"
|
||||
}) {
|
||||
override fun generateFields(cw: ClassWriter) {
|
||||
with(cw) {
|
||||
fields.forEach { it.value.generateField(this) }
|
||||
|
||||
visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC + ACC_SYNTHETIC,
|
||||
"\$VALUES", asArray, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory object used by the serializer when building [Schema]s based
|
||||
* on an AMQP schema.
|
||||
*/
|
||||
object CarpenterSchemaFactory {
|
||||
fun newInstance(
|
||||
name: String,
|
||||
fields: Map<String, Field>,
|
||||
superclass: Schema? = null,
|
||||
interfaces: List<Class<*>> = emptyList(),
|
||||
isInterface: Boolean = false
|
||||
): Schema =
|
||||
if (isInterface) InterfaceSchema(name, fields, superclass, interfaces)
|
||||
else ClassSchema(name, fields, superclass, interfaces)
|
||||
}
|
||||
|
@ -1,136 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import jdk.internal.org.objectweb.asm.Opcodes.*
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
abstract class Field(val field: Class<out Any?>) {
|
||||
abstract var descriptor: String?
|
||||
|
||||
companion object {
|
||||
const val unsetName = "Unset"
|
||||
}
|
||||
|
||||
var name: String = unsetName
|
||||
abstract val type: String
|
||||
|
||||
abstract fun generateField(cw: ClassWriter)
|
||||
abstract fun visitParameter(mv: MethodVisitor, idx: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Any field that can be a member of an object
|
||||
*
|
||||
* Known
|
||||
* - [NullableField]
|
||||
* - [NonNullableField]
|
||||
*/
|
||||
abstract class ClassField(field: Class<out Any?>) : Field(field) {
|
||||
abstract val nullabilityAnnotation: String
|
||||
abstract fun nullTest(mv: MethodVisitor, slot: Int)
|
||||
|
||||
override var descriptor: String? = Type.getDescriptor(this.field)
|
||||
override val type: String get() = if (this.field.isPrimitive) this.descriptor!! else "Ljava/lang/Object;"
|
||||
|
||||
fun addNullabilityAnnotation(mv: MethodVisitor) {
|
||||
mv.visitAnnotation(nullabilityAnnotation, true).visitEnd()
|
||||
}
|
||||
|
||||
override fun generateField(cw: ClassWriter) {
|
||||
cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null).visitAnnotation(
|
||||
nullabilityAnnotation, true).visitEnd()
|
||||
}
|
||||
|
||||
override fun visitParameter(mv: MethodVisitor, idx: Int) {
|
||||
with(mv) {
|
||||
visitParameter(name, 0)
|
||||
if (!field.isPrimitive) {
|
||||
visitParameterAnnotation(idx, nullabilityAnnotation, true).visitEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A member of a constructed class that can be assigned to null, the
|
||||
* mandatory type for primitives, but also any member that cannot be
|
||||
* null
|
||||
*
|
||||
* maps to AMQP mandatory = true fields
|
||||
*/
|
||||
open class NonNullableField(field: Class<out Any?>) : ClassField(field) {
|
||||
override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;"
|
||||
|
||||
constructor(name: String, field: Class<out Any?>) : this(field) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
||||
assert(name != unsetName)
|
||||
|
||||
if (!field.isPrimitive) {
|
||||
with(mv) {
|
||||
visitVarInsn(ALOAD, 0) // load this
|
||||
visitVarInsn(ALOAD, slot) // load parameter
|
||||
visitLdcInsn("param \"$name\" cannot be null")
|
||||
visitMethodInsn(INVOKESTATIC,
|
||||
"java/util/Objects",
|
||||
"requireNonNull",
|
||||
"(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", false)
|
||||
visitInsn(POP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A member of a constructed class that can be assigned to null,
|
||||
*
|
||||
* maps to AMQP mandatory = false fields
|
||||
*/
|
||||
class NullableField(field: Class<out Any?>) : ClassField(field) {
|
||||
override val nullabilityAnnotation = "Ljavax/annotation/Nullable;"
|
||||
|
||||
constructor(name: String, field: Class<out Any?>) : this(field) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
init {
|
||||
if (field.isPrimitive) {
|
||||
throw NullablePrimitiveException(name, field)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nullTest(mv: MethodVisitor, slot: Int) {
|
||||
assert(name != unsetName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents enum constants within an enum
|
||||
*/
|
||||
class EnumField : Field(Enum::class.java) {
|
||||
override var descriptor: String? = null
|
||||
|
||||
override val type: String
|
||||
get() = "Ljava/lang/Enum;"
|
||||
|
||||
override fun generateField(cw: ClassWriter) {
|
||||
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC + ACC_ENUM, name,
|
||||
descriptor, null, null).visitEnd()
|
||||
}
|
||||
|
||||
override fun visitParameter(mv: MethodVisitor, idx: Int) {
|
||||
mv.visitParameter(name, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Field Schema object of the correct type depending weather
|
||||
* the AMQP schema indicates it's mandatory (non nullable) or not (nullable)
|
||||
*/
|
||||
object FieldFactory {
|
||||
fun newInstance(mandatory: Boolean, name: String, field: Class<out Any?>) =
|
||||
if (mandatory) NonNullableField(name, field) else NullableField(name, field)
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.ClosureSerializer
|
||||
import java.io.Serializable
|
||||
|
||||
object CordaClosureSerializer : ClosureSerializer() {
|
||||
const val ERROR_MESSAGE = "Unable to serialize Java Lambda expression, unless explicitly declared e.g., Runnable r = (Runnable & Serializable) () -> System.out.println(\"Hello world!\");"
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, target: Any) {
|
||||
if (!isSerializable(target)) {
|
||||
throw IllegalArgumentException(ERROR_MESSAGE)
|
||||
}
|
||||
super.write(kryo, output, target)
|
||||
}
|
||||
|
||||
private fun isSerializable(target: Any): Boolean {
|
||||
return target is Serializable
|
||||
}
|
||||
}
|
||||
|
||||
object CordaClosureBlacklistSerializer : ClosureSerializer() {
|
||||
const val ERROR_MESSAGE = "Java 8 Lambda expressions are not supported for serialization."
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, target: Any) {
|
||||
throw IllegalArgumentException(ERROR_MESSAGE)
|
||||
}
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.ClosureSerializer
|
||||
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
|
||||
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||
import de.javakaffee.kryoserializers.BitSetSerializer
|
||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
||||
import de.javakaffee.kryoserializers.guava.*
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.AbstractAttachment
|
||||
import net.corda.core.internal.readFully
|
||||
import net.corda.core.serialization.MissingAttachmentsException
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.GeneratedAttachment
|
||||
import net.corda.nodeapi.internal.serialization.MutableClassWhitelist
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||
import org.objenesis.instantiator.ObjectInstantiator
|
||||
import org.objenesis.strategy.InstantiatorStrategy
|
||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||
import org.slf4j.Logger
|
||||
import sun.security.ec.ECPublicKeyImpl
|
||||
import sun.security.provider.certpath.X509CertPath
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Modifier.isPublic
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
object DefaultKryoCustomizer {
|
||||
private val serializationWhitelists: List<SerializationWhitelist> by lazy {
|
||||
ServiceLoader.load(SerializationWhitelist::class.java, this.javaClass.classLoader).toList() + DefaultWhitelist
|
||||
}
|
||||
|
||||
fun customize(kryo: Kryo, publicKeySerializer: Serializer<PublicKey> = PublicKeySerializer): Kryo {
|
||||
return kryo.apply {
|
||||
// Store a little schema of field names in the stream the first time a class is used which increases tolerance
|
||||
// for change to a class.
|
||||
setDefaultSerializer(CompatibleFieldSerializer::class.java)
|
||||
// Take the safest route here and allow subclasses to have fields named the same as super classes.
|
||||
fieldSerializerConfig.cachedFieldNameStrategy = FieldSerializer.CachedFieldNameStrategy.EXTENDED
|
||||
|
||||
instantiatorStrategy = CustomInstantiatorStrategy()
|
||||
|
||||
// Required for HashCheckingStream (de)serialization.
|
||||
// Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...).
|
||||
addDefaultSerializer(InputStream::class.java, InputStreamSerializer)
|
||||
addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
|
||||
addDefaultSerializer(Logger::class.java, LoggerSerializer)
|
||||
addDefaultSerializer(X509Certificate::class.java, X509CertificateSerializer)
|
||||
|
||||
// WARNING: reordering the registrations here will cause a change in the serialized form, since classes
|
||||
// with custom serializers get written as registration ids. This will break backwards-compatibility.
|
||||
// Please add any new registrations to the end.
|
||||
// TODO: re-organise registrations into logical groups before v1.0
|
||||
|
||||
register(Arrays.asList("").javaClass, ArraysAsListSerializer())
|
||||
register(SignedTransaction::class.java, SignedTransactionSerializer)
|
||||
register(WireTransaction::class.java, WireTransactionSerializer)
|
||||
register(SerializedBytes::class.java, SerializedBytesSerializer)
|
||||
UnmodifiableCollectionsSerializer.registerSerializers(this)
|
||||
ImmutableListSerializer.registerSerializers(this)
|
||||
ImmutableSetSerializer.registerSerializers(this)
|
||||
ImmutableSortedSetSerializer.registerSerializers(this)
|
||||
ImmutableMapSerializer.registerSerializers(this)
|
||||
ImmutableMultimapSerializer.registerSerializers(this)
|
||||
// InputStream subclasses whitelisting, required for attachments.
|
||||
register(BufferedInputStream::class.java, InputStreamSerializer)
|
||||
register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer)
|
||||
noReferencesWithin<WireTransaction>()
|
||||
register(ECPublicKeyImpl::class.java, publicKeySerializer)
|
||||
register(EdDSAPublicKey::class.java, publicKeySerializer)
|
||||
register(EdDSAPrivateKey::class.java, PrivateKeySerializer)
|
||||
register(CompositeKey::class.java, publicKeySerializer) // Using a custom serializer for compactness
|
||||
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
|
||||
register(Array<StackTraceElement>::class, read = { _, _ -> emptyArray() }, write = { _, _, _ -> })
|
||||
// This ensures a NonEmptySetSerializer is constructed with an initial value.
|
||||
register(NonEmptySet::class.java, NonEmptySetSerializer)
|
||||
register(BitSet::class.java, BitSetSerializer())
|
||||
register(Class::class.java, ClassSerializer)
|
||||
register(FileInputStream::class.java, InputStreamSerializer)
|
||||
register(CertPath::class.java, CertPathSerializer)
|
||||
register(X509CertPath::class.java, CertPathSerializer)
|
||||
register(BCECPrivateKey::class.java, PrivateKeySerializer)
|
||||
register(BCECPublicKey::class.java, publicKeySerializer)
|
||||
register(BCRSAPrivateCrtKey::class.java, PrivateKeySerializer)
|
||||
register(BCRSAPublicKey::class.java, publicKeySerializer)
|
||||
register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer)
|
||||
register(BCSphincs256PublicKey::class.java, publicKeySerializer)
|
||||
register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer)
|
||||
register(PartyAndCertificate::class.java, PartyAndCertificateSerializer)
|
||||
|
||||
// Don't deserialize PrivacySalt via its default constructor.
|
||||
register(PrivacySalt::class.java, PrivacySaltSerializer)
|
||||
|
||||
// Used by the remote verifier, and will possibly be removed in future.
|
||||
register(ContractAttachment::class.java, ContractAttachmentSerializer)
|
||||
|
||||
register(java.lang.invoke.SerializedLambda::class.java)
|
||||
register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer)
|
||||
register(ContractUpgradeWireTransaction::class.java, ContractUpgradeWireTransactionSerializer)
|
||||
register(ContractUpgradeFilteredTransaction::class.java, ContractUpgradeFilteredTransactionSerializer)
|
||||
|
||||
for (whitelistProvider in serializationWhitelists) {
|
||||
val types = whitelistProvider.whitelist
|
||||
require(types.toSet().size == types.size) {
|
||||
val duplicates = types.toMutableList()
|
||||
types.toSet().forEach { duplicates -= it }
|
||||
"Cannot add duplicate classes to the whitelist ($duplicates)."
|
||||
}
|
||||
for (type in types) {
|
||||
((kryo.classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomInstantiatorStrategy : InstantiatorStrategy {
|
||||
private val fallbackStrategy = StdInstantiatorStrategy()
|
||||
// Use this to allow construction of objects using a JVM backdoor that skips invoking the constructors, if there
|
||||
// is no no-arg constructor available.
|
||||
private val defaultStrategy = Kryo.DefaultInstantiatorStrategy(fallbackStrategy)
|
||||
|
||||
override fun <T> newInstantiatorOf(type: Class<T>): ObjectInstantiator<T> {
|
||||
// However this doesn't work for non-public classes in the java. namespace
|
||||
val strat = if (type.name.startsWith("java.") && !isPublic(type.modifiers)) fallbackStrategy else defaultStrategy
|
||||
return strat.newInstantiatorOf(type)
|
||||
}
|
||||
}
|
||||
|
||||
private object PartyAndCertificateSerializer : Serializer<PartyAndCertificate>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: PartyAndCertificate) {
|
||||
kryo.writeClassAndObject(output, obj.certPath)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<PartyAndCertificate>): PartyAndCertificate {
|
||||
return PartyAndCertificate(kryo.readClassAndObject(input) as CertPath)
|
||||
}
|
||||
}
|
||||
|
||||
private object NonEmptySetSerializer : Serializer<NonEmptySet<Any>>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: NonEmptySet<Any>) {
|
||||
// Write out the contents as normal
|
||||
output.writeInt(obj.size, true)
|
||||
obj.forEach { kryo.writeClassAndObject(output, it) }
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<NonEmptySet<Any>>): NonEmptySet<Any> {
|
||||
val size = input.readInt(true)
|
||||
require(size >= 1) { "Invalid size read off the wire: $size" }
|
||||
val list = ArrayList<Any>(size)
|
||||
repeat(size) {
|
||||
list += kryo.readClassAndObject(input)
|
||||
}
|
||||
return list.toNonEmptySet()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Avoid deserialising PrivacySalt via its default constructor
|
||||
* because the random number generator may not be available.
|
||||
*/
|
||||
private object PrivacySaltSerializer : Serializer<PrivacySalt>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: PrivacySalt) {
|
||||
output.writeBytesWithLength(obj.bytes)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<PrivacySalt>): PrivacySalt {
|
||||
return PrivacySalt(input.readBytesWithLength())
|
||||
}
|
||||
}
|
||||
|
||||
private object ContractAttachmentSerializer : Serializer<ContractAttachment>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractAttachment) {
|
||||
if (kryo.serializationContext() != null) {
|
||||
obj.attachment.id.writeTo(output)
|
||||
} else {
|
||||
val buffer = ByteArrayOutputStream()
|
||||
obj.attachment.open().use { it.copyTo(buffer) }
|
||||
output.writeBytesWithLength(buffer.toByteArray())
|
||||
}
|
||||
output.writeString(obj.contract)
|
||||
kryo.writeClassAndObject(output, obj.additionalContracts)
|
||||
output.writeString(obj.uploader)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractAttachment>): ContractAttachment {
|
||||
if (kryo.serializationContext() != null) {
|
||||
val attachmentHash = SecureHash.SHA256(input.readBytes(32))
|
||||
val contract = input.readString()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
val context = kryo.serializationContext()!!
|
||||
val attachmentStorage = context.serviceHub.attachments
|
||||
|
||||
val lazyAttachment = object : AbstractAttachment({
|
||||
val attachment = attachmentStorage.openAttachment(attachmentHash)
|
||||
?: throw MissingAttachmentsException(listOf(attachmentHash))
|
||||
attachment.open().readFully()
|
||||
}) {
|
||||
override val id = attachmentHash
|
||||
}
|
||||
|
||||
return ContractAttachment(lazyAttachment, contract, additionalContracts, uploader)
|
||||
} else {
|
||||
val attachment = GeneratedAttachment(input.readBytesWithLength())
|
||||
val contract = input.readString()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val additionalContracts = kryo.readClassAndObject(input) as Set<ContractClassName>
|
||||
val uploader = input.readString()
|
||||
return ContractAttachment(attachment, contract, additionalContracts, uploader)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,497 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.*
|
||||
import com.esotericsoftware.kryo.factories.ReflectionSerializerFactory
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
|
||||
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Checkpoint
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.Storage
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.serialization.CordaClassResolver
|
||||
import net.corda.nodeapi.internal.serialization.serializationContextKey
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KMutableProperty
|
||||
import kotlin.reflect.KParameter
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
/**
|
||||
* Serialization utilities, using the Kryo framework with a custom serializer for immutable data classes and a dead
|
||||
* simple, totally non-extensible binary (sub)format. Used exclusively within Corda for checkpointing flows as
|
||||
* it will happily deserialise literally anything, including malicious streams that would reconstruct classes
|
||||
* in invalid states and thus violating system invariants. In the context of checkpointing a Java stack, this is
|
||||
* absolutely the functionality we desire, for a stable binary wire format and persistence technology, we have
|
||||
* the AMQP implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A serializer that avoids writing the wrapper class to the byte stream, thus ensuring [SerializedBytes] is a pure
|
||||
* type safety hack.
|
||||
*/
|
||||
object SerializedBytesSerializer : Serializer<SerializedBytes<Any>>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: SerializedBytes<Any>) {
|
||||
output.writeVarInt(obj.size, true)
|
||||
obj.writeTo(output)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<SerializedBytes<Any>>): SerializedBytes<Any> {
|
||||
return SerializedBytes(input.readBytes(input.readVarInt(true)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes properties and deserializes by using the constructor. This assumes that all backed properties are
|
||||
* set via the constructor and the class is immutable.
|
||||
*/
|
||||
class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>() {
|
||||
val props = klass.memberProperties.sortedBy { it.name }
|
||||
val propsByName = props.associateBy { it.name }
|
||||
val constructor = klass.primaryConstructor!!
|
||||
|
||||
init {
|
||||
// Verify that this class is immutable (all properties are final)
|
||||
assert(props.none { it is KMutableProperty<*> })
|
||||
}
|
||||
|
||||
// Just a utility to help us catch cases where nodes are running out of sync versions.
|
||||
private fun hashParameters(params: List<KParameter>): Int {
|
||||
return params.map {
|
||||
(it.name ?: "") + it.index.toString() + it.type.javaType.typeName
|
||||
}.hashCode()
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, obj: T) {
|
||||
output.writeVarInt(constructor.parameters.size, true)
|
||||
output.writeInt(hashParameters(constructor.parameters))
|
||||
for (param in constructor.parameters) {
|
||||
val kProperty = propsByName[param.name!!]!!
|
||||
kProperty.isAccessible = true
|
||||
when (param.type.javaType.typeName) {
|
||||
"int" -> output.writeVarInt(kProperty.get(obj) as Int, true)
|
||||
"long" -> output.writeVarLong(kProperty.get(obj) as Long, true)
|
||||
"short" -> output.writeShort(kProperty.get(obj) as Int)
|
||||
"char" -> output.writeChar(kProperty.get(obj) as Char)
|
||||
"byte" -> output.writeByte(kProperty.get(obj) as Byte)
|
||||
"double" -> output.writeDouble(kProperty.get(obj) as Double)
|
||||
"float" -> output.writeFloat(kProperty.get(obj) as Float)
|
||||
"boolean" -> output.writeBoolean(kProperty.get(obj) as Boolean)
|
||||
else -> try {
|
||||
kryo.writeClassAndObject(output, kProperty.get(obj))
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Failed to serialize ${param.name} in ${klass.qualifiedName}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<T>): T {
|
||||
assert(type.kotlin == klass)
|
||||
val numFields = input.readVarInt(true)
|
||||
val fieldTypeHash = input.readInt()
|
||||
|
||||
// A few quick checks for data evolution. Note that this is not guaranteed to catch every problem! But it's
|
||||
// good enough for a prototype.
|
||||
if (numFields != constructor.parameters.size)
|
||||
throw KryoException("Mismatch between number of constructor parameters and number of serialised fields " +
|
||||
"for ${klass.qualifiedName} ($numFields vs ${constructor.parameters.size})")
|
||||
if (fieldTypeHash != hashParameters(constructor.parameters))
|
||||
throw KryoException("Hashcode mismatch for parameter types for ${klass.qualifiedName}: unsupported type evolution has happened.")
|
||||
|
||||
val args = arrayOfNulls<Any?>(numFields)
|
||||
var cursor = 0
|
||||
for (param in constructor.parameters) {
|
||||
args[cursor++] = when (param.type.javaType.typeName) {
|
||||
"int" -> input.readVarInt(true)
|
||||
"long" -> input.readVarLong(true)
|
||||
"short" -> input.readShort()
|
||||
"char" -> input.readChar()
|
||||
"byte" -> input.readByte()
|
||||
"double" -> input.readDouble()
|
||||
"float" -> input.readFloat()
|
||||
"boolean" -> input.readBoolean()
|
||||
else -> kryo.readClassAndObject(input)
|
||||
}
|
||||
}
|
||||
// If the constructor throws an exception, pass it through instead of wrapping it.
|
||||
return try {
|
||||
constructor.call(*args)
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This is a temporary inefficient serializer for sending InputStreams through RPC. This may be done much more
|
||||
// efficiently using Artemis's large message feature.
|
||||
object InputStreamSerializer : Serializer<InputStream>() {
|
||||
override fun write(kryo: Kryo, output: Output, stream: InputStream) {
|
||||
val buffer = ByteArray(4096)
|
||||
while (true) {
|
||||
val numberOfBytesRead = stream.read(buffer)
|
||||
if (numberOfBytesRead != -1) {
|
||||
output.writeInt(numberOfBytesRead, true)
|
||||
output.writeBytes(buffer, 0, numberOfBytesRead)
|
||||
} else {
|
||||
output.writeInt(0, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<InputStream>): InputStream {
|
||||
val chunks = ArrayList<ByteArray>()
|
||||
while (true) {
|
||||
val chunk = input.readBytesWithLength()
|
||||
if (chunk.isEmpty()) {
|
||||
break
|
||||
} else {
|
||||
chunks.add(chunk)
|
||||
}
|
||||
}
|
||||
val flattened = ByteArray(chunks.sumBy { it.size })
|
||||
var offset = 0
|
||||
for (chunk in chunks) {
|
||||
System.arraycopy(chunk, 0, flattened, offset, chunk.size)
|
||||
offset += chunk.size
|
||||
}
|
||||
return flattened.inputStream()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inline fun <T> Kryo.useClassLoader(cl: ClassLoader, body: () -> T): T {
|
||||
val tmp = this.classLoader ?: ClassLoader.getSystemClassLoader()
|
||||
this.classLoader = cl
|
||||
try {
|
||||
return body()
|
||||
} finally {
|
||||
this.classLoader = tmp
|
||||
}
|
||||
}
|
||||
|
||||
fun Output.writeBytesWithLength(byteArray: ByteArray) {
|
||||
this.writeInt(byteArray.size, true)
|
||||
this.writeBytes(byteArray)
|
||||
}
|
||||
|
||||
fun Input.readBytesWithLength(): ByteArray {
|
||||
val size = this.readInt(true)
|
||||
return this.readBytes(size)
|
||||
}
|
||||
|
||||
/** A serialisation engine that knows how to deserialise code inside a sandbox */
|
||||
@ThreadSafe
|
||||
object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.componentGroups)
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<WireTransaction>): WireTransaction {
|
||||
val componentGroups: List<ComponentGroup> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
return WireTransaction(componentGroups, privacySalt)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object NotaryChangeWireTransactionSerializer : Serializer<NotaryChangeWireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: NotaryChangeWireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.serializedComponents)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<NotaryChangeWireTransaction>): NotaryChangeWireTransaction {
|
||||
val components: List<OpaqueBytes> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
return NotaryChangeWireTransaction(components)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object ContractUpgradeWireTransactionSerializer : Serializer<ContractUpgradeWireTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeWireTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.serializedComponents)
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractUpgradeWireTransaction>): ContractUpgradeWireTransaction {
|
||||
val components: List<OpaqueBytes> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
|
||||
return ContractUpgradeWireTransaction(components, privacySalt)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object ContractUpgradeFilteredTransactionSerializer : Serializer<ContractUpgradeFilteredTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ContractUpgradeFilteredTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.visibleComponents)
|
||||
kryo.writeClassAndObject(output, obj.hiddenComponents)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ContractUpgradeFilteredTransaction>): ContractUpgradeFilteredTransaction {
|
||||
val visibleComponents: Map<Int, ContractUpgradeFilteredTransaction.FilteredComponent> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val hiddenComponents: Map<Int, SecureHash> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
return ContractUpgradeFilteredTransaction(visibleComponents, hiddenComponents)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object SignedTransactionSerializer : Serializer<SignedTransaction>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: SignedTransaction) {
|
||||
kryo.writeClassAndObject(output, obj.txBits)
|
||||
kryo.writeClassAndObject(output, obj.sigs)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<SignedTransaction>): SignedTransaction {
|
||||
return SignedTransaction(
|
||||
uncheckedCast<Any?, SerializedBytes<CoreTransaction>>(kryo.readClassAndObject(input)),
|
||||
uncheckedCast<Any?, List<TransactionSignature>>(kryo.readClassAndObject(input))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UseCaseSerializer<T>(private val allowedUseCases: EnumSet<SerializationContext.UseCase>) : Serializer<T>() {
|
||||
protected fun checkUseCase() {
|
||||
net.corda.nodeapi.internal.serialization.checkUseCase(allowedUseCases)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object PrivateKeySerializer : UseCaseSerializer<PrivateKey>(EnumSet.of(Storage, Checkpoint)) {
|
||||
override fun write(kryo: Kryo, output: Output, obj: PrivateKey) {
|
||||
checkUseCase()
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<PrivateKey>): PrivateKey {
|
||||
val A = input.readBytesWithLength()
|
||||
return Crypto.decodePrivateKey(A)
|
||||
}
|
||||
}
|
||||
|
||||
/** For serialising a public key */
|
||||
@ThreadSafe
|
||||
object PublicKeySerializer : Serializer<PublicKey>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: PublicKey) {
|
||||
// TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser.
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<PublicKey>): PublicKey {
|
||||
val A = input.readBytesWithLength()
|
||||
return Crypto.decodePublicKey(A)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for reading lists with number of elements at the beginning.
|
||||
* @param minLen minimum number of elements we expect for list to include, defaults to 1
|
||||
* @param expectedLen expected length of the list, defaults to null if arbitrary length list read
|
||||
*/
|
||||
inline fun <reified T> readListOfLength(kryo: Kryo, input: Input, minLen: Int = 1, expectedLen: Int? = null): List<T> {
|
||||
val elemCount = input.readInt()
|
||||
if (elemCount < minLen) throw KryoException("Cannot deserialize list, too little elements. Minimum required: $minLen, got: $elemCount")
|
||||
if (expectedLen != null && elemCount != expectedLen)
|
||||
throw KryoException("Cannot deserialize list, expected length: $expectedLen, got: $elemCount.")
|
||||
return (1..elemCount).map { kryo.readClassAndObject(input) as T }
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to disable whitelist checking during calls from our Kryo code to register a serializer, since it checks
|
||||
* for existing registrations and then will enter our [CordaClassResolver.getRegistration] method.
|
||||
*/
|
||||
open class CordaKryo(classResolver: ClassResolver) : Kryo(classResolver, MapReferenceResolver()) {
|
||||
override fun register(type: Class<*>?): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
try {
|
||||
return super.register(type)
|
||||
} finally {
|
||||
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||
}
|
||||
}
|
||||
|
||||
override fun register(type: Class<*>?, id: Int): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
try {
|
||||
return super.register(type, id)
|
||||
} finally {
|
||||
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||
}
|
||||
}
|
||||
|
||||
override fun register(type: Class<*>?, serializer: Serializer<*>?): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
try {
|
||||
return super.register(type, serializer)
|
||||
} finally {
|
||||
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||
}
|
||||
}
|
||||
|
||||
override fun register(registration: Registration?): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
try {
|
||||
return super.register(registration)
|
||||
} finally {
|
||||
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : Any> Kryo.register(
|
||||
type: KClass<T>,
|
||||
crossinline read: (Kryo, Input) -> T,
|
||||
crossinline write: (Kryo, Output, T) -> Unit): Registration {
|
||||
return register(
|
||||
type.java,
|
||||
object : Serializer<T>() {
|
||||
override fun read(kryo: Kryo, input: Input, clazz: Class<T>): T = read(kryo, input)
|
||||
override fun write(kryo: Kryo, output: Output, obj: T) = write(kryo, output, obj)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to mark any types which can have the same instance within it more than once. This will make sure
|
||||
* the serialised form is stable across multiple serialise-deserialise cycles. Using this on a type with internal cyclic
|
||||
* references will throw a stack overflow exception during serialisation.
|
||||
*/
|
||||
inline fun <reified T : Any> Kryo.noReferencesWithin() {
|
||||
register(T::class.java, NoReferencesSerializer(getSerializer(T::class.java)))
|
||||
}
|
||||
|
||||
class NoReferencesSerializer<T>(private val baseSerializer: Serializer<T>) : Serializer<T>() {
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<T>): T {
|
||||
return kryo.withoutReferences { baseSerializer.read(kryo, input, type) }
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, obj: T) {
|
||||
kryo.withoutReferences { baseSerializer.write(kryo, output, obj) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Kryo.withoutReferences(block: () -> T): T {
|
||||
val previousValue = setReferences(false)
|
||||
try {
|
||||
return block()
|
||||
} finally {
|
||||
references = previousValue
|
||||
}
|
||||
}
|
||||
|
||||
/** For serialising a Logger. */
|
||||
@ThreadSafe
|
||||
object LoggerSerializer : Serializer<Logger>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: Logger) {
|
||||
output.writeString(obj.name)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Logger>): Logger {
|
||||
return LoggerFactory.getLogger(input.readString())
|
||||
}
|
||||
}
|
||||
|
||||
object ClassSerializer : Serializer<Class<*>>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Class<*>>): Class<*> {
|
||||
val className = input.readString()
|
||||
return Class.forName(className, true, kryo.classLoader)
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, clazz: Class<*>) {
|
||||
output.writeString(clazz.name)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object CertPathSerializer : Serializer<CertPath>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<CertPath>): CertPath {
|
||||
val factory = CertificateFactory.getInstance(input.readString())
|
||||
return factory.generateCertPath(input.readBytesWithLength().inputStream())
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, obj: CertPath) {
|
||||
output.writeString(obj.type)
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object X509CertificateSerializer : Serializer<X509Certificate>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
|
||||
return X509CertificateFactory().generateCertificate(input.readBytesWithLength().inputStream())
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
fun Kryo.serializationContext(): SerializeAsTokenContext? = context.get(serializationContextKey) as? SerializeAsTokenContext
|
||||
|
||||
/**
|
||||
* For serializing instances if [Throwable] honoring the fact that [java.lang.Throwable.suppressedExceptions]
|
||||
* might be un-initialized/empty.
|
||||
* In the absence of this class [CompatibleFieldSerializer] will be used which will assign a *new* instance of
|
||||
* unmodifiable collection to [java.lang.Throwable.suppressedExceptions] which will fail some sentinel identity checks
|
||||
* e.g. in [java.lang.Throwable.addSuppressed]
|
||||
*/
|
||||
@ThreadSafe
|
||||
class ThrowableSerializer<T>(kryo: Kryo, type: Class<T>) : Serializer<Throwable>(false, true) {
|
||||
|
||||
private companion object {
|
||||
private val suppressedField = Throwable::class.java.getDeclaredField("suppressedExceptions")
|
||||
|
||||
private val sentinelValue = let {
|
||||
val sentinelField = Throwable::class.java.getDeclaredField("SUPPRESSED_SENTINEL")
|
||||
sentinelField.isAccessible = true
|
||||
sentinelField.get(null)
|
||||
}
|
||||
|
||||
init {
|
||||
suppressedField.isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
private val delegate: Serializer<Throwable> = uncheckedCast(ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type))
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, throwable: Throwable) {
|
||||
delegate.write(kryo, output, throwable)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Throwable>): Throwable {
|
||||
val throwableRead = delegate.read(kryo, input, type)
|
||||
if (throwableRead.suppressed.isEmpty()) {
|
||||
throwableRead.setSuppressedToSentinel()
|
||||
}
|
||||
return throwableRead
|
||||
}
|
||||
|
||||
private fun Throwable.setSuppressedToSentinel() = suppressedField.set(this, sentinelValue)
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import com.esotericsoftware.kryo.serializers.ClosureSerializer
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.*
|
||||
import net.corda.nodeapi.internal.serialization.SectionId
|
||||
import java.security.PublicKey
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0))
|
||||
|
||||
private object AutoCloseableSerialisationDetector : Serializer<AutoCloseable>() {
|
||||
override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) {
|
||||
val message = "${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " +
|
||||
"Restoring such resources across node restarts is not supported. Make sure code accessing it is " +
|
||||
"confined to a private method or the reference is nulled out."
|
||||
throw UnsupportedOperationException(message)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<AutoCloseable>) = throw IllegalStateException("Should not reach here!")
|
||||
}
|
||||
|
||||
abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
private val kryoPoolsForContexts = ConcurrentHashMap<Pair<ClassWhitelist, ClassLoader>, KryoPool>()
|
||||
|
||||
protected abstract fun rpcClientKryoPool(context: SerializationContext): KryoPool
|
||||
protected abstract fun rpcServerKryoPool(context: SerializationContext): KryoPool
|
||||
|
||||
// this can be overridden in derived serialization schemes
|
||||
protected open val publicKeySerializer: Serializer<PublicKey> = PublicKeySerializer
|
||||
|
||||
private fun getPool(context: SerializationContext): KryoPool {
|
||||
return kryoPoolsForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) {
|
||||
when (context.useCase) {
|
||||
SerializationContext.UseCase.Checkpoint ->
|
||||
KryoPool.Builder {
|
||||
val serializer = Fiber.getFiberSerializer(false) as KryoSerializer
|
||||
val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) }
|
||||
// TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that
|
||||
val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true }
|
||||
serializer.kryo.apply {
|
||||
field.set(this, classResolver)
|
||||
// don't allow overriding the public key serializer for checkpointing
|
||||
DefaultKryoCustomizer.customize(this)
|
||||
addDefaultSerializer(AutoCloseable::class.java, AutoCloseableSerialisationDetector)
|
||||
register(ClosureSerializer.Closure::class.java, CordaClosureSerializer)
|
||||
classLoader = it.second
|
||||
}
|
||||
}.build()
|
||||
SerializationContext.UseCase.RPCClient ->
|
||||
rpcClientKryoPool(context)
|
||||
SerializationContext.UseCase.RPCServer ->
|
||||
rpcServerKryoPool(context)
|
||||
else ->
|
||||
KryoPool.Builder {
|
||||
DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(context)), publicKeySerializer).apply { classLoader = it.second }
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> SerializationContext.kryo(task: Kryo.() -> T): T {
|
||||
return getPool(this).run { kryo ->
|
||||
kryo.context.ensureCapacity(properties.size)
|
||||
properties.forEach { kryo.context.put(it.key, it.value) }
|
||||
try {
|
||||
kryo.task()
|
||||
} finally {
|
||||
kryo.context.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
val dataBytes = kryoMagic.consume(byteSequence)
|
||||
?: throw KryoException("Serialized bytes header does not match expected format.")
|
||||
return context.kryo {
|
||||
kryoInput(ByteBufferInputStream(dataBytes)) {
|
||||
val result: T
|
||||
loop@ while (true) {
|
||||
when (SectionId.reader.readFrom(this)) {
|
||||
SectionId.ENCODING -> {
|
||||
val encoding = CordaSerializationEncoding.reader.readFrom(this)
|
||||
context.encodingWhitelist.acceptEncoding(encoding) || throw KryoException(encodingNotPermittedFormat.format(encoding))
|
||||
substitute(encoding::wrap)
|
||||
}
|
||||
SectionId.DATA_AND_STOP, SectionId.ALT_DATA_AND_STOP -> {
|
||||
result = if (context.objectReferencesEnabled) {
|
||||
uncheckedCast(readClassAndObject(this))
|
||||
} else {
|
||||
withoutReferences { uncheckedCast<Any?, T>(readClassAndObject(this)) }
|
||||
}
|
||||
break@loop
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
return context.kryo {
|
||||
SerializedBytes(kryoOutput {
|
||||
kryoMagic.writeTo(this)
|
||||
context.encoding?.let { encoding ->
|
||||
SectionId.ENCODING.writeTo(this)
|
||||
(encoding as CordaSerializationEncoding).writeTo(this)
|
||||
substitute(encoding::wrap)
|
||||
}
|
||||
SectionId.ALT_DATA_AND_STOP.writeTo(this) // Forward-compatible in null-encoding case.
|
||||
if (context.objectReferencesEnabled) {
|
||||
writeClassAndObject(this, obj)
|
||||
} else {
|
||||
withoutReferences { writeClassAndObject(this, obj) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
@file:JvmName("KryoStreams")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.nodeapi.internal.serialization.byteArrayOutput
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.SequenceInputStream
|
||||
|
||||
private val serializationBufferPool = LazyPool(
|
||||
newInstance = { ByteArray(64 * 1024) })
|
||||
|
||||
internal fun <T> kryoInput(underlying: InputStream, task: Input.() -> T): T {
|
||||
return serializationBufferPool.run {
|
||||
Input(it).use { input ->
|
||||
input.inputStream = underlying
|
||||
input.task()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> kryoOutput(task: Output.() -> T): ByteArray {
|
||||
return byteArrayOutput { underlying ->
|
||||
serializationBufferPool.run {
|
||||
Output(it).use { output ->
|
||||
output.outputStream = underlying
|
||||
output.task()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Output.substitute(transform: (OutputStream) -> OutputStream) {
|
||||
flush()
|
||||
outputStream = transform(outputStream)
|
||||
}
|
||||
|
||||
internal fun Input.substitute(transform: (InputStream) -> InputStream) {
|
||||
inputStream = transform(SequenceInputStream(buffer.copyOfRange(position(), limit()).inputStream(), inputStream))
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.kryo
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.serialization.SerializationToken
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
|
||||
/**
|
||||
* A Kryo serializer for [SerializeAsToken] implementations.
|
||||
*/
|
||||
class SerializeAsTokenSerializer<T : SerializeAsToken> : Serializer<T>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: T) {
|
||||
kryo.writeClassAndObject(output, obj.toToken(kryo.serializationContext()
|
||||
?: throw KryoException("Attempt to write a ${SerializeAsToken::class.simpleName} instance of ${obj.javaClass.name} without initialising a context")))
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<T>): T {
|
||||
val token = (kryo.readClassAndObject(input) as? SerializationToken)
|
||||
?: throw KryoException("Non-token read for tokenized type: ${type.name}")
|
||||
val fromToken = token.fromToken(kryo.serializationContext()
|
||||
?: throw KryoException("Attempt to read a token for a ${SerializeAsToken::class.simpleName} instance of ${type.name} without initialising a context"))
|
||||
return type.castIfPossible(fromToken)
|
||||
?: throw KryoException("Token read ($token) did not return expected tokenized type: ${type.name}")
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import net.corda.core.serialization.SerializationContext;
|
||||
import net.corda.core.serialization.SerializationFactory;
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SchemaKt;
|
||||
import net.corda.testing.core.SerializationEnvironmentRule;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.io.Serializable;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
|
||||
|
||||
public final class ForbiddenLambdaSerializationTests {
|
||||
private EnumSet<SerializationContext.UseCase> contexts = EnumSet.complementOf(
|
||||
EnumSet.of(SerializationContext.UseCase.Checkpoint, SerializationContext.UseCase.Testing));
|
||||
@Rule
|
||||
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
|
||||
private SerializationFactory factory;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
factory = testSerialization.getSerializationFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
public final void serialization_fails_for_serializable_java_lambdas() {
|
||||
contexts.forEach(ctx -> {
|
||||
SerializationContext context = new SerializationContextImpl(SchemaKt.getAmqpMagic(),
|
||||
this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null);
|
||||
String value = "Hey";
|
||||
Callable<String> target = (Callable<String> & Serializable) () -> value;
|
||||
|
||||
Throwable throwable = catchThrowable(() -> serialize(target, context));
|
||||
|
||||
assertThat(throwable)
|
||||
.isNotNull()
|
||||
.isInstanceOf(NotSerializableException.class)
|
||||
.hasMessageContaining(getClass().getName());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void serialization_fails_for_not_serializable_java_lambdas() {
|
||||
contexts.forEach(ctx -> {
|
||||
SerializationContext context = new SerializationContextImpl(SchemaKt.getAmqpMagic(),
|
||||
this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, ctx, null);
|
||||
String value = "Hey";
|
||||
Callable<String> target = () -> value;
|
||||
|
||||
Throwable throwable = catchThrowable(() -> serialize(target, context));
|
||||
|
||||
assertThat(throwable)
|
||||
.isNotNull()
|
||||
.isInstanceOf(NotSerializableException.class)
|
||||
.hasMessageContaining(getClass().getName());
|
||||
});
|
||||
}
|
||||
|
||||
private <T> SerializedBytes<T> serialize(final T target, final SerializationContext context) {
|
||||
return factory.serialize(target, context);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import net.corda.core.serialization.SerializationContext;
|
||||
import net.corda.core.serialization.SerializationFactory;
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.testing.core.SerializationEnvironmentRule;
|
||||
import net.corda.nodeapi.internal.serialization.kryo.CordaClosureSerializer;
|
||||
import net.corda.nodeapi.internal.serialization.kryo.KryoSerializationSchemeKt;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
|
||||
|
||||
public final class LambdaCheckpointSerializationTest {
|
||||
@Rule
|
||||
public final SerializationEnvironmentRule testSerialization = new SerializationEnvironmentRule();
|
||||
private SerializationFactory factory;
|
||||
private SerializationContext context;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
factory = testSerialization.getSerializationFactory();
|
||||
context = new SerializationContextImpl(KryoSerializationSchemeKt.getKryoMagic(), this.getClass().getClassLoader(), AllWhitelist.INSTANCE, Maps.newHashMap(), true, SerializationContext.UseCase.Checkpoint, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void serialization_works_for_serializable_java_lambdas() throws Exception {
|
||||
String value = "Hey";
|
||||
Callable<String> target = (Callable<String> & Serializable) () -> value;
|
||||
|
||||
SerializedBytes<Callable<String>> serialized = serialize(target);
|
||||
Callable<String> deserialized = deserialize(serialized, Callable.class);
|
||||
|
||||
assertThat(deserialized.call()).isEqualTo(value);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void serialization_fails_for_not_serializable_java_lambdas() throws Exception {
|
||||
String value = "Hey";
|
||||
Callable<String> target = () -> value;
|
||||
|
||||
Throwable throwable = catchThrowable(() -> serialize(target));
|
||||
|
||||
assertThat(throwable).isNotNull();
|
||||
assertThat(throwable).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(throwable).hasMessage(CordaClosureSerializer.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
private <T> SerializedBytes<T> serialize(final T target) {
|
||||
return factory.serialize(target, context);
|
||||
}
|
||||
|
||||
private <T> T deserialize(final SerializedBytes<? extends T> bytes, final Class<T> type) {
|
||||
return factory.deserialize(bytes, type, context);
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
|
||||
@Ignore("Current behaviour allows for the serialization of objects with private members, this will be disallowed at some point in the future")
|
||||
public class ErrorMessageTests {
|
||||
private String errMsg(String property, String testname) {
|
||||
return "Property '"
|
||||
+ property
|
||||
+ "' or its getter is non public, this renders class 'class "
|
||||
+ testname
|
||||
+ "$C' unserializable -> class "
|
||||
+ testname
|
||||
+ "$C";
|
||||
}
|
||||
|
||||
static class C {
|
||||
public Integer a;
|
||||
|
||||
public C(Integer a) {
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
private Integer getA() { return this.a; }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJavaConstructorAnnotations() {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
|
||||
Assertions.assertThatThrownBy(() -> ser.serialize(new C(1), TestSerializationContext.testSerializationContext))
|
||||
.isInstanceOf(NotSerializableException.class)
|
||||
.hasMessage(errMsg("a", getClass().getName()));
|
||||
}
|
||||
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.junit.Test;
|
||||
import java.io.NotSerializableException;
|
||||
|
||||
import static org.jgroups.util.Util.assertEquals;
|
||||
|
||||
public class JavaGenericsTest {
|
||||
private static class Inner {
|
||||
private final Integer v;
|
||||
|
||||
private Inner(Integer v) { this.v = v; }
|
||||
public Integer getV() { return v; }
|
||||
}
|
||||
|
||||
private static class A<T> {
|
||||
private final T t;
|
||||
|
||||
private A(T t) { this.t = t; }
|
||||
public T getT() { return t; }
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicGeneric() throws NotSerializableException {
|
||||
A a1 = new A(1);
|
||||
|
||||
SerializerFactory factory = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
SerializedBytes<?> bytes = ser.serialize(a1, TestSerializationContext.testSerializationContext);
|
||||
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
A a2 = des.deserialize(bytes, A.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals(1, a2.getT());
|
||||
}
|
||||
|
||||
private SerializedBytes<?> forceWildcardSerialize(A<?> a) throws NotSerializableException {
|
||||
SerializerFactory factory = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
return (new SerializationOutput(factory)).serialize(a, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
|
||||
private SerializedBytes<?> forceWildcardSerializeFactory(
|
||||
A<?> a,
|
||||
SerializerFactory factory) throws NotSerializableException {
|
||||
return (new SerializationOutput(factory)).serialize(a, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
|
||||
private A<?> forceWildcardDeserialize(SerializedBytes<?> bytes) throws NotSerializableException {
|
||||
SerializerFactory factory = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
return des.deserialize(bytes, A.class, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
|
||||
private A<?> forceWildcardDeserializeFactory(
|
||||
SerializedBytes<?> bytes,
|
||||
SerializerFactory factory) throws NotSerializableException {
|
||||
return (new DeserializationInput(factory)).deserialize(bytes, A.class,
|
||||
TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forceWildcard() throws NotSerializableException {
|
||||
SerializedBytes<?> bytes = forceWildcardSerialize(new A(new Inner(29)));
|
||||
Inner i = (Inner)forceWildcardDeserialize(bytes).getT();
|
||||
assertEquals(29, i.getV());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forceWildcardSharedFactory() throws NotSerializableException {
|
||||
SerializerFactory factory = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializedBytes<?> bytes = forceWildcardSerializeFactory(new A(new Inner(29)), factory);
|
||||
Inner i = (Inner)forceWildcardDeserializeFactory(bytes, factory).getT();
|
||||
|
||||
assertEquals(29, i.getV());
|
||||
}
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.util.List;
|
||||
|
||||
class OuterClass1 {
|
||||
protected SerializationOutput ser;
|
||||
DeserializationInput desExisting;
|
||||
DeserializationInput desRegen;
|
||||
|
||||
OuterClass1() {
|
||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
this.ser = new SerializationOutput(factory1);
|
||||
this.desExisting = new DeserializationInput(factory1);
|
||||
this.desRegen = new DeserializationInput(factory2);
|
||||
}
|
||||
|
||||
class DummyState implements ContractState {
|
||||
@Override @NotNull public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() throws NotSerializableException {
|
||||
SerializedBytes b = ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext);
|
||||
desExisting.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
desRegen.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
class Inherator1 extends OuterClass1 {
|
||||
public void iRun() throws NotSerializableException {
|
||||
SerializedBytes b = ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext);
|
||||
desExisting.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
desRegen.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
class OuterClass2 {
|
||||
protected SerializationOutput ser;
|
||||
DeserializationInput desExisting;
|
||||
DeserializationInput desRegen;
|
||||
|
||||
OuterClass2() {
|
||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
this.ser = new SerializationOutput(factory1);
|
||||
this.desExisting = new DeserializationInput(factory1);
|
||||
this.desRegen = new DeserializationInput(factory2);
|
||||
}
|
||||
|
||||
protected class DummyState implements ContractState {
|
||||
private Integer count;
|
||||
|
||||
DummyState(Integer count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override @NotNull public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() throws NotSerializableException {
|
||||
SerializedBytes b = ser.serialize(new DummyState(12), TestSerializationContext.testSerializationContext);
|
||||
desExisting.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
desRegen.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
class Inherator2 extends OuterClass2 {
|
||||
public void iRun() throws NotSerializableException {
|
||||
SerializedBytes b = ser.serialize(new DummyState(12), TestSerializationContext.testSerializationContext);
|
||||
desExisting.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
desRegen.deserialize(b, DummyState.class, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Make the base class abstract
|
||||
abstract class AbstractClass2 {
|
||||
protected SerializationOutput ser;
|
||||
|
||||
AbstractClass2() {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
this.ser = new SerializationOutput(factory);
|
||||
}
|
||||
|
||||
protected class DummyState implements ContractState {
|
||||
@Override @NotNull public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Inherator4 extends AbstractClass2 {
|
||||
public void run() throws NotSerializableException {
|
||||
ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractClass3 {
|
||||
protected class DummyState implements ContractState {
|
||||
@Override @NotNull public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Inherator5 extends AbstractClass3 {
|
||||
public void run() throws NotSerializableException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
class Inherator6 extends AbstractClass3 {
|
||||
public class Wrapper {
|
||||
//@Suppress("UnusedDeclaration"])
|
||||
private ContractState cState;
|
||||
|
||||
Wrapper(ContractState cState) {
|
||||
this.cState = cState;
|
||||
}
|
||||
}
|
||||
|
||||
public void run() throws NotSerializableException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
ser.serialize(new Wrapper(new DummyState()), TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
||||
|
||||
public class JavaNestedClassesTests {
|
||||
@Test
|
||||
public void publicNested() {
|
||||
Assertions.assertThatThrownBy(() -> new OuterClass1().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void privateNested() {
|
||||
Assertions.assertThatThrownBy(() -> new OuterClass2().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicNestedInherited() {
|
||||
Assertions.assertThatThrownBy(() -> new Inherator1().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
|
||||
Assertions.assertThatThrownBy(() -> new Inherator1().iRun()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedNestedInherited() {
|
||||
Assertions.assertThatThrownBy(() -> new Inherator2().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
|
||||
Assertions.assertThatThrownBy(() -> new Inherator2().iRun()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void abstractNested() {
|
||||
Assertions.assertThatThrownBy(() -> new Inherator4().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void abstractNestedFactoryOnNested() {
|
||||
Assertions.assertThatThrownBy(() -> new Inherator5().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void abstractNestedFactoryOnNestedInWrapper() {
|
||||
Assertions.assertThatThrownBy(() -> new Inherator6().run()).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.util.List;
|
||||
|
||||
abstract class JavaNestedInheritenceTestsBase {
|
||||
class DummyState implements ContractState {
|
||||
@Override @NotNull public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Wrapper {
|
||||
private ContractState cs;
|
||||
Wrapper(ContractState cs) { this.cs = cs; }
|
||||
}
|
||||
|
||||
class TemplateWrapper<T> {
|
||||
public T obj;
|
||||
TemplateWrapper(T obj) { this.obj = obj; }
|
||||
}
|
||||
|
||||
public class JavaNestedInheritenceTests extends JavaNestedInheritenceTestsBase {
|
||||
@Test
|
||||
public void serializeIt() {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
|
||||
Assertions.assertThatThrownBy(() -> ser.serialize(new DummyState(), TestSerializationContext.testSerializationContext)).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeIt2() {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
Assertions.assertThatThrownBy(() -> ser.serialize(new Wrapper (new DummyState()), TestSerializationContext.testSerializationContext)).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeIt3() throws NotSerializableException {
|
||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
|
||||
Assertions.assertThatThrownBy(() -> ser.serialize(new TemplateWrapper<ContractState> (new DummyState()), TestSerializationContext.testSerializationContext)).isInstanceOf(
|
||||
NotSerializableException.class).hasMessageContaining(
|
||||
"has synthetic fields and is likely a nested inner class");
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
|
||||
public class JavaPrivatePropertyTests {
|
||||
static class C {
|
||||
private String a;
|
||||
|
||||
C(String a) { this.a = a; }
|
||||
}
|
||||
|
||||
static class C2 {
|
||||
private String a;
|
||||
|
||||
C2(String a) { this.a = a; }
|
||||
|
||||
public String getA() { return a; }
|
||||
}
|
||||
|
||||
static class B {
|
||||
private Boolean b;
|
||||
|
||||
B(Boolean b) { this.b = b; }
|
||||
|
||||
public Boolean isB() {
|
||||
return this.b;
|
||||
}
|
||||
}
|
||||
|
||||
static class B2 {
|
||||
private Boolean b;
|
||||
|
||||
public Boolean isB() {
|
||||
return this.b;
|
||||
}
|
||||
|
||||
public void setB(Boolean b) {
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
static class B3 {
|
||||
private Boolean b;
|
||||
|
||||
// break the BEAN format explicitly (i.e. it's not isB)
|
||||
public Boolean isb() {
|
||||
return this.b;
|
||||
}
|
||||
|
||||
public void setB(Boolean b) {
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
static class C3 {
|
||||
private Integer a;
|
||||
|
||||
public Integer getA() {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
public Boolean isA() {
|
||||
return this.a > 0;
|
||||
}
|
||||
|
||||
public void setA(Integer a) {
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateBooleanWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
B b = new B(true);
|
||||
B b2 = des.deserialize(ser.serialize(b, TestSerializationContext.testSerializationContext), B.class, TestSerializationContext.testSerializationContext);
|
||||
assertEquals (b.b, b2.b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateBooleanWithNoConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
B2 b = new B2();
|
||||
b.setB(false);
|
||||
B2 b2 = des.deserialize(ser.serialize(b, TestSerializationContext.testSerializationContext), B2.class, TestSerializationContext.testSerializationContext);
|
||||
assertEquals (b.b, b2.b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCapitilsationOfIs() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
B3 b = new B3();
|
||||
b.setB(false);
|
||||
B3 b2 = des.deserialize(ser.serialize(b, TestSerializationContext.testSerializationContext), B3.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
// since we can't find a getter for b (isb != isB) then we won't serialize that parameter
|
||||
assertNull (b2.b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateIntWithBoolean() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
C3 c = new C3();
|
||||
c.setA(12345);
|
||||
C3 c2 = des.deserialize(ser.serialize(c, TestSerializationContext.testSerializationContext), C3.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
C c = new C("dripping taps");
|
||||
C c2 = des.deserialize(ser.serialize(c, TestSerializationContext.testSerializationContext), C.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
|
||||
//
|
||||
// Now ensure we actually got a private property serializer
|
||||
//
|
||||
Field f = SerializerFactory.class.getDeclaredField("serializersByDescriptor");
|
||||
f.setAccessible(true);
|
||||
|
||||
Map<?, AMQPSerializer<?>> serializersByDescriptor = (Map<?, AMQPSerializer<?>>) f.get(factory);
|
||||
|
||||
assertEquals(1, serializersByDescriptor.size());
|
||||
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
|
||||
assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size());
|
||||
Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray();
|
||||
assertTrue (((PropertyAccessor)propertyReaders[0]).getGetter().getPropertyReader() instanceof PrivatePropertyReader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singlePrivateWithConstructorAndGetter()
|
||||
throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
|
||||
SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory);
|
||||
DeserializationInput des = new DeserializationInput(factory);
|
||||
|
||||
C2 c = new C2("dripping taps");
|
||||
C2 c2 = des.deserialize(ser.serialize(c, TestSerializationContext.testSerializationContext), C2.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals (c.a, c2.a);
|
||||
|
||||
//
|
||||
// Now ensure we actually got a private property serializer
|
||||
//
|
||||
Field f = SerializerFactory.class.getDeclaredField("serializersByDescriptor");
|
||||
f.setAccessible(true);
|
||||
Map<?, AMQPSerializer<?>> serializersByDescriptor = (Map<?, AMQPSerializer<?>>) f.get(factory);
|
||||
|
||||
assertEquals(1, serializersByDescriptor.size());
|
||||
ObjectSerializer cSerializer = ((ObjectSerializer)serializersByDescriptor.values().toArray()[0]);
|
||||
assertEquals(1, cSerializer.getPropertySerializers().getSerializationOrder().size());
|
||||
Object[] propertyReaders = cSerializer.getPropertySerializers().getSerializationOrder().toArray();
|
||||
assertTrue (((PropertyAccessor)propertyReaders[0]).getGetter().getPropertyReader() instanceof PublicPropertyReader);
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
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(),
|
||||
new EvolutionSerializerGetter(),
|
||||
new SerializerFingerPrinter());
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
SerializedBytes<Object> bytes = ser.serialize(bra, TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.core.serialization.ConstructorForDeserialization;
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.apache.qpid.proton.codec.DecoderImpl;
|
||||
import org.apache.qpid.proton.codec.EncoderImpl;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.NotSerializableException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class JavaSerializationOutputTests {
|
||||
|
||||
static class Foo {
|
||||
private final String bob;
|
||||
private final int count;
|
||||
|
||||
public Foo(String msg, long count) {
|
||||
this.bob = msg;
|
||||
this.count = (int) count;
|
||||
}
|
||||
|
||||
@ConstructorForDeserialization
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Foo foo = (Foo) o;
|
||||
|
||||
if (count != foo.count) return false;
|
||||
return bob != null ? bob.equals(foo.bob) : foo.bob == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = bob != null ? bob.hashCode() : 0;
|
||||
result = 31 * result + count;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static class UnAnnotatedFoo {
|
||||
private final String bob;
|
||||
private final 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
UnAnnotatedFoo foo = (UnAnnotatedFoo) o;
|
||||
|
||||
if (count != foo.count) return false;
|
||||
return bob != null ? bob.equals(foo.bob) : foo.bob == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = bob != null ? bob.hashCode() : 0;
|
||||
result = 31 * result + count;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static class BoxedFoo {
|
||||
private final String fred;
|
||||
private final Integer count;
|
||||
|
||||
private BoxedFoo(String fred, Integer count) {
|
||||
this.fred = fred;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public String getFred() {
|
||||
return fred;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Integer getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
BoxedFoo boxedFoo = (BoxedFoo) o;
|
||||
|
||||
if (fred != null ? !fred.equals(boxedFoo.fred) : boxedFoo.fred != null) return false;
|
||||
return count != null ? count.equals(boxedFoo.count) : boxedFoo.count == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = fred != null ? fred.hashCode() : 0;
|
||||
result = 31 * result + (count != null ? count.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class BoxedFooNotNull {
|
||||
private final String fred;
|
||||
private final Integer count;
|
||||
|
||||
private BoxedFooNotNull(String fred, Integer count) {
|
||||
this.fred = fred;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public String getFred() {
|
||||
return fred;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Integer getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
BoxedFooNotNull boxedFoo = (BoxedFooNotNull) o;
|
||||
|
||||
if (fred != null ? !fred.equals(boxedFoo.fred) : boxedFoo.fred != null) return false;
|
||||
return count != null ? count.equals(boxedFoo.count) : boxedFoo.count == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = fred != null ? fred.hashCode() : 0;
|
||||
result = 31 * result + (count != null ? count.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private Object serdes(Object obj) throws NotSerializableException {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
SerializedBytes<Object> bytes = ser.serialize(obj, TestSerializationContext.testSerializationContext);
|
||||
|
||||
DecoderImpl decoder = new DecoderImpl();
|
||||
|
||||
decoder.register(Envelope.Companion.getDESCRIPTOR(), Envelope.Companion);
|
||||
decoder.register(Schema.Companion.getDESCRIPTOR(), Schema.Companion);
|
||||
decoder.register(Descriptor.Companion.getDESCRIPTOR(), Descriptor.Companion);
|
||||
decoder.register(Field.Companion.getDESCRIPTOR(), Field.Companion);
|
||||
decoder.register(CompositeType.Companion.getDESCRIPTOR(), CompositeType.Companion);
|
||||
decoder.register(Choice.Companion.getDESCRIPTOR(), Choice.Companion);
|
||||
decoder.register(RestrictedType.Companion.getDESCRIPTOR(), RestrictedType.Companion);
|
||||
decoder.register(Transform.Companion.getDESCRIPTOR(), Transform.Companion);
|
||||
decoder.register(TransformsSchema.Companion.getDESCRIPTOR(), TransformsSchema.Companion);
|
||||
|
||||
new EncoderImpl(decoder);
|
||||
decoder.setByteBuffer(ByteBuffer.wrap(bytes.getBytes(), 8, bytes.getSize() - 8));
|
||||
Envelope result = (Envelope) decoder.readObject();
|
||||
assertTrue(result != null);
|
||||
|
||||
DeserializationInput des = new DeserializationInput(factory2);
|
||||
Object desObj = des.deserialize(bytes, Object.class, TestSerializationContext.testSerializationContext);
|
||||
assertTrue(Objects.deepEquals(obj, desObj));
|
||||
|
||||
// Now repeat with a re-used factory
|
||||
SerializationOutput ser2 = new SerializationOutput(factory1);
|
||||
DeserializationInput des2 = new DeserializationInput(factory1);
|
||||
Object desObj2 = des2.deserialize(ser2.serialize(obj, TestSerializationContext.testSerializationContext),
|
||||
Object.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertTrue(Objects.deepEquals(obj, desObj2));
|
||||
// TODO: check schema is as expected
|
||||
return desObj2;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJavaConstructorAnnotations() throws NotSerializableException {
|
||||
Foo obj = new Foo("Hello World!", 123);
|
||||
serdes(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJavaConstructorWithoutAnnotations() throws NotSerializableException {
|
||||
UnAnnotatedFoo obj = new UnAnnotatedFoo("Hello World!", 123);
|
||||
serdes(obj);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBoxedTypes() throws NotSerializableException {
|
||||
BoxedFoo obj = new BoxedFoo("Hello World!", 123);
|
||||
serdes(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBoxedTypesNotNull() throws NotSerializableException {
|
||||
BoxedFooNotNull obj = new BoxedFooNotNull("Hello World!", 123);
|
||||
serdes(obj);
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable;
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ListsSerializationJavaTest {
|
||||
|
||||
@CordaSerializable
|
||||
interface Parent {
|
||||
}
|
||||
|
||||
public static class Child implements Parent {
|
||||
private final int value;
|
||||
|
||||
Child(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Child child = (Child) o;
|
||||
|
||||
return value == child.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Needed to show that there is a property called "value"
|
||||
@SuppressWarnings("unused")
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
public static class CovariantContainer<T extends Parent> {
|
||||
private final List<T> content;
|
||||
|
||||
CovariantContainer(List<T> content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
CovariantContainer<T> that = (CovariantContainer<T>) o;
|
||||
|
||||
return content != null ? content.equals(that.content) : that.content == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return content != null ? content.hashCode() : 0;
|
||||
}
|
||||
|
||||
// Needed to show that there is a property called "content"
|
||||
@SuppressWarnings("unused")
|
||||
public List<T> getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
public static class CovariantContainer2 {
|
||||
private final List<? extends Parent> content;
|
||||
|
||||
CovariantContainer2(List<? extends Parent> content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
CovariantContainer2 that = (CovariantContainer2) o;
|
||||
|
||||
return content != null ? content.equals(that.content) : that.content == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return content != null ? content.hashCode() : 0;
|
||||
}
|
||||
|
||||
// Needed to show that there is a property called "content"
|
||||
@SuppressWarnings("unused")
|
||||
public List<? extends Parent> getContent() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkCovariance() throws Exception {
|
||||
List<Child> payload = new ArrayList<>();
|
||||
payload.add(new Child(1));
|
||||
payload.add(new Child(2));
|
||||
CovariantContainer<Child> container = new CovariantContainer<>(payload);
|
||||
assertEqualAfterRoundTripSerialization(container, CovariantContainer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkCovariance2() throws Exception {
|
||||
List<Child> payload = new ArrayList<>();
|
||||
payload.add(new Child(1));
|
||||
payload.add(new Child(2));
|
||||
CovariantContainer2 container = new CovariantContainer2(payload);
|
||||
assertEqualAfterRoundTripSerialization(container, CovariantContainer2.class);
|
||||
}
|
||||
|
||||
// Have to have own version as Kotlin inline functions cannot be easily called from Java
|
||||
private static <T> void assertEqualAfterRoundTripSerialization(T container, Class<T> clazz) throws Exception {
|
||||
EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerializerGetter,
|
||||
fingerPrinter);
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
SerializedBytes<Object> bytes = ser.serialize(container, TestSerializationContext.testSerializationContext);
|
||||
DeserializationInput des = new DeserializationInput(factory1);
|
||||
T deserialized = des.deserialize(bytes, clazz, TestSerializationContext.testSerializationContext);
|
||||
Assert.assertEquals(container, deserialized);
|
||||
}
|
||||
}
|
@ -1,365 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp;
|
||||
|
||||
import net.corda.core.serialization.SerializedBytes;
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist;
|
||||
import net.corda.nodeapi.internal.serialization.amqp.testutils.TestSerializationContext;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.NotSerializableException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SetterConstructorTests {
|
||||
|
||||
static class C {
|
||||
private int a;
|
||||
private int b;
|
||||
private int c;
|
||||
|
||||
public int getA() { return a; }
|
||||
public int getB() { return b; }
|
||||
public int getC() { return c; }
|
||||
|
||||
public void setA(int a) { this.a = a; }
|
||||
public void setB(int b) { this.b = b; }
|
||||
public void setC(int c) { this.c = c; }
|
||||
}
|
||||
|
||||
static class C2 {
|
||||
private int a;
|
||||
private int b;
|
||||
private int c;
|
||||
|
||||
public int getA() { return a; }
|
||||
public int getB() { return b; }
|
||||
public int getC() { return c; }
|
||||
|
||||
public void setA(int a) { this.a = a; }
|
||||
public void setB(int b) { this.b = b; }
|
||||
}
|
||||
|
||||
static class C3 {
|
||||
private int a;
|
||||
private int b;
|
||||
private int c;
|
||||
|
||||
public int getA() { return a; }
|
||||
public int getC() { return c; }
|
||||
|
||||
public void setA(int a) { this.a = a; }
|
||||
public void setB(int b) { this.b = b; }
|
||||
public void setC(int c) { this.c = c; }
|
||||
}
|
||||
|
||||
static class C4 {
|
||||
private int a;
|
||||
private int b;
|
||||
private int c;
|
||||
|
||||
public int getA() { return a; }
|
||||
protected int getB() { return b; }
|
||||
public int getC() { return c; }
|
||||
|
||||
private void setA(int a) { this.a = a; }
|
||||
public void setB(int b) { this.b = b; }
|
||||
public void setC(int c) { this.c = c; }
|
||||
}
|
||||
|
||||
static class CIntList {
|
||||
private List<Integer> l;
|
||||
|
||||
public List getL() { return l; }
|
||||
public void setL(List<Integer> l) { this.l = l; }
|
||||
}
|
||||
|
||||
static class Inner1 {
|
||||
private String a;
|
||||
|
||||
public Inner1(String a) { this.a = a; }
|
||||
public String getA() { return this.a; }
|
||||
}
|
||||
|
||||
static class Inner2 {
|
||||
private Double a;
|
||||
|
||||
public Double getA() { return this.a; }
|
||||
public void setA(Double a) { this.a = a; }
|
||||
}
|
||||
|
||||
static class Outer {
|
||||
private Inner1 a;
|
||||
private String b;
|
||||
private Inner2 c;
|
||||
|
||||
public Inner1 getA() { return a; }
|
||||
public String getB() { return b; }
|
||||
public Inner2 getC() { return c; }
|
||||
|
||||
public void setA(Inner1 a) { this.a = a; }
|
||||
public void setB(String b) { this.b = b; }
|
||||
public void setC(Inner2 c) { this.c = c; }
|
||||
}
|
||||
|
||||
static class TypeMismatch {
|
||||
private Integer a;
|
||||
|
||||
public void setA(Integer a) { this.a = a; }
|
||||
public String getA() { return this.a.toString(); }
|
||||
}
|
||||
|
||||
static class TypeMismatch2 {
|
||||
private Integer a;
|
||||
|
||||
public void setA(String a) { this.a = Integer.parseInt(a); }
|
||||
public Integer getA() { return this.a; }
|
||||
}
|
||||
|
||||
// despite having no constructor we should still be able to serialise an instance of C
|
||||
@Test
|
||||
public void serialiseC() throws NotSerializableException {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
|
||||
C c1 = new C();
|
||||
c1.setA(1);
|
||||
c1.setB(2);
|
||||
c1.setC(3);
|
||||
Schema schemas = ser.serializeAndReturnSchema(c1, TestSerializationContext.testSerializationContext).component2();
|
||||
assertEquals(1, schemas.component1().size());
|
||||
assertEquals(this.getClass().getName() + "$C", schemas.component1().get(0).getName());
|
||||
|
||||
CompositeType ct = (CompositeType) schemas.component1().get(0);
|
||||
|
||||
assertEquals(3, ct.getFields().size());
|
||||
assertEquals("a", ct.getFields().get(0).getName());
|
||||
assertEquals("b", ct.getFields().get(1).getName());
|
||||
assertEquals("c", ct.getFields().get(2).getName());
|
||||
|
||||
// No setter for c so should only serialise two properties
|
||||
C2 c2 = new C2();
|
||||
c2.setA(1);
|
||||
c2.setB(2);
|
||||
schemas = ser.serializeAndReturnSchema(c2, TestSerializationContext.testSerializationContext).component2();
|
||||
|
||||
assertEquals(1, schemas.component1().size());
|
||||
assertEquals(this.getClass().getName() + "$C2", schemas.component1().get(0).getName());
|
||||
|
||||
ct = (CompositeType) schemas.component1().get(0);
|
||||
|
||||
// With no setter for c we should only serialise a and b into the stream
|
||||
assertEquals(2, ct.getFields().size());
|
||||
assertEquals("a", ct.getFields().get(0).getName());
|
||||
assertEquals("b", ct.getFields().get(1).getName());
|
||||
|
||||
// no getter for b so shouldn't serialise it,thus only a and c should apper in the envelope
|
||||
C3 c3 = new C3();
|
||||
c3.setA(1);
|
||||
c3.setB(2);
|
||||
c3.setC(3);
|
||||
schemas = ser.serializeAndReturnSchema(c3, TestSerializationContext.testSerializationContext).component2();
|
||||
|
||||
assertEquals(1, schemas.component1().size());
|
||||
assertEquals(this.getClass().getName() + "$C3", schemas.component1().get(0).getName());
|
||||
|
||||
ct = (CompositeType) schemas.component1().get(0);
|
||||
|
||||
// With no setter for c we should only serialise a and b into the stream
|
||||
assertEquals(2, ct.getFields().size());
|
||||
assertEquals("a", ct.getFields().get(0).getName());
|
||||
assertEquals("c", ct.getFields().get(1).getName());
|
||||
|
||||
C4 c4 = new C4();
|
||||
c4.setA(1);
|
||||
c4.setB(2);
|
||||
c4.setC(3);
|
||||
schemas = ser.serializeAndReturnSchema(c4, TestSerializationContext.testSerializationContext).component2();
|
||||
|
||||
assertEquals(1, schemas.component1().size());
|
||||
assertEquals(this.getClass().getName() + "$C4", schemas.component1().get(0).getName());
|
||||
|
||||
ct = (CompositeType) schemas.component1().get(0);
|
||||
|
||||
// With non public visibility on a setter and getter for a and b, only c should be serialised
|
||||
assertEquals(1, ct.getFields().size());
|
||||
assertEquals("c", ct.getFields().get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deserialiseC() throws NotSerializableException {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
C cPre1 = new C();
|
||||
|
||||
int a = 1;
|
||||
int b = 2;
|
||||
int c = 3;
|
||||
|
||||
cPre1.setA(a);
|
||||
cPre1.setB(b);
|
||||
cPre1.setC(c);
|
||||
|
||||
SerializedBytes bytes = new SerializationOutput(factory1).serialize(cPre1, TestSerializationContext.testSerializationContext);
|
||||
|
||||
C cPost1 = new DeserializationInput(factory1).deserialize(bytes, C.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals(a, cPost1.a);
|
||||
assertEquals(b, cPost1.b);
|
||||
assertEquals(c, cPost1.c);
|
||||
|
||||
C2 cPre2 = new C2();
|
||||
cPre2.setA(1);
|
||||
cPre2.setB(2);
|
||||
|
||||
C2 cPost2 = new DeserializationInput(factory1).deserialize(new SerializationOutput(factory1).serialize(cPre2, TestSerializationContext.testSerializationContext),
|
||||
C2.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals(a, cPost2.a);
|
||||
assertEquals(b, cPost2.b);
|
||||
|
||||
// no setter for c means nothing will be serialised and thus it will have the default value of zero
|
||||
// set
|
||||
assertEquals(0, cPost2.c);
|
||||
|
||||
C3 cPre3 = new C3();
|
||||
cPre3.setA(1);
|
||||
cPre3.setB(2);
|
||||
cPre3.setC(3);
|
||||
|
||||
C3 cPost3 = new DeserializationInput(factory1).deserialize(
|
||||
new SerializationOutput(factory1).serialize(cPre3, TestSerializationContext.testSerializationContext),
|
||||
C3.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals(a, cPost3.a);
|
||||
|
||||
// no getter for b means, as before, it'll have been not set and will thus be defaulted to 0
|
||||
assertEquals(0, cPost3.b);
|
||||
assertEquals(c, cPost3.c);
|
||||
|
||||
C4 cPre4 = new C4();
|
||||
cPre4.setA(1);
|
||||
cPre4.setB(2);
|
||||
cPre4.setC(3);
|
||||
|
||||
C4 cPost4 = new DeserializationInput(factory1).deserialize(
|
||||
new SerializationOutput(factory1).serialize(cPre4,
|
||||
TestSerializationContext.testSerializationContext),
|
||||
C4.class,
|
||||
TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals(0, cPost4.a);
|
||||
assertEquals(0, cPost4.b);
|
||||
assertEquals(c, cPost4.c);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serialiseOuterAndInner() throws NotSerializableException {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
Inner1 i1 = new Inner1("Hello");
|
||||
Inner2 i2 = new Inner2();
|
||||
i2.setA(10.5);
|
||||
|
||||
Outer o = new Outer();
|
||||
o.setA(i1);
|
||||
o.setB("World");
|
||||
o.setC(i2);
|
||||
|
||||
Outer post = new DeserializationInput(factory1).deserialize(
|
||||
new SerializationOutput(factory1).serialize(
|
||||
o, TestSerializationContext.testSerializationContext),
|
||||
Outer.class, TestSerializationContext.testSerializationContext);
|
||||
|
||||
assertEquals("Hello", post.a.a);
|
||||
assertEquals("World", post.b);
|
||||
assertEquals((Double)10.5, post.c.a);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeMistmatch() {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
TypeMismatch tm = new TypeMismatch();
|
||||
tm.setA(10);
|
||||
assertEquals("10", tm.getA());
|
||||
|
||||
Assertions.assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm,
|
||||
TestSerializationContext.testSerializationContext)).isInstanceOf (
|
||||
NotSerializableException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeMistmatch2() {
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
TypeMismatch2 tm = new TypeMismatch2();
|
||||
tm.setA("10");
|
||||
assertEquals((Integer)10, tm.getA());
|
||||
|
||||
Assertions.assertThatThrownBy(() -> new SerializationOutput(factory1).serialize(tm,
|
||||
TestSerializationContext.testSerializationContext)).isInstanceOf(
|
||||
NotSerializableException.class);
|
||||
}
|
||||
|
||||
// This not blowing up means it's working
|
||||
@Test
|
||||
public void intList() throws NotSerializableException {
|
||||
CIntList cil = new CIntList();
|
||||
|
||||
List<Integer> l = new ArrayList<>();
|
||||
l.add(1);
|
||||
l.add(2);
|
||||
l.add(3);
|
||||
|
||||
cil.setL(l);
|
||||
|
||||
EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
|
||||
FingerPrinter fingerPrinter = new SerializerFingerPrinter();
|
||||
SerializerFactory factory1 = new SerializerFactory(
|
||||
AllWhitelist.INSTANCE,
|
||||
ClassLoader.getSystemClassLoader(),
|
||||
evolutionSerialiserGetter,
|
||||
fingerPrinter);
|
||||
|
||||
// if we've got super / sub types on the setter vs the underlying type the wrong way around this will
|
||||
// explode. See CORDA-1229 (https://r3-cev.atlassian.net/browse/CORDA-1229)
|
||||
new DeserializationInput(factory1).deserialize(
|
||||
new SerializationOutput(factory1).serialize(
|
||||
cil, TestSerializationContext.testSerializationContext),
|
||||
CIntList.class,
|
||||
TestSerializationContext.testSerializationContext);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user