mirror of
https://github.com/corda/corda.git
synced 2025-06-17 06:38:21 +00:00
Merge from Corda master
This commit is contained in:
@ -68,7 +68,7 @@ object RPCApi {
|
||||
|
||||
val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION =
|
||||
"${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " +
|
||||
"${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'"
|
||||
"${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'"
|
||||
val RPC_CLIENT_BINDING_ADDITION_FILTER_EXPRESSION =
|
||||
"${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_ADDED.name}' AND " +
|
||||
"${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'"
|
||||
@ -97,20 +97,20 @@ object RPCApi {
|
||||
* @param clientAddress return address to contact the client at.
|
||||
* @param id a unique ID for the request, which the server will use to identify its response with.
|
||||
* @param methodName name of the method (procedure) to be called.
|
||||
* @param arguments arguments to pass to the method, if any.
|
||||
* @param serialisedArguments Serialised arguments to pass to the method, if any.
|
||||
*/
|
||||
data class RpcRequest(
|
||||
val clientAddress: SimpleString,
|
||||
val id: RpcRequestId,
|
||||
val methodName: String,
|
||||
val arguments: List<Any?>
|
||||
val serialisedArguments: ByteArray
|
||||
) : ClientToServer() {
|
||||
fun writeToClientMessage(context: SerializationContext, message: ClientMessage) {
|
||||
fun writeToClientMessage(message: ClientMessage) {
|
||||
MessageUtil.setJMSReplyTo(message, clientAddress)
|
||||
message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REQUEST.ordinal)
|
||||
message.putLongProperty(RPC_ID_FIELD_NAME, id.toLong)
|
||||
message.putStringProperty(METHOD_NAME_FIELD_NAME, methodName)
|
||||
message.bodyBuffer.writeBytes(arguments.serialize(context = context).bytes)
|
||||
message.bodyBuffer.writeBytes(serialisedArguments)
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,20 +128,20 @@ object RPCApi {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromClientMessage(context: SerializationContext, message: ClientMessage): ClientToServer {
|
||||
fun fromClientMessage(message: ClientMessage): ClientToServer {
|
||||
val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)]
|
||||
return when (tag) {
|
||||
RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest(
|
||||
clientAddress = MessageUtil.getJMSReplyTo(message),
|
||||
id = RpcRequestId(message.getLongProperty(RPC_ID_FIELD_NAME)),
|
||||
methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME),
|
||||
arguments = message.getBodyAsByteArray().deserialize(context = context)
|
||||
serialisedArguments = message.getBodyAsByteArray()
|
||||
)
|
||||
RPCApi.ClientToServer.Tag.OBSERVABLES_CLOSED -> {
|
||||
val ids = ArrayList<ObservableId>()
|
||||
val buffer = message.bodyBuffer
|
||||
val numberOfIds = buffer.readInt()
|
||||
for (i in 1 .. numberOfIds) {
|
||||
for (i in 1..numberOfIds) {
|
||||
ids.add(ObservableId(buffer.readLong()))
|
||||
}
|
||||
ObservablesClosed(ids)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.nodeapi
|
||||
|
||||
import net.corda.nodeapi.config.OldConfig
|
||||
import net.corda.nodeapi.config.toConfig
|
||||
|
||||
data class User(
|
||||
@OldConfig("user")
|
||||
@ -8,9 +9,6 @@ data class User(
|
||||
val password: String,
|
||||
val permissions: Set<String>) {
|
||||
override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)"
|
||||
fun toMap() = mapOf(
|
||||
"username" to username,
|
||||
"password" to password,
|
||||
"permissions" to permissions
|
||||
)
|
||||
@Deprecated("Use toConfig().root().unwrapped() instead")
|
||||
fun toMap(): Map<String, Any> = toConfig().root().unwrapped()
|
||||
}
|
||||
|
@ -1,18 +1,26 @@
|
||||
@file:JvmName("ConfigUtilities")
|
||||
|
||||
package net.corda.nodeapi.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigUtil
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Modifier.isStatic
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
@ -25,7 +33,7 @@ import kotlin.reflect.jvm.jvmErasure
|
||||
annotation class OldConfig(val value: String)
|
||||
|
||||
// TODO Move other config parsing to use parseAs and remove this
|
||||
operator fun <T> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||
return getValueInternal(metadata.name, metadata.returnType)
|
||||
}
|
||||
|
||||
@ -52,9 +60,8 @@ fun Config.toProperties(): Properties {
|
||||
{ it.value.unwrapped().toString() })
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> Config.getValueInternal(path: String, type: KType): T {
|
||||
return (if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) as T
|
||||
private fun <T : Any> Config.getValueInternal(path: String, type: KType): T {
|
||||
return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type))
|
||||
}
|
||||
|
||||
private fun Config.getSingleValue(path: String, type: KType): Any? {
|
||||
@ -71,8 +78,8 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
|
||||
NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path))
|
||||
Path::class -> Paths.get(getString(path))
|
||||
URL::class -> URL(getString(path))
|
||||
Properties::class -> getConfig(path).toProperties()
|
||||
CordaX500Name::class -> CordaX500Name.parse(getString(path))
|
||||
Properties::class -> getConfig(path).toProperties()
|
||||
else -> if (typeClass.java.isEnum) {
|
||||
parseEnum(typeClass.java, getString(path))
|
||||
} else {
|
||||
@ -122,9 +129,69 @@ private fun Config.defaultToOldPath(property: KProperty<*>): String {
|
||||
return property.name
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(enumType as Class<Proxy.Type>, name) // Any enum will do
|
||||
private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge<Proxy.Type>(uncheckedCast(enumType), name) // Any enum will do
|
||||
|
||||
private fun <T : Enum<T>> enumBridge(clazz: Class<T>, name: String): T = java.lang.Enum.valueOf(clazz, name)
|
||||
|
||||
/**
|
||||
* Convert the receiver object into a [Config]. This does the inverse action of [parseAs].
|
||||
*/
|
||||
fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig()
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
// Reflect over the fields of the receiver and generate a value Map that can use to create Config object.
|
||||
private fun Any.toConfigMap(): Map<String, Any> {
|
||||
val values = HashMap<String, Any>()
|
||||
for (field in javaClass.declaredFields) {
|
||||
if (isStatic(field.modifiers) || field.isSynthetic) continue
|
||||
field.isAccessible = true
|
||||
val value = field.get(this) ?: continue
|
||||
val configValue = if (value is String || value is Boolean || value is Number) {
|
||||
// These types are supported by Config as use as is
|
||||
value
|
||||
} else if (value is Temporal || value is NetworkHostAndPort || value is CordaX500Name || value is Path || value is URL) {
|
||||
// These types make sense to be represented as Strings and the exact inverse parsing function for use in parseAs
|
||||
value.toString()
|
||||
} else if (value is Enum<*>) {
|
||||
// Expicitly use the Enum's name in case the toString is overridden, which would make parsing problematic.
|
||||
value.name
|
||||
} else if (value is Properties) {
|
||||
// For Properties we treat keys with . as nested configs
|
||||
ConfigFactory.parseMap(uncheckedCast(value)).root()
|
||||
} else if (value is Iterable<*>) {
|
||||
value.toConfigIterable(field)
|
||||
} else {
|
||||
// Else this is a custom object recursed over
|
||||
value.toConfigMap()
|
||||
}
|
||||
values[field.name] = configValue
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// For Iterables figure out the type parameter and apply the same logic as above on the individual elements.
|
||||
private fun Iterable<*>.toConfigIterable(field: Field): Iterable<Any?> {
|
||||
val elementType = (field.genericType as ParameterizedType).actualTypeArguments[0] as Class<*>
|
||||
return when (elementType) {
|
||||
// For the types already supported by Config we can use the Iterable as is
|
||||
String::class.java -> this
|
||||
Integer::class.java -> this
|
||||
java.lang.Long::class.java -> this
|
||||
java.lang.Double::class.java -> this
|
||||
java.lang.Boolean::class.java -> this
|
||||
LocalDate::class.java -> map(Any?::toString)
|
||||
Instant::class.java -> map(Any?::toString)
|
||||
NetworkHostAndPort::class.java -> map(Any?::toString)
|
||||
Path::class.java -> map(Any?::toString)
|
||||
URL::class.java -> map(Any?::toString)
|
||||
CordaX500Name::class.java -> map(Any?::toString)
|
||||
Properties::class.java -> map { ConfigFactory.parseMap(uncheckedCast(it)).root() }
|
||||
else -> if (elementType.isEnum) {
|
||||
map { (it as Enum<*>).name }
|
||||
} else {
|
||||
map { it?.toConfigMap() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger("net.corda.nodeapi.config")
|
||||
|
@ -72,7 +72,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
val stream = ByteArrayOutputStream()
|
||||
try {
|
||||
attachment.extractFile(path, stream)
|
||||
} catch(e: FileNotFoundException) {
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw ClassNotFoundException(name)
|
||||
}
|
||||
val bytes = stream.toByteArray()
|
||||
@ -99,7 +99,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
val stream = ByteArrayOutputStream()
|
||||
attachment.extractFile(path, stream)
|
||||
return ByteArrayInputStream(stream.toByteArray())
|
||||
} catch(e: FileNotFoundException) {
|
||||
} catch (e: FileNotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ class ServiceType private constructor(val id: String) {
|
||||
}
|
||||
|
||||
val notary: ServiceType = corda.getSubType("notary")
|
||||
val networkMap: ServiceType = corda.getSubType("network_map")
|
||||
|
||||
fun parse(id: String): ServiceType = ServiceType(id)
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
|
||||
@ -14,12 +13,6 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0
|
||||
|
||||
class AMQPSerializationCustomization(val factory: SerializerFactory) : SerializationCustomization {
|
||||
override fun addToWhitelist(vararg types: Class<*>) {
|
||||
factory.addToWhitelist(*types)
|
||||
}
|
||||
}
|
||||
|
||||
fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
|
||||
require(types.toSet().size == types.size) {
|
||||
val duplicates = types.toMutableList()
|
||||
@ -33,13 +26,14 @@ fun SerializerFactory.addToWhitelist(vararg types: Class<*>) {
|
||||
|
||||
abstract class AbstractAMQPSerializationScheme : SerializationScheme {
|
||||
internal companion object {
|
||||
private val pluginRegistries: List<CordaPluginRegistry> by lazy {
|
||||
ServiceLoader.load(CordaPluginRegistry::class.java, this::class.java.classLoader).toList()
|
||||
private val serializationWhitelists: List<SerializationWhitelist> by lazy {
|
||||
ServiceLoader.load(SerializationWhitelist::class.java, this::class.java.classLoader).toList() + DefaultWhitelist
|
||||
}
|
||||
|
||||
fun registerCustomSerializers(factory: SerializerFactory) {
|
||||
factory.apply {
|
||||
with(factory) {
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.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.X500NameSerializer)
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer)
|
||||
@ -67,8 +61,8 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme {
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.BitSetSerializer(this))
|
||||
register(net.corda.nodeapi.internal.serialization.amqp.custom.EnumSetSerializer(this))
|
||||
}
|
||||
val customizer = AMQPSerializationCustomization(factory)
|
||||
pluginRegistries.forEach { it.customizeSerialization(customizer) }
|
||||
for (whitelistProvider in serializationWhitelists)
|
||||
factory.addToWhitelist(*whitelistProvider.whitelist.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,9 @@ import kotlin.collections.LinkedHashSet
|
||||
* 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 {
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("ClientContexts")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
|
@ -11,6 +11,7 @@ import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.internal.serialization.amqp.hasAnnotationInHierarchy
|
||||
import java.io.PrintWriter
|
||||
import java.lang.reflect.Modifier.isAbstract
|
||||
import java.nio.charset.StandardCharsets
|
||||
@ -30,9 +31,9 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
|
||||
* 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
|
||||
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
|
||||
@ -56,14 +57,13 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
|
||||
private fun checkClass(type: Class<*>): Registration? {
|
||||
// If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking.
|
||||
if (!whitelistEnabled) return null
|
||||
// Allow primitives, abstracts and interfaces
|
||||
if (type.isPrimitive || type == Any::class.java || isAbstract(type.modifiers) || type == String::class.java) 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)
|
||||
// Kotlin lambdas require some special treatment
|
||||
if (kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type)) return null
|
||||
// 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.
|
||||
@ -115,13 +115,7 @@ class CordaClassResolver(serializationContext: SerializationContext) : DefaultCl
|
||||
return (type.classLoader !is AttachmentsClassLoader)
|
||||
&& !KryoSerializable::class.java.isAssignableFrom(type)
|
||||
&& !type.isAnnotationPresent(DefaultSerializer::class.java)
|
||||
&& (type.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(type))
|
||||
}
|
||||
|
||||
// Recursively check interfaces for our annotation.
|
||||
private fun hasInheritedAnnotation(type: Class<*>): Boolean {
|
||||
return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(it) }
|
||||
|| (type.superclass != null && hasInheritedAnnotation(type.superclass))
|
||||
&& (type.isAnnotationPresent(CordaSerializable::class.java) || whitelist.hasAnnotationInHierarchy(type))
|
||||
}
|
||||
|
||||
// Need to clear out class names from attachments.
|
||||
@ -157,14 +151,15 @@ object AllWhitelist : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>): Boolean = true
|
||||
}
|
||||
|
||||
// TODO: Need some concept of from which class loader
|
||||
class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate {
|
||||
companion object {
|
||||
val whitelist: MutableSet<String> = Collections.synchronizedSet(mutableSetOf())
|
||||
}
|
||||
sealed class AbstractMutableClassWhitelist(private val whitelist: MutableSet<String>, private val delegate: ClassWhitelist) : MutableClassWhitelist {
|
||||
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
return (type.name in whitelist) || delegate.hasListed(type)
|
||||
/**
|
||||
* 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<*>) {
|
||||
@ -172,21 +167,17 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass
|
||||
}
|
||||
}
|
||||
|
||||
// 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.node.CordaPluginRegistry], since implements [MutableClassWhitelist].
|
||||
* A whitelist that can be customised via the [net.corda.core.node.SerializationWhitelist], since it implements [MutableClassWhitelist].
|
||||
*/
|
||||
class TransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate {
|
||||
val whitelist: MutableSet<String> = Collections.synchronizedSet(mutableSetOf())
|
||||
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
return (type.name in whitelist) || delegate.hasListed(type)
|
||||
}
|
||||
|
||||
override fun add(entry: Class<*>) {
|
||||
whitelist += entry.name
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -204,7 +195,7 @@ class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true)
|
||||
if (fileName != null && fileName.isNotEmpty()) {
|
||||
try {
|
||||
return PrintWriter(Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE), true)
|
||||
} catch(ioEx: Exception) {
|
||||
} catch (ioEx: Exception) {
|
||||
log.error("Could not open/create whitelist journal file for append: $fileName", ioEx)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
@ -49,8 +49,8 @@ import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
object DefaultKryoCustomizer {
|
||||
private val pluginRegistries: List<CordaPluginRegistry> by lazy {
|
||||
ServiceLoader.load(CordaPluginRegistry::class.java, this.javaClass.classLoader).toList()
|
||||
private val serializationWhitelists: List<SerializationWhitelist> by lazy {
|
||||
ServiceLoader.load(SerializationWhitelist::class.java, this.javaClass.classLoader).toList() + DefaultWhitelist
|
||||
}
|
||||
|
||||
fun customize(kryo: Kryo): Kryo {
|
||||
@ -88,10 +88,10 @@ object DefaultKryoCustomizer {
|
||||
register(BufferedInputStream::class.java, InputStreamSerializer)
|
||||
register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer)
|
||||
noReferencesWithin<WireTransaction>()
|
||||
register(ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer)
|
||||
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
|
||||
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
|
||||
register(CompositeKey::class.java, CompositeKeySerializer) // Using a custom serializer for compactness
|
||||
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.
|
||||
@ -109,7 +109,6 @@ object DefaultKryoCustomizer {
|
||||
register(BCRSAPublicKey::class.java, PublicKeySerializer)
|
||||
register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer)
|
||||
register(BCSphincs256PublicKey::class.java, PublicKeySerializer)
|
||||
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
|
||||
register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer)
|
||||
register(PartyAndCertificate::class.java, PartyAndCertificateSerializer)
|
||||
|
||||
@ -122,8 +121,17 @@ object DefaultKryoCustomizer {
|
||||
register(java.lang.invoke.SerializedLambda::class.java)
|
||||
register(ClosureSerializer.Closure::class.java, CordaClosureBlacklistSerializer)
|
||||
|
||||
val customization = KryoSerializationCustomization(this)
|
||||
pluginRegistries.forEach { it.customizeSerialization(customization) }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,6 +140,7 @@ object DefaultKryoCustomizer {
|
||||
// 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
|
||||
@ -143,6 +152,7 @@ object DefaultKryoCustomizer {
|
||||
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)
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.SerializationCustomization
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import rx.Notification
|
||||
@ -12,10 +11,9 @@ import java.util.*
|
||||
/**
|
||||
* NOTE: We do not whitelist [HashMap] or [HashSet] since they are unstable under serialization.
|
||||
*/
|
||||
class DefaultWhitelist : CordaPluginRegistry() {
|
||||
override fun customizeSerialization(custom: SerializationCustomization): Boolean {
|
||||
custom.apply {
|
||||
addToWhitelist(Array<Any>(0, {}).javaClass,
|
||||
object DefaultWhitelist : SerializationWhitelist {
|
||||
override val whitelist =
|
||||
listOf(Array<Any>(0, {}).javaClass,
|
||||
Notification::class.java,
|
||||
Notification.Kind::class.java,
|
||||
ArrayList::class.java,
|
||||
@ -60,8 +58,6 @@ class DefaultWhitelist : CordaPluginRegistry() {
|
||||
java.util.LinkedHashMap::class.java,
|
||||
BitSet::class.java,
|
||||
OnErrorNotImplementedException::class.java,
|
||||
StackTraceElement::class.java)
|
||||
}
|
||||
return true
|
||||
}
|
||||
StackTraceElement::class.java
|
||||
)
|
||||
}
|
||||
|
@ -10,11 +10,12 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.*
|
||||
import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.toFuture
|
||||
@ -24,20 +25,12 @@ import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.SgxSupport
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import org.bouncycastle.asn1.ASN1InputStream
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
import sun.security.ec.ECPublicKeyImpl
|
||||
import sun.security.util.DerValue
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
@ -253,9 +246,8 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
kryo.writeClassAndObject(output, obj.privacySalt)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<WireTransaction>): WireTransaction {
|
||||
val componentGroups = kryo.readClassAndObject(input) as List<ComponentGroup>
|
||||
val componentGroups: List<ComponentGroup> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val privacySalt = kryo.readClassAndObject(input) as PrivacySalt
|
||||
return WireTransaction(componentGroups, privacySalt)
|
||||
}
|
||||
@ -269,9 +261,8 @@ object NotaryChangeWireTransactionSerializer : Serializer<NotaryChangeWireTransa
|
||||
kryo.writeClassAndObject(output, obj.newNotary)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<NotaryChangeWireTransaction>): NotaryChangeWireTransaction {
|
||||
val inputs = kryo.readClassAndObject(input) as List<StateRef>
|
||||
val inputs: List<StateRef> = uncheckedCast(kryo.readClassAndObject(input))
|
||||
val notary = kryo.readClassAndObject(input) as Party
|
||||
val newNotary = kryo.readClassAndObject(input) as Party
|
||||
|
||||
@ -286,78 +277,24 @@ object SignedTransactionSerializer : Serializer<SignedTransaction>() {
|
||||
kryo.writeClassAndObject(output, obj.sigs)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<SignedTransaction>): SignedTransaction {
|
||||
return SignedTransaction(
|
||||
kryo.readClassAndObject(input) as SerializedBytes<CoreTransaction>,
|
||||
kryo.readClassAndObject(input) as List<TransactionSignature>
|
||||
uncheckedCast<Any?, SerializedBytes<CoreTransaction>>(kryo.readClassAndObject(input)),
|
||||
uncheckedCast<Any?, List<TransactionSignature>>(kryo.readClassAndObject(input))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** For serialising an ed25519 private key */
|
||||
@ThreadSafe
|
||||
object Ed25519PrivateKeySerializer : Serializer<EdDSAPrivateKey>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: EdDSAPrivateKey) {
|
||||
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
|
||||
output.writeBytesWithLength(obj.seed)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<EdDSAPrivateKey>): EdDSAPrivateKey {
|
||||
val seed = input.readBytesWithLength()
|
||||
return EdDSAPrivateKey(EdDSAPrivateKeySpec(seed, Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec))
|
||||
}
|
||||
}
|
||||
|
||||
/** For serialising an ed25519 public key */
|
||||
@ThreadSafe
|
||||
object Ed25519PublicKeySerializer : Serializer<EdDSAPublicKey>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: EdDSAPublicKey) {
|
||||
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
|
||||
output.writeBytesWithLength(obj.abyte)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<EdDSAPublicKey>): EdDSAPublicKey {
|
||||
val A = input.readBytesWithLength()
|
||||
return EdDSAPublicKey(EdDSAPublicKeySpec(A, Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec))
|
||||
}
|
||||
}
|
||||
|
||||
/** For serialising an ed25519 public key */
|
||||
@ThreadSafe
|
||||
object ECPublicKeyImplSerializer : Serializer<ECPublicKeyImpl>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: ECPublicKeyImpl) {
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<ECPublicKeyImpl>): ECPublicKeyImpl {
|
||||
val A = input.readBytesWithLength()
|
||||
val der = DerValue(A)
|
||||
return ECPublicKeyImpl.parse(der) as ECPublicKeyImpl
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249.
|
||||
@ThreadSafe
|
||||
object CompositeKeySerializer : Serializer<CompositeKey>() {
|
||||
override fun write(kryo: Kryo, output: Output, obj: CompositeKey) {
|
||||
output.writeInt(obj.threshold)
|
||||
output.writeInt(obj.children.size)
|
||||
obj.children.forEach { kryo.writeClassAndObject(output, it) }
|
||||
}
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<CompositeKey>): CompositeKey {
|
||||
val threshold = input.readInt()
|
||||
val children = readListOfLength<CompositeKey.NodeAndWeight>(kryo, input, minLen = 2)
|
||||
val builder = CompositeKey.Builder()
|
||||
children.forEach { builder.addKey(it.node, it.weight) }
|
||||
return builder.build(threshold) as CompositeKey
|
||||
sealed class UseCaseSerializer<T>(private val allowedUseCases: EnumSet<SerializationContext.UseCase>) : Serializer<T>() {
|
||||
protected fun checkUseCase() {
|
||||
checkUseCase(allowedUseCases)
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
object PrivateKeySerializer : Serializer<PrivateKey>() {
|
||||
object PrivateKeySerializer : UseCaseSerializer<PrivateKey>(EnumSet.of(Storage, Checkpoint)) {
|
||||
override fun write(kryo: Kryo, output: Output, obj: PrivateKey) {
|
||||
checkUseCase()
|
||||
output.writeBytesWithLength(obj.encoded)
|
||||
}
|
||||
|
||||
@ -531,7 +468,7 @@ object LoggerSerializer : Serializer<Logger>() {
|
||||
object ClassSerializer : Serializer<Class<*>>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Class<*>>): Class<*> {
|
||||
val className = input.readString()
|
||||
return Class.forName(className)
|
||||
return Class.forName(className, true, kryo.classLoader)
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, clazz: Class<*>) {
|
||||
@ -608,8 +545,7 @@ class ThrowableSerializer<T>(kryo: Kryo, type: Class<T>) : Serializer<Throwable>
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val delegate: Serializer<Throwable> = ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type) as Serializer<Throwable>
|
||||
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)
|
||||
@ -617,7 +553,7 @@ class ThrowableSerializer<T>(kryo: Kryo, type: Class<T>) : Serializer<Throwable>
|
||||
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Throwable>): Throwable {
|
||||
val throwableRead = delegate.read(kryo, input, type)
|
||||
if(throwableRead.suppressed.isEmpty()) {
|
||||
if (throwableRead.suppressed.isEmpty()) {
|
||||
throwableRead.setSuppressedToSentinel()
|
||||
}
|
||||
return throwableRead
|
||||
|
@ -1,17 +0,0 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import net.corda.core.serialization.SerializationCustomization
|
||||
|
||||
class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization {
|
||||
override fun 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) {
|
||||
((kryo.classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type)
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ import com.google.common.cache.CacheBuilder
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
@ -26,7 +27,7 @@ import java.util.concurrent.ExecutionException
|
||||
|
||||
val attachmentsClassLoaderEnabledPropertyName = "attachments.class.loader.enabled"
|
||||
|
||||
object NotSupportedSeralizationScheme : SerializationScheme {
|
||||
object NotSupportedSerializationScheme : SerializationScheme {
|
||||
private fun doThrow(): Nothing = throw UnsupportedOperationException("Serialization scheme not supported.")
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean = doThrow()
|
||||
@ -51,7 +52,7 @@ data class SerializationContextImpl(override val preferredSerializationVersion:
|
||||
* We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context.
|
||||
*/
|
||||
override fun withAttachmentsClassLoader(attachmentHashes: List<SecureHash>): SerializationContext {
|
||||
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean ?: false || return this
|
||||
properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this
|
||||
val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContextImpl ?: return this // Some tests don't set one.
|
||||
try {
|
||||
return withClassLoader(cache.get(attachmentHashes) {
|
||||
@ -106,7 +107,7 @@ open class SerializationFactoryImpl : SerializationFactory() {
|
||||
registeredSchemes
|
||||
.filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) }
|
||||
.forEach { return@computeIfAbsent it }
|
||||
NotSupportedSeralizationScheme
|
||||
NotSupportedSerializationScheme
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,8 +142,8 @@ open class SerializationFactoryImpl : SerializationFactory() {
|
||||
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."
|
||||
"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)
|
||||
}
|
||||
|
||||
@ -204,11 +205,10 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
Input(byteSequence.bytes, byteSequence.offset + headerSize, byteSequence.size - headerSize).use { input ->
|
||||
return pool.run { kryo ->
|
||||
withContext(kryo, context) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (context.objectReferencesEnabled) {
|
||||
kryo.readClassAndObject(input) as T
|
||||
uncheckedCast(kryo.readClassAndObject(input))
|
||||
} else {
|
||||
kryo.withoutReferences { kryo.readClassAndObject(input) as T }
|
||||
kryo.withoutReferences { uncheckedCast<Any?, T>(kryo.readClassAndObject(input)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("ServerContexts")
|
||||
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
|
@ -0,0 +1,12 @@
|
||||
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'")
|
||||
}
|
||||
}
|
@ -23,14 +23,13 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
|
||||
//
|
||||
// We *need* to retain knowledge for AMQP deserialisation 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"
|
||||
}
|
||||
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 by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") }
|
||||
internal val elementType: Type by lazy { type.componentType() }
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
@ -34,12 +35,10 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
|
||||
}
|
||||
|
||||
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
|
||||
if(supportedTypes.containsKey(declaredClass)) {
|
||||
if (supportedTypes.containsKey(declaredClass)) {
|
||||
// Simple case - it is already known to be a collection.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return deriveParametrizedType(declaredType, declaredClass as Class<out Collection<*>>)
|
||||
}
|
||||
else if (actualClass != null && Collection::class.java.isAssignableFrom(actualClass)) {
|
||||
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)
|
||||
@ -49,11 +48,11 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
|
||||
}
|
||||
|
||||
private fun deriveParametrizedType(declaredType: Type, collectionClass: Class<out Collection<*>>): ParameterizedType =
|
||||
(declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType))
|
||||
(declaredType as? ParameterizedType) ?: DeserializedParameterizedType(collectionClass, arrayOf(SerializerFactory.AnyType))
|
||||
|
||||
|
||||
private fun findMostSuitableCollectionType(actualClass: Class<*>): Class<out Collection<*>> =
|
||||
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
|
||||
supportedTypes.keys.findLast { it.isAssignableFrom(actualClass) }!!
|
||||
|
||||
}
|
||||
|
||||
@ -61,13 +60,13 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
|
||||
|
||||
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor), emptyList())
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) {
|
||||
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) = ifThrowsAppend({declaredType.typeName}) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
withList {
|
||||
@ -78,7 +77,7 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) {
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ declaredType.typeName }) {
|
||||
// TODO: Can we verify the entries in the list?
|
||||
concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) })
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
@ -9,7 +10,7 @@ import java.lang.reflect.Type
|
||||
* 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> : AMQPSerializer<T> {
|
||||
abstract class CustomSerializer<T : Any> : AMQPSerializer<T> {
|
||||
/**
|
||||
* 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.
|
||||
@ -36,8 +37,7 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
|
||||
data.withDescribed(descriptor) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
writeDescribedObject(obj as T, data, type, output)
|
||||
writeDescribedObject(uncheckedCast(obj), data, type, output)
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
* 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>(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer<T>) : CustomSerializer<T>() {
|
||||
class SubClass<T : Any>(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer<T>) : CustomSerializer<T>() {
|
||||
// TODO: should this be empty or contain the schema of the super?
|
||||
override val schemaForDocumentation = Schema(emptyList())
|
||||
|
||||
@ -76,7 +76,7 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
* 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>(protected val clazz: Class<T>, protected val withInheritance: Boolean) : CustomSerializer<T>() {
|
||||
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.valueOf("$DESCRIPTOR_DOMAIN:${nameForType(clazz)}")
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
@ -87,12 +87,12 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
/**
|
||||
* Additional base features for a custom serializer for a particular class, that excludes subclasses.
|
||||
*/
|
||||
abstract class Is<T>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, false)
|
||||
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>(clazz: Class<T>) : CustomSerializerImp<T>(clazz, true)
|
||||
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
|
||||
@ -101,10 +101,10 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
* 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, P>(clazz: Class<T>,
|
||||
protected val proxyClass: Class<P>,
|
||||
protected val factory: SerializerFactory,
|
||||
withInheritance: Boolean = true) : CustomSerializerImp<T>(clazz, withInheritance) {
|
||||
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) }
|
||||
@ -134,8 +134,7 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val proxy = proxySerializer.readObject(obj, schema, input) as P
|
||||
val proxy: P = uncheckedCast(proxySerializer.readObject(obj, schema, input))
|
||||
return fromProxy(proxy)
|
||||
}
|
||||
}
|
||||
@ -151,12 +150,11 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
|
||||
* @param make A lambda for constructing an instance, that defaults to calling a constructor that expects a string.
|
||||
* @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method.
|
||||
*/
|
||||
abstract class ToString<T>(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() })
|
||||
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(
|
||||
|
@ -11,6 +11,8 @@ import org.apache.qpid.proton.codec.Data
|
||||
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)
|
||||
@ -58,7 +60,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
deserializeAndReturnEnvelope(bytes, T::class.java)
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
private fun getEnvelope(bytes: ByteSequence): Envelope {
|
||||
internal fun getEnvelope(bytes: ByteSequence): Envelope {
|
||||
// Check that the lead bytes match expected header
|
||||
val headerSize = AmqpHeaderV1_0.size
|
||||
if (bytes.take(headerSize) != AmqpHeaderV1_0) {
|
||||
@ -78,9 +80,9 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
private fun <R> des(generator: () -> R): R {
|
||||
try {
|
||||
return generator()
|
||||
} catch(nse: NotSerializableException) {
|
||||
} catch (nse: NotSerializableException) {
|
||||
throw nse
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
throw NotSerializableException("Unexpected throwable: ${t.message} ${t.getStackTraceAsString()}")
|
||||
} finally {
|
||||
objectHistory.clear()
|
||||
@ -142,10 +144,18 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory) {
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Currently performs rather basic checks aimed in particular at [java.util.List<Command<?>>] and
|
||||
* [java.lang.Class<? extends net.corda.core.contracts.Contract>]
|
||||
* 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 =
|
||||
asClass() == that.asClass() && that is ParameterizedType
|
||||
when (that) {
|
||||
is ParameterizedType -> asClass() == that.asClass()
|
||||
is TypeVariable<*> -> isSubClassOf(that.bounds.first())
|
||||
is WildcardType -> isSubClassOf(that.upperBounds.first())
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -30,9 +30,9 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
|
||||
val enumName = (obj as List<*>)[0] as String
|
||||
val enumOrd = obj[1] as Int
|
||||
val fromOrd = type.asClass()!!.enumConstants[enumOrd]
|
||||
val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>?
|
||||
|
||||
if (enumName != fromOrd?.toString()) {
|
||||
if (enumName != fromOrd?.name) {
|
||||
throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but "
|
||||
+ "ordinality has changed")
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import org.apache.qpid.proton.amqp.Symbol
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.io.NotSerializableException
|
||||
@ -31,8 +32,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
LinkedHashMap::class.java to { map -> LinkedHashMap(map) },
|
||||
TreeMap::class.java to { map -> TreeMap(map) },
|
||||
EnumMap::class.java to { map ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
EnumMap(map as Map<EnumJustUsedForCasting, Any>)
|
||||
EnumMap(uncheckedCast<Map<*, *>, Map<EnumJustUsedForCasting, Any>>(map))
|
||||
}
|
||||
))
|
||||
|
||||
@ -42,12 +42,10 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
|
||||
fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType {
|
||||
declaredClass.checkSupportedMapType()
|
||||
if(supportedTypes.containsKey(declaredClass)) {
|
||||
if (supportedTypes.containsKey(declaredClass)) {
|
||||
// Simple case - it is already known to be a map.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return deriveParametrizedType(declaredType, declaredClass as Class<out Map<*, *>>)
|
||||
}
|
||||
else if (actualClass != null && Map::class.java.isAssignableFrom(actualClass)) {
|
||||
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)
|
||||
@ -68,14 +66,14 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
|
||||
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor), emptyList())
|
||||
|
||||
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({declaredType.typeName}) {
|
||||
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) = ifThrowsAppend({declaredType.typeName}) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ declaredType.typeName }) {
|
||||
obj.javaClass.checkSupportedMapType()
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
@ -90,7 +88,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({declaredType.typeName}) {
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): 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(schema, input, it) }
|
||||
concreteBuilder(entries.toMap())
|
||||
@ -109,13 +107,11 @@ 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 (
|
||||
} 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")
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({clazz.typeName}) {
|
||||
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) = ifThrowsAppend({ clazz.typeName }) {
|
||||
// Write described
|
||||
data.withDescribed(typeNotation.descriptor) {
|
||||
// Write list
|
||||
@ -53,7 +53,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
|
||||
}
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({clazz.typeName}) {
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = ifThrowsAppend({ clazz.typeName }) {
|
||||
if (obj is List<*>) {
|
||||
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
|
||||
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.codec.Data
|
||||
import java.lang.reflect.Method
|
||||
@ -20,8 +21,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
val default: String? = generateDefault()
|
||||
val mandatory: Boolean = generateMandatory()
|
||||
|
||||
private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface ?: false
|
||||
private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive ?: false
|
||||
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)
|
||||
@ -44,19 +45,28 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
}
|
||||
|
||||
private fun generateMandatory(): Boolean {
|
||||
return isJVMPrimitive || !(readMethod?.returnsNullable() ?: true)
|
||||
return isJVMPrimitive || readMethod?.returnsNullable() == false
|
||||
}
|
||||
|
||||
private fun Method.returnsNullable(): Boolean {
|
||||
val returnTypeString = this.declaringClass.kotlin.memberProperties.firstOrNull { it.javaGetter == this }?.returnType?.toString() ?: "?"
|
||||
return returnTypeString.endsWith('?') || returnTypeString.endsWith('!')
|
||||
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.
|
||||
logger.error("Unexpected internal Kotlin error", e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<PropertySerializer>()
|
||||
|
||||
fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
|
||||
readMethod?.isAccessible = true
|
||||
if (SerializerFactory.isPrimitive(resolvedType)) {
|
||||
return when(resolvedType) {
|
||||
return when (resolvedType) {
|
||||
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
|
||||
else -> AMQPPrimitivePropertySerializer(name, readMethod, resolvedType)
|
||||
}
|
||||
@ -76,17 +86,17 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
// 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}) {
|
||||
override fun writeClassInfo(output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
||||
if (resolvedType != Any::class.java) {
|
||||
typeSerializer.writeClassInfo(output)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({nameForDebug}) {
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? = ifThrowsAppend({ nameForDebug }) {
|
||||
input.readObjectOrNull(obj, schema, resolvedType)
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({nameForDebug}) {
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) = ifThrowsAppend({ nameForDebug }) {
|
||||
output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType)
|
||||
}
|
||||
|
||||
@ -123,7 +133,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
override fun writeClassInfo(output: SerializationOutput) {}
|
||||
|
||||
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
|
||||
return if(obj == null) null else (obj as Short).toChar()
|
||||
return if (obj == null) null else (obj as Short).toChar()
|
||||
}
|
||||
|
||||
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
|
||||
|
@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import com.google.common.hash.Hasher
|
||||
import com.google.common.hash.Hashing
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toBase64
|
||||
@ -94,8 +95,7 @@ data class Schema(val types: List<TypeNotation>) : DescribedType {
|
||||
|
||||
override fun newInstance(described: Any?): Schema {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return Schema(list[0] as List<TypeNotation>)
|
||||
return Schema(uncheckedCast(list[0]))
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,8 +162,7 @@ data class Field(val name: String, val type: String, val requires: List<String>,
|
||||
|
||||
override fun newInstance(described: Any?): Field {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return Field(list[0] as String, list[1] as String, list[2] as List<String>, list[3] as? String, list[4] as? String, list[5] as Boolean, list[6] as Boolean)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,8 +222,7 @@ data class CompositeType(override val name: String, override val label: String?,
|
||||
|
||||
override fun newInstance(described: Any?): CompositeType {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return CompositeType(list[0] as String, list[1] as? String, list[2] as List<String>, list[3] as Descriptor, list[4] as List<Field>)
|
||||
return CompositeType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as Descriptor, uncheckedCast(list[4]))
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,8 +271,7 @@ data class RestrictedType(override val name: String,
|
||||
|
||||
override fun newInstance(described: Any?): RestrictedType {
|
||||
val list = described as? List<*> ?: throw IllegalStateException("Was expecting a list")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return RestrictedType(list[0] as String, list[1] as? String, list[2] as List<String>, list[3] as String, list[4] as Descriptor, list[5] as List<Choice>)
|
||||
return RestrictedType(list[0] as String, list[1] as? String, uncheckedCast(list[2]), list[3] as String, list[4] as Descriptor, uncheckedCast(list[5]))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import com.google.common.primitives.Primitives
|
||||
import com.google.common.reflect.TypeToken
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
@ -51,7 +53,7 @@ internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
|
||||
}
|
||||
}
|
||||
|
||||
return preferredCandidate?.apply { isAccessible = true}
|
||||
return preferredCandidate?.apply { isAccessible = true }
|
||||
?: throw NotSerializableException("No constructor for deserialization found for $clazz.")
|
||||
} else {
|
||||
return null
|
||||
@ -80,8 +82,8 @@ private fun <T : Any> propertiesForSerializationFromConstructor(kotlinConstructo
|
||||
for (param in kotlinConstructor.parameters) {
|
||||
val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.")
|
||||
val matchingProperty = properties[name] ?:
|
||||
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.")
|
||||
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.")
|
||||
// Check that the method has a getter in java.
|
||||
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." +
|
||||
" If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
|
||||
@ -123,7 +125,7 @@ private fun exploreType(type: Type?, interfaces: MutableSet<Type>, serializerFac
|
||||
val clazz = type?.asClass()
|
||||
if (clazz != null) {
|
||||
if (clazz.isInterface) {
|
||||
if(serializerFactory.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting.
|
||||
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) {
|
||||
@ -189,6 +191,8 @@ internal fun Type.asClass(): Class<*>? {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -261,3 +265,19 @@ private fun Throwable.setMessage(newMsg: String) {
|
||||
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))
|
||||
}
|
||||
|
@ -82,14 +82,13 @@ open class SerializationOutput(internal val serializerFactory: SerializerFactory
|
||||
}
|
||||
|
||||
val retrievedRefCount = objectHistory[obj]
|
||||
if(retrievedRefCount == null) {
|
||||
if (retrievedRefCount == null) {
|
||||
serializer.writeObject(obj, data, type, this)
|
||||
// Important to do it after serialization such that dependent object will have preceding reference numbers
|
||||
// assigned to them first as they will be first read from the stream on receiving end.
|
||||
// Skip for primitive types as they are too small and overhead of referencing them will be much higher than their content
|
||||
if (suitableForObjectReference(obj.javaClass)) objectHistory.put(obj, objectHistory.size)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
data.writeReferencedObject(ReferencedObject(retrievedRefCount))
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ 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.serialization.CordaSerializable
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.*
|
||||
import org.apache.qpid.proton.amqp.*
|
||||
import java.io.NotSerializableException
|
||||
@ -30,11 +30,11 @@ data class FactorySchemaAndDescriptor(val schema: Schema, val typeDescriptor: An
|
||||
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
|
||||
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
|
||||
@ThreadSafe
|
||||
class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
|
||||
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
|
||||
private val customSerializers = CopyOnWriteArrayList<CustomSerializer<out Any>>()
|
||||
val classCarpenter = ClassCarpenter(cl, whitelist)
|
||||
open val classCarpenter = ClassCarpenter(cl, whitelist)
|
||||
val classloader: ClassLoader
|
||||
get() = classCarpenter.classloader
|
||||
|
||||
@ -81,6 +81,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
}
|
||||
}
|
||||
Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> serializersByType.computeIfAbsent(actualClass ?: declaredClass) {
|
||||
whitelist.requireWhitelisted(actualType)
|
||||
EnumSerializer(actualType, actualClass ?: declaredClass, this)
|
||||
}
|
||||
else -> makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
|
||||
@ -105,6 +106,8 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
val declaredComponent = declaredType.genericComponentType
|
||||
inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray()
|
||||
}
|
||||
is TypeVariable<*> -> actualClass
|
||||
is WildcardType -> actualClass
|
||||
else -> null
|
||||
}
|
||||
|
||||
@ -239,10 +242,10 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this)
|
||||
else ArraySerializer.make(type, this)
|
||||
} else if (clazz.kotlin.objectInstance != null) {
|
||||
whitelisted(clazz)
|
||||
whitelist.requireWhitelisted(clazz)
|
||||
SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this)
|
||||
} else {
|
||||
whitelisted(type)
|
||||
whitelist.requireWhitelisted(type)
|
||||
ObjectSerializer(type, this)
|
||||
}
|
||||
}
|
||||
@ -260,32 +263,13 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
|
||||
return customSerializer
|
||||
} else {
|
||||
// Make a subclass serializer for the subclass and return that...
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return CustomSerializer.SubClass(clazz, customSerializer as CustomSerializer<Any>)
|
||||
return CustomSerializer.SubClass(clazz, uncheckedCast(customSerializer))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun whitelisted(type: Type) {
|
||||
val clazz = type.asClass()!!
|
||||
if (isNotWhitelisted(clazz)) {
|
||||
throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.")
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore SimpleFieldAccess as we add it to anything we build in the carpenter.
|
||||
internal fun isNotWhitelisted(clazz: Class<*>): Boolean = clazz == SimpleFieldAccess::class.java ||
|
||||
(!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz))
|
||||
|
||||
// Recursively check the class, interfaces and superclasses for our annotation.
|
||||
private fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
|
||||
return type.isAnnotationPresent(CordaSerializable::class.java) ||
|
||||
type.interfaces.any { hasAnnotationInHierarchy(it) }
|
||||
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
|
||||
}
|
||||
|
||||
private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer<Any> {
|
||||
val rawType = declaredType.rawType as Class<*>
|
||||
rawType.checkSupportedMapType()
|
||||
|
@ -1,11 +1,11 @@
|
||||
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.*
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
/**
|
||||
* A serializer that writes out an [EnumSet] as a type, plus list of instances in the set.
|
||||
*/
|
||||
@ -16,7 +16,7 @@ class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Enu
|
||||
|
||||
private fun elementType(set: EnumSet<*>): Class<*> {
|
||||
return if (set.isEmpty()) {
|
||||
EnumSet.complementOf(set as EnumSet<MapSerializer.EnumJustUsedForCasting>).first().javaClass
|
||||
EnumSet.complementOf(uncheckedCast<EnumSet<*>, EnumSet<MapSerializer.EnumJustUsedForCasting>>(set)).first().javaClass
|
||||
} else {
|
||||
set.first().javaClass
|
||||
}
|
||||
@ -24,9 +24,9 @@ class EnumSetSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Enu
|
||||
|
||||
override fun fromProxy(proxy: EnumSetProxy): EnumSet<*> {
|
||||
return if (proxy.elements.isEmpty()) {
|
||||
EnumSet.noneOf(proxy.clazz as Class<MapSerializer.EnumJustUsedForCasting>)
|
||||
EnumSet.noneOf(uncheckedCast<Class<*>, Class<MapSerializer.EnumJustUsedForCasting>>(proxy.clazz))
|
||||
} else {
|
||||
EnumSet.copyOf(proxy.elements as List<MapSerializer.EnumJustUsedForCasting>)
|
||||
EnumSet.copyOf(uncheckedCast<List<Any>, List<MapSerializer.EnumJustUsedForCasting>>(proxy.elements))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp.custom
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.*
|
||||
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) {
|
||||
checkUseCase(allowedUseCases)
|
||||
output.writeObject(obj.encoded, data, clazz)
|
||||
}
|
||||
|
||||
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PrivateKey {
|
||||
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
|
||||
return Crypto.decodePrivateKey(bits)
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
for (prop in props) {
|
||||
extraProperties[prop.name] = prop.readMethod!!.invoke(obj)
|
||||
}
|
||||
} catch(e: NotSerializableException) {
|
||||
} catch (e: NotSerializableException) {
|
||||
logger.warn("Unexpected exception", e)
|
||||
}
|
||||
obj.originalMessage
|
||||
@ -67,7 +67,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
|
||||
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).apply {
|
||||
return CordaRuntimeException(proxy.exceptionClass, null, null).apply {
|
||||
this.setMessage(proxy.message)
|
||||
this.setCause(proxy.cause)
|
||||
this.stackTrace = proxy.stackTrace
|
||||
|
@ -12,10 +12,12 @@ class ZonedDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Pro
|
||||
// so that any change to internals of `ZonedDateTime` is detected early.
|
||||
companion object {
|
||||
val ofLenient = 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)
|
||||
|
@ -132,5 +132,10 @@ fun AMQPField.validateType(classloader: ClassLoader) = when (type) {
|
||||
}
|
||||
|
||||
private fun ClassLoader.exists(clazz: String) = run {
|
||||
try { this.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } }
|
||||
try {
|
||||
this.loadClass(clazz); true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,8 @@ 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.util.*
|
||||
|
||||
/**
|
||||
@ -17,6 +15,7 @@ import java.util.*
|
||||
* 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?
|
||||
}
|
||||
@ -134,7 +133,10 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
visit(TARGET_VERSION, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, schema.jvmName,
|
||||
"L$jlEnum<L${schema.jvmName};>;", jlEnum, null)
|
||||
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
if (schema.flags.cordaSerializable()) {
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
}
|
||||
|
||||
generateFields(schema)
|
||||
generateStaticEnumConstructor(schema)
|
||||
generateEnumConstructor()
|
||||
@ -151,8 +153,10 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
cw.apply {
|
||||
visit(TARGET_VERSION, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null,
|
||||
jlObject, interfaces)
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
|
||||
if (schema.flags.cordaSerializable()) {
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
}
|
||||
generateAbstractGetters(schema)
|
||||
}.visitEnd()
|
||||
}
|
||||
@ -163,20 +167,25 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
val superName = schema.superclass?.jvmName ?: jlObject
|
||||
val interfaces = schema.interfaces.map { it.name.jvm }.toMutableList()
|
||||
|
||||
if (SimpleFieldAccess::class.java !in schema.interfaces) {
|
||||
if (SimpleFieldAccess::class.java !in schema.interfaces
|
||||
&& schema.flags.cordaSerializable()
|
||||
&& schema.flags.simpleFieldAccess()) {
|
||||
interfaces.add(SimpleFieldAccess::class.java.name.jvm)
|
||||
}
|
||||
|
||||
cw.apply {
|
||||
visit(TARGET_VERSION, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName,
|
||||
interfaces.toTypedArray())
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
|
||||
if (schema.flags.cordaSerializable()) {
|
||||
visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd()
|
||||
}
|
||||
generateFields(schema)
|
||||
generateClassConstructor(schema)
|
||||
generateGetters(schema)
|
||||
if (schema.superclass == null)
|
||||
if (schema.superclass == null) {
|
||||
generateGetMethod() // From SimplePropertyAccess
|
||||
}
|
||||
generateToString(schema)
|
||||
}.visitEnd()
|
||||
}
|
||||
@ -237,10 +246,9 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateGetters(schema: Schema) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for ((name, type) in (schema.fields as Map<String, ClassField>)) {
|
||||
for ((name, type) in schema.fields) {
|
||||
visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null).apply {
|
||||
type.addNullabilityAnnotation(this)
|
||||
(type as ClassField).addNullabilityAnnotation(this)
|
||||
visitCode()
|
||||
visitVarInsn(ALOAD, 0) // Load 'this'
|
||||
visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor)
|
||||
@ -258,8 +266,7 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
}
|
||||
|
||||
private fun ClassWriter.generateAbstractGetters(schema: Schema) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for ((name, field) in (schema.fields as Map<String, ClassField>)) {
|
||||
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()
|
||||
@ -363,9 +370,8 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
|
||||
// Assign the fields from parameters.
|
||||
var slot = 1 + superclassFields.size
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for ((name, field) in (schema.fields as Map<String, ClassField>)) {
|
||||
field.nullTest(this, slot)
|
||||
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.
|
||||
@ -391,11 +397,21 @@ class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
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.keys.forEach { require(isJavaName(it)) { "Not a valid Java name: $it" } }
|
||||
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.
|
||||
|
@ -1,11 +1,14 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
class DuplicateNameException : RuntimeException(
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.CordaRuntimeException
|
||||
|
||||
class DuplicateNameException : CordaRuntimeException(
|
||||
"An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.")
|
||||
|
||||
class InterfaceMismatchException(msg: String) : RuntimeException(msg)
|
||||
class InterfaceMismatchException(msg: String) : CordaRuntimeException(msg)
|
||||
|
||||
class NullablePrimitiveException(msg: String) : RuntimeException(msg)
|
||||
class NullablePrimitiveException(msg: String) : CordaRuntimeException(msg)
|
||||
|
||||
class UncarpentableException(name: String, field: String, type: String) :
|
||||
Exception("Class $name is loadable yet contains field $field of unknown type $type")
|
||||
CordaException("Class $name is loadable yet contains field $field of unknown type $type")
|
||||
|
@ -1,8 +1,12 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import kotlin.collections.LinkedHashMap
|
||||
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 contsruct
|
||||
@ -20,6 +24,8 @@ abstract class Schema(
|
||||
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) }
|
||||
|
||||
@ -41,6 +47,18 @@ abstract class Schema(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,2 +0,0 @@
|
||||
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
|
||||
net.corda.nodeapi.internal.serialization.DefaultWhitelist
|
@ -0,0 +1,135 @@
|
||||
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 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 {
|
||||
SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader());
|
||||
SerializationOutput ser = new SerializationOutput(factory1);
|
||||
SerializedBytes<Object> bytes = ser.serialize(container);
|
||||
DeserializationInput des = new DeserializationInput(factory1);
|
||||
T deserialized = des.deserialize(bytes, clazz);
|
||||
Assert.assertEquals(container, deserialized);
|
||||
}
|
||||
}
|
@ -76,54 +76,66 @@ class ConfigParsingTest {
|
||||
testPropertyType<URLData, URLListData, URL>(URL("http://localhost:1234"), URL("http://localhost:1235"), valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun CordaX500Name() {
|
||||
testPropertyType<CordaX500NameData, CordaX500NameListData, CordaX500Name>(
|
||||
CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"),
|
||||
CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"),
|
||||
valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flat Properties`() {
|
||||
val config = config("value" to mapOf("key" to "prop"))
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["key"] = "prop" })
|
||||
val data = PropertiesData(Properties().apply { this["key"] = "prop" })
|
||||
assertThat(config.parseAs<PropertiesData>()).isEqualTo(data)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Properties key with dot`() {
|
||||
val config = config("value" to mapOf("key.key2" to "prop"))
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["key.key2"] = "prop" })
|
||||
val data = PropertiesData(Properties().apply { this["key.key2"] = "prop" })
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(data.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nested Properties`() {
|
||||
val config = config("value" to mapOf("first" to mapOf("second" to "prop")))
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["first.second"] = "prop" })
|
||||
val data = PropertiesData(Properties().apply { this["first.second"] = "prop" })
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(data.value)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `List of Properties`() {
|
||||
val config = config("values" to listOf(emptyMap(), mapOf("key" to "prop")))
|
||||
assertThat(config.parseAs<PropertiesListData>().values).containsExactly(
|
||||
val data = PropertiesListData(listOf(
|
||||
Properties(),
|
||||
Properties().apply { this["key"] = "prop" })
|
||||
Properties().apply { this["key"] = "prop" }))
|
||||
assertThat(config.parseAs<PropertiesListData>().values).isEqualTo(data.values)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Set`() {
|
||||
val config = config("values" to listOf("a", "a", "b"))
|
||||
assertThat(config.parseAs<StringSetData>().values).containsOnly("a", "b")
|
||||
val data = StringSetData(setOf("a", "b"))
|
||||
assertThat(config("values" to listOf("a", "a", "b")).parseAs<StringSetData>()).isEqualTo(data)
|
||||
assertThat(data.toConfig()).isEqualTo(config("values" to listOf("a", "b")))
|
||||
assertThat(empty().parseAs<StringSetData>().values).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun x500Name() {
|
||||
testPropertyType<X500NameData, X500NameListData, CordaX500Name>(CordaX500Name(organisation = "Mock Party", locality = "London", country = "GB"), CordaX500Name(organisation = "Mock Party 2", locality = "London", country = "GB"), valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multi property data class`() {
|
||||
val data = config(
|
||||
val config = config(
|
||||
"b" to true,
|
||||
"i" to 123,
|
||||
"l" to listOf("a", "b"))
|
||||
.parseAs<MultiPropertyData>()
|
||||
val data = config.parseAs<MultiPropertyData>()
|
||||
assertThat(data.i).isEqualTo(123)
|
||||
assertThat(data.b).isTrue()
|
||||
assertThat(data.l).containsExactly("a", "b")
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -133,6 +145,7 @@ class ConfigParsingTest {
|
||||
"value" to "nested"))
|
||||
val data = NestedData(StringData("nested"))
|
||||
assertThat(config.parseAs<NestedData>()).isEqualTo(data)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -143,12 +156,14 @@ class ConfigParsingTest {
|
||||
mapOf("value" to "2")))
|
||||
val data = DataListData(listOf(StringData("1"), StringData("2")))
|
||||
assertThat(config.parseAs<DataListData>()).isEqualTo(data)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default value property`() {
|
||||
assertThat(config("a" to 3).parseAs<DefaultData>()).isEqualTo(DefaultData(3, 2))
|
||||
assertThat(config("a" to 3, "defaultOfTwo" to 3).parseAs<DefaultData>()).isEqualTo(DefaultData(3, 3))
|
||||
assertThat(DefaultData(3).toConfig()).isEqualTo(config("a" to 3, "defaultOfTwo" to 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -156,12 +171,19 @@ class ConfigParsingTest {
|
||||
assertThat(empty().parseAs<NullableData>().nullable).isNull()
|
||||
assertThat(config("nullable" to null).parseAs<NullableData>().nullable).isNull()
|
||||
assertThat(config("nullable" to "not null").parseAs<NullableData>().nullable).isEqualTo("not null")
|
||||
assertThat(NullableData(null).toConfig()).isEqualTo(empty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `old config property`() {
|
||||
assertThat(config("oldValue" to "old").parseAs<OldData>().newValue).isEqualTo("old")
|
||||
assertThat(config("newValue" to "new").parseAs<OldData>().newValue).isEqualTo("new")
|
||||
assertThat(OldData("old").toConfig()).isEqualTo(config("newValue" to "old"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `static field`() {
|
||||
assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3))
|
||||
}
|
||||
|
||||
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
|
||||
@ -177,6 +199,7 @@ class ConfigParsingTest {
|
||||
val config = config("value" to if (valueToString) value.toString() else value)
|
||||
val data = constructor.call(value)
|
||||
assertThat(config.parseAs<T>().value).isEqualTo(data.value)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
|
||||
private inline fun <reified T : ListData<V>, V : Any> testListProperty(value1: V, value2: V, valuesToString: Boolean) {
|
||||
@ -187,6 +210,7 @@ class ConfigParsingTest {
|
||||
val config = config("values" to configValues.take(n))
|
||||
val data = constructor.call(rawValues.take(n))
|
||||
assertThat(config.parseAs<T>().values).isEqualTo(data.values)
|
||||
assertThat(data.toConfig()).isEqualTo(config)
|
||||
}
|
||||
assertThat(empty().parseAs<T>().values).isEmpty()
|
||||
}
|
||||
@ -228,8 +252,8 @@ class ConfigParsingTest {
|
||||
data class PathListData(override val values: List<Path>) : ListData<Path>
|
||||
data class URLData(override val value: URL) : SingleData<URL>
|
||||
data class URLListData(override val values: List<URL>) : ListData<URL>
|
||||
data class X500NameData(override val value: CordaX500Name) : SingleData<CordaX500Name>
|
||||
data class X500NameListData(override val values: List<CordaX500Name>) : ListData<CordaX500Name>
|
||||
data class CordaX500NameData(override val value: CordaX500Name) : SingleData<CordaX500Name>
|
||||
data class CordaX500NameListData(override val values: List<CordaX500Name>) : ListData<CordaX500Name>
|
||||
data class PropertiesData(override val value: Properties) : SingleData<Properties>
|
||||
data class PropertiesListData(override val values: List<Properties>) : ListData<Properties>
|
||||
data class MultiPropertyData(val i: Int, val b: Boolean, val l: List<String>)
|
||||
@ -240,7 +264,12 @@ class ConfigParsingTest {
|
||||
data class OldData(
|
||||
@OldConfig("oldValue")
|
||||
val newValue: String)
|
||||
data class DataWithCompanion(val value: Int) {
|
||||
companion object {
|
||||
@Suppress("unused")
|
||||
val companionValue = 2
|
||||
}
|
||||
}
|
||||
|
||||
enum class TestEnum { Value1, Value2 }
|
||||
|
||||
}
|
@ -9,7 +9,6 @@ import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -37,7 +36,7 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase()
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder {
|
||||
val state = State(magicNumber)
|
||||
return TransactionBuilder(notary)
|
||||
.withItems(StateAndContract(state, ATTACHMENT_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey))
|
||||
.withItems(StateAndContract(state, ATTACHMENT_PROGRAM_ID), Command(Commands.Create(), owner.party.owningKey))
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,12 +44,7 @@ class AttachmentsClassLoaderStaticContractTests : TestDependencyInjectionBase()
|
||||
|
||||
@Before
|
||||
fun `create service hub`() {
|
||||
serviceHub = MockServices(cordappPackages=listOf("net.corda.nodeapi.internal"))
|
||||
}
|
||||
|
||||
@After
|
||||
fun `clear packages`() {
|
||||
unsetCordappPackages()
|
||||
serviceHub = MockServices(cordappPackages = listOf("net.corda.nodeapi.internal"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.toWireTransaction
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.serialization.*
|
||||
@ -44,6 +45,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
whenever(serviceHub.attachments).thenReturn(attachmentStorage)
|
||||
return this.withServiceHub(serviceHub)
|
||||
}
|
||||
|
||||
private fun SerializationContext.withServiceHub(serviceHub: ServiceHub): SerializationContext {
|
||||
return this.withTokenContext(SerializeAsTokenContextImpl(serviceHub) {}).withProperty(attachmentsClassLoaderEnabledPropertyName, true)
|
||||
}
|
||||
@ -53,8 +55,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
|
||||
class DummyServiceHub : MockServices() {
|
||||
override val cordappProvider: CordappProviderImpl
|
||||
= CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH))).start(attachments)
|
||||
|
||||
= CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments)
|
||||
private val cordapp get() = cordappProvider.cordapps.first()
|
||||
val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!!
|
||||
val appContext get() = cordappProvider.getAppContext(cordapp)
|
||||
@ -265,7 +266,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `test serialization of sub-sequence OpaqueBytes`() {
|
||||
val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3 ,2)
|
||||
val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3, 2)
|
||||
val bytes = bytesSequence.serialize()
|
||||
val copiedBytesSequence = bytes.deserialize()
|
||||
|
||||
@ -300,7 +301,7 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `test deserialize of WireTransaction where contract cannot be found`() {
|
||||
kryoSpecific<AttachmentsClassLoaderTests>("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") {
|
||||
kryoSpecific("Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not") {
|
||||
ClassLoaderForTests().use { child ->
|
||||
val contractClass = Class.forName(ISOLATED_CONTRACT_CLASS_NAME, true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
@ -309,8 +310,8 @@ class AttachmentsClassLoaderTests : TestDependencyInjectionBase() {
|
||||
val attachmentRef = serviceHub.attachmentId
|
||||
val bytes = run {
|
||||
val outboundContext = SerializationFactory.defaultFactory.defaultContext
|
||||
.withServiceHub(serviceHub)
|
||||
.withClassLoader(child)
|
||||
.withServiceHub(serviceHub)
|
||||
.withClassLoader(child)
|
||||
val wireTransaction = tx.toWireTransaction(serviceHub, outboundContext)
|
||||
wireTransaction.serialize(context = outboundContext)
|
||||
}
|
||||
|
@ -34,6 +34,23 @@ enum class Foo {
|
||||
abstract val value: Int
|
||||
}
|
||||
|
||||
enum class BadFood {
|
||||
Mud {
|
||||
override val value = -1
|
||||
};
|
||||
|
||||
abstract val value: Int
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class Simple {
|
||||
Easy
|
||||
}
|
||||
|
||||
enum class BadSimple {
|
||||
Nasty
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
open class Element
|
||||
|
||||
@ -106,17 +123,36 @@ class CordaClassResolverTests {
|
||||
|
||||
@Test
|
||||
fun `Annotation on enum works for specialised entries`() {
|
||||
// TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug.
|
||||
@Suppress("UNSUPPORTED_FEATURE")
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java)
|
||||
}
|
||||
|
||||
@Test(expected = KryoException::class)
|
||||
fun `Unannotated specialised enum does not work`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(BadFood.Mud::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Annotation on simple enum works`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(Simple.Easy::class.java)
|
||||
}
|
||||
|
||||
@Test(expected = KryoException::class)
|
||||
fun `Unannotated simple enum does not work`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(BadSimple.Nasty::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Annotation on array element works`() {
|
||||
val values = arrayOf(Element())
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass)
|
||||
}
|
||||
|
||||
@Test(expected = KryoException::class)
|
||||
fun `Unannotated array elements do not work`() {
|
||||
val values = arrayOf(NotSerializable())
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Annotation not needed on abstract class`() {
|
||||
CordaClassResolver(emptyWhitelistContext).getRegistration(AbstractClass::class.java)
|
||||
@ -271,6 +307,7 @@ class CordaClassResolverTests {
|
||||
}
|
||||
|
||||
open class SubHashSet<E> : HashSet<E>()
|
||||
|
||||
@Test
|
||||
fun `Check blacklisted subclass`() {
|
||||
expectedEx.expect(IllegalStateException::class.java)
|
||||
@ -281,6 +318,7 @@ class CordaClassResolverTests {
|
||||
}
|
||||
|
||||
class SubSubHashSet<E> : SubHashSet<E>()
|
||||
|
||||
@Test
|
||||
fun `Check blacklisted subsubclass`() {
|
||||
expectedEx.expect(IllegalStateException::class.java)
|
||||
@ -291,6 +329,7 @@ class CordaClassResolverTests {
|
||||
}
|
||||
|
||||
class ConnectionImpl(private val connection: Connection) : Connection by connection
|
||||
|
||||
@Test
|
||||
fun `Check blacklisted interface impl`() {
|
||||
expectedEx.expect(IllegalStateException::class.java)
|
||||
@ -302,6 +341,7 @@ class CordaClassResolverTests {
|
||||
|
||||
interface SubConnection : Connection
|
||||
class SubConnectionImpl(private val subConnection: SubConnection) : SubConnection by subConnection
|
||||
|
||||
@Test
|
||||
fun `Check blacklisted super-interface impl`() {
|
||||
expectedEx.expect(IllegalStateException::class.java)
|
||||
@ -320,6 +360,7 @@ class CordaClassResolverTests {
|
||||
|
||||
@CordaSerializable
|
||||
class CordaSerializableHashSet<E> : HashSet<E>()
|
||||
|
||||
@Test
|
||||
fun `Check blacklist precedes CordaSerializable`() {
|
||||
expectedEx.expect(IllegalStateException::class.java)
|
||||
|
@ -43,7 +43,7 @@ class KryoTests : TestDependencyInjectionBase() {
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P)
|
||||
SerializationContext.UseCase.Storage)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -188,10 +188,10 @@ class KryoTests : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `serialize - deserialize PrivacySalt`() {
|
||||
val expected = PrivacySalt(byteArrayOf(
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
31, 32
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
31, 32
|
||||
))
|
||||
val serializedBytes = expected.serialize(factory, context)
|
||||
val actual = serializedBytes.deserialize(factory, context)
|
||||
@ -267,7 +267,7 @@ class KryoTests : TestDependencyInjectionBase() {
|
||||
assertEquals(exception.message, exception2.message)
|
||||
|
||||
assertEquals(1, exception2.suppressed.size)
|
||||
assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message }})
|
||||
assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message } })
|
||||
|
||||
val toBeSuppressedOnReceiverSide = IllegalStateException("bazz2")
|
||||
exception2.addSuppressed(toBeSuppressedOnReceiverSide)
|
||||
|
@ -4,19 +4,31 @@ import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.node.services.statemachine.SessionData
|
||||
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.Envelope
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.amqpSpecific
|
||||
import net.corda.testing.kryoSpecific
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.NotSerializableException
|
||||
import java.nio.charset.StandardCharsets.*
|
||||
import java.nio.charset.StandardCharsets.US_ASCII
|
||||
import java.util.*
|
||||
|
||||
class ListsSerializationTest : TestDependencyInjectionBase() {
|
||||
private companion object {
|
||||
val javaEmptyListClass = Collections.emptyList<Any>().javaClass
|
||||
|
||||
fun <T : Any> verifyEnvelope(serBytes: SerializedBytes<T>, envVerBody: (Envelope) -> Unit) =
|
||||
amqpSpecific("AMQP specific envelope verification") {
|
||||
val context = SerializationFactory.defaultFactory.defaultContext
|
||||
val envelope = DeserializationInput(SerializerFactory(context.whitelist, context.deserializationClassLoader)).getEnvelope(serBytes)
|
||||
envVerBody(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -29,21 +41,24 @@ class ListsSerializationTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `check list can be serialized as part of SessionData`() {
|
||||
run {
|
||||
val sessionData = SessionData(123, listOf(1))
|
||||
val sessionData = SessionData(123, listOf(1).serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(listOf(1), sessionData.payload.deserialize())
|
||||
}
|
||||
run {
|
||||
val sessionData = SessionData(123, listOf(1, 2))
|
||||
val sessionData = SessionData(123, listOf(1, 2).serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(listOf(1, 2), sessionData.payload.deserialize())
|
||||
}
|
||||
run {
|
||||
val sessionData = SessionData(123, emptyList<Int>())
|
||||
val sessionData = SessionData(123, emptyList<Int>().serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(emptyList<Int>(), sessionData.payload.deserialize())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check empty list serialises as Java emptyList`() {
|
||||
fun `check empty list serialises as Java emptyList`() = kryoSpecific("Kryo specific test") {
|
||||
val nameID = 0
|
||||
val serializedForm = emptyList<Int>().serialize()
|
||||
val output = ByteArrayOutputStream().apply {
|
||||
@ -60,7 +75,7 @@ class ListsSerializationTest : TestDependencyInjectionBase() {
|
||||
data class WrongPayloadType(val payload: ArrayList<Int>)
|
||||
|
||||
@Test
|
||||
fun `check throws for forbidden declared type`() = amqpSpecific<ListsSerializationTest>("Such exceptions are not expected in Kryo mode.") {
|
||||
fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") {
|
||||
val payload = ArrayList<Int>()
|
||||
payload.add(1)
|
||||
payload.add(2)
|
||||
@ -68,11 +83,34 @@ class ListsSerializationTest : TestDependencyInjectionBase() {
|
||||
Assertions.assertThatThrownBy { wrongPayloadType.serialize() }
|
||||
.isInstanceOf(NotSerializableException::class.java).hasMessageContaining("Cannot derive collection type for declaredType")
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
interface Parent
|
||||
|
||||
data class Child(val value: Int) : Parent
|
||||
|
||||
@CordaSerializable
|
||||
data class CovariantContainer<out T : Parent>(val payload: List<T>)
|
||||
|
||||
@Test
|
||||
fun `check covariance`() {
|
||||
val payload = ArrayList<Child>()
|
||||
payload.add(Child(1))
|
||||
payload.add(Child(2))
|
||||
val container = CovariantContainer(payload)
|
||||
|
||||
fun verifyEnvelopeBody(envelope: Envelope) {
|
||||
envelope.schema.types.single { typeNotation -> typeNotation.name == java.util.List::class.java.name + "<?>" }
|
||||
}
|
||||
|
||||
assertEqualAfterRoundTripSerialization(container, { bytes -> verifyEnvelope(bytes, ::verifyEnvelopeBody) })
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun<reified T : Any> assertEqualAfterRoundTripSerialization(obj: T) {
|
||||
internal inline fun <reified T : Any> assertEqualAfterRoundTripSerialization(obj: T, noinline streamValidation: ((SerializedBytes<T>) -> Unit)? = null) {
|
||||
|
||||
val serializedForm: SerializedBytes<T> = obj.serialize()
|
||||
streamValidation?.invoke(serializedForm)
|
||||
val deserializedInstance = serializedForm.deserialize()
|
||||
|
||||
assertEquals(obj, deserializedInstance)
|
||||
|
@ -3,17 +3,19 @@ package net.corda.nodeapi.internal.serialization
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.statemachine.SessionData
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.amqpSpecific
|
||||
import net.corda.testing.kryoSpecific
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class MapsSerializationTest : TestDependencyInjectionBase() {
|
||||
private companion object {
|
||||
@ -22,7 +24,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check EmptyMap serialization`() = amqpSpecific<MapsSerializationTest>("kotlin.collections.EmptyMap is not enabled for Kryo serialization") {
|
||||
fun `check EmptyMap serialization`() = amqpSpecific("kotlin.collections.EmptyMap is not enabled for Kryo serialization") {
|
||||
assertEqualAfterRoundTripSerialization(emptyMap<Any, Any>())
|
||||
}
|
||||
|
||||
@ -33,15 +35,16 @@ class MapsSerializationTest : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `check list can be serialized as part of SessionData`() {
|
||||
val sessionData = SessionData(123, smallMap)
|
||||
val sessionData = SessionData(123, smallMap.serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(smallMap, sessionData.payload.deserialize())
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class WrongPayloadType(val payload: HashMap<String, String>)
|
||||
|
||||
@Test
|
||||
fun `check throws for forbidden declared type`() = amqpSpecific<ListsSerializationTest>("Such exceptions are not expected in Kryo mode.") {
|
||||
fun `check throws for forbidden declared type`() = amqpSpecific("Such exceptions are not expected in Kryo mode.") {
|
||||
val payload = HashMap<String, String>(smallMap)
|
||||
val wrongPayloadType = WrongPayloadType(payload)
|
||||
Assertions.assertThatThrownBy { wrongPayloadType.serialize() }
|
||||
@ -64,7 +67,7 @@ class MapsSerializationTest : TestDependencyInjectionBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check empty map serialises as Java emptyMap`() = kryoSpecific<MapsSerializationTest>("Specifically checks Kryo serialization") {
|
||||
fun `check empty map serialises as Java emptyMap`() = kryoSpecific("Specifically checks Kryo serialization") {
|
||||
val nameID = 0
|
||||
val serializedForm = emptyMap<Int, Int>().serialize()
|
||||
val output = ByteArrayOutputStream().apply {
|
||||
|
@ -0,0 +1,42 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.serialization.SerializationContext.UseCase.*
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.security.PrivateKey
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class PrivateKeySerializationTest(private val privateKey: PrivateKey, private val testName: String) : TestDependencyInjectionBase() {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{1}")
|
||||
fun data(): Collection<Array<Any>> {
|
||||
val privateKeys: List<PrivateKey> = Crypto.supportedSignatureSchemes().filterNot { Crypto.COMPOSITE_KEY === it }
|
||||
.map { Crypto.generateKeyPair(it).private }
|
||||
|
||||
return privateKeys.map { arrayOf<Any>(it, PrivateKeySerializationTest::class.java.simpleName + "-" + it.javaClass.simpleName) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passed with expected UseCases`() {
|
||||
assertTrue { privateKey.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes.isNotEmpty() }
|
||||
assertTrue { privateKey.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT).bytes.isNotEmpty() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed with wrong UseCase`() {
|
||||
assertThatThrownBy { privateKey.serialize(context = SerializationDefaults.P2P_CONTEXT) }
|
||||
.isInstanceOf(IllegalStateException::class.java)
|
||||
.hasMessageContaining("UseCase '${P2P}' is not within")
|
||||
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class SerializationTokenTest : TestDependencyInjectionBase() {
|
||||
class SerializationTokenTest : TestDependencyInjectionBase() {
|
||||
|
||||
private lateinit var factory: SerializationFactory
|
||||
private lateinit var context: SerializationContext
|
||||
@ -58,7 +58,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() {
|
||||
val testContext = this.context.withTokenContext(context)
|
||||
val serializedBytes = tokenizableBefore.serialize(factory, testContext)
|
||||
val tokenizableAfter = serializedBytes.deserialize(factory, testContext)
|
||||
assertThat(tokenizableAfter).isSameAs(tokenizableBefore)
|
||||
assertThat(tokenizableAfter).isSameAs(tokenizableBefore)
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException::class)
|
||||
@ -92,11 +92,11 @@ class SerializationTokenTest : TestDependencyInjectionBase() {
|
||||
|
||||
val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(this.context)))
|
||||
val stream = ByteArrayOutputStream()
|
||||
Output(stream).use {
|
||||
it.write(KryoHeaderV0_1.bytes)
|
||||
kryo.writeClass(it, SingletonSerializeAsToken::class.java)
|
||||
kryo.writeObject(it, emptyList<Any>())
|
||||
}
|
||||
Output(stream).use {
|
||||
it.write(KryoHeaderV0_1.bytes)
|
||||
kryo.writeClass(it, SingletonSerializeAsToken::class.java)
|
||||
kryo.writeObject(it, emptyList<Any>())
|
||||
}
|
||||
val serializedBytes = SerializedBytes<Any>(stream.toByteArray())
|
||||
serializedBytes.deserialize(factory, testContext)
|
||||
}
|
||||
@ -105,6 +105,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() {
|
||||
object UnitSerializationToken : SerializationToken {
|
||||
override fun fromToken(context: SerializeAsTokenContext): Any = UnitSerializeAsToken()
|
||||
}
|
||||
|
||||
override fun toToken(context: SerializeAsTokenContext): SerializationToken = UnitSerializationToken
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,13 @@ package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.statemachine.SessionData
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import org.junit.Assert.*
|
||||
import net.corda.testing.kryoSpecific
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
@ -25,21 +28,24 @@ class SetsSerializationTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `check set can be serialized as part of SessionData`() {
|
||||
run {
|
||||
val sessionData = SessionData(123, setOf(1))
|
||||
val sessionData = SessionData(123, setOf(1).serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(setOf(1), sessionData.payload.deserialize())
|
||||
}
|
||||
run {
|
||||
val sessionData = SessionData(123, setOf(1, 2))
|
||||
val sessionData = SessionData(123, setOf(1, 2).serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(setOf(1, 2), sessionData.payload.deserialize())
|
||||
}
|
||||
run {
|
||||
val sessionData = SessionData(123, emptySet<Int>())
|
||||
val sessionData = SessionData(123, emptySet<Int>().serialize())
|
||||
assertEqualAfterRoundTripSerialization(sessionData)
|
||||
assertEquals(emptySet<Int>(), sessionData.payload.deserialize())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check empty set serialises as Java emptySet`() {
|
||||
fun `check empty set serialises as Java emptySet`() = kryoSpecific("Checks Kryo header properties") {
|
||||
val nameID = 0
|
||||
val serializedForm = emptySet<Int>().serialize()
|
||||
val output = ByteArrayOutputStream().apply {
|
||||
|
@ -17,25 +17,28 @@ class DeserializeMapTests {
|
||||
@Test
|
||||
fun mapTest() {
|
||||
data class C(val c: Map<String, Int>)
|
||||
val c = C (mapOf("A" to 1, "B" to 2))
|
||||
|
||||
val c = C(mapOf("A" to 1, "B" to 2))
|
||||
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
}
|
||||
|
||||
@Test(expected=java.io.NotSerializableException::class)
|
||||
@Test(expected = java.io.NotSerializableException::class)
|
||||
fun abstractMapFromMapOf() {
|
||||
data class C(val c: AbstractMap<String, Int>)
|
||||
val c = C (mapOf("A" to 1, "B" to 2) as AbstractMap)
|
||||
|
||||
val c = C(mapOf("A" to 1, "B" to 2) as AbstractMap)
|
||||
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
}
|
||||
|
||||
@Test(expected=java.io.NotSerializableException::class)
|
||||
@Test(expected = java.io.NotSerializableException::class)
|
||||
fun abstractMapFromTreeMap() {
|
||||
data class C(val c: AbstractMap<String, Int>)
|
||||
val c = C (TreeMap(mapOf("A" to 1, "B" to 2)))
|
||||
|
||||
val c = C(TreeMap(mapOf("A" to 1, "B" to 2)))
|
||||
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
@ -44,7 +47,8 @@ class DeserializeMapTests {
|
||||
@Test
|
||||
fun sortedMapTest() {
|
||||
data class C(val c: SortedMap<String, Int>)
|
||||
val c = C(sortedMapOf ("A" to 1, "B" to 2))
|
||||
|
||||
val c = C(sortedMapOf("A" to 1, "B" to 2))
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
}
|
||||
@ -52,7 +56,8 @@ class DeserializeMapTests {
|
||||
@Test
|
||||
fun navigableMapTest() {
|
||||
data class C(val c: NavigableMap<String, Int>)
|
||||
val c = C(TreeMap (mapOf("A" to 1, "B" to 2)).descendingMap())
|
||||
|
||||
val c = C(TreeMap(mapOf("A" to 1, "B" to 2)).descendingMap())
|
||||
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
@ -61,9 +66,10 @@ class DeserializeMapTests {
|
||||
@Test
|
||||
fun dictionaryTest() {
|
||||
data class C(val c: Dictionary<String, Int>)
|
||||
val v : Hashtable<String, Int> = Hashtable()
|
||||
v.put ("a", 1)
|
||||
v.put ("b", 2)
|
||||
|
||||
val v: Hashtable<String, Int> = Hashtable()
|
||||
v.put("a", 1)
|
||||
v.put("b", 2)
|
||||
val c = C(v)
|
||||
|
||||
// expected to throw
|
||||
@ -74,9 +80,10 @@ class DeserializeMapTests {
|
||||
@Test
|
||||
fun hashtableTest() {
|
||||
data class C(val c: Hashtable<String, Int>)
|
||||
val v : Hashtable<String, Int> = Hashtable()
|
||||
v.put ("a", 1)
|
||||
v.put ("b", 2)
|
||||
|
||||
val v: Hashtable<String, Int> = Hashtable()
|
||||
v.put("a", 1)
|
||||
v.put("b", 2)
|
||||
val c = C(v)
|
||||
|
||||
// expected to throw
|
||||
@ -86,8 +93,9 @@ class DeserializeMapTests {
|
||||
|
||||
@Test
|
||||
fun hashMapTest() {
|
||||
data class C(val c : HashMap<String, Int>)
|
||||
val c = C (HashMap (mapOf("A" to 1, "B" to 2)))
|
||||
data class C(val c: HashMap<String, Int>)
|
||||
|
||||
val c = C(HashMap(mapOf("A" to 1, "B" to 2)))
|
||||
|
||||
// expect this to throw
|
||||
Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) }
|
||||
@ -96,8 +104,9 @@ class DeserializeMapTests {
|
||||
|
||||
@Test
|
||||
fun weakHashMapTest() {
|
||||
data class C(val c : WeakHashMap<String, Int>)
|
||||
val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2)))
|
||||
data class C(val c: WeakHashMap<String, Int>)
|
||||
|
||||
val c = C(WeakHashMap(mapOf("A" to 1, "B" to 2)))
|
||||
|
||||
Assertions.assertThatThrownBy { TestSerializationOutput(VERBOSE, sf).serialize(c) }
|
||||
.isInstanceOf(IllegalArgumentException::class.java).hasMessageContaining("Weak references with map types not supported. Suggested fix: use java.util.LinkedHashMap instead.")
|
||||
@ -106,7 +115,8 @@ class DeserializeMapTests {
|
||||
@Test
|
||||
fun concreteTreeMapTest() {
|
||||
data class C(val c: TreeMap<String, Int>)
|
||||
val c = C(TreeMap (mapOf("A" to 1, "B" to 3)))
|
||||
|
||||
val c = C(TreeMap(mapOf("A" to 1, "B" to 3)))
|
||||
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
@ -114,8 +124,9 @@ class DeserializeMapTests {
|
||||
|
||||
@Test
|
||||
fun concreteLinkedHashMapTest() {
|
||||
data class C(val c : LinkedHashMap<String, Int>)
|
||||
val c = C (LinkedHashMap (mapOf("A" to 1, "B" to 2)))
|
||||
data class C(val c: LinkedHashMap<String, Int>)
|
||||
|
||||
val c = C(LinkedHashMap(mapOf("A" to 1, "B" to 2)))
|
||||
|
||||
val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c)
|
||||
DeserializationInput(sf).deserialize(serialisedBytes)
|
||||
|
@ -402,7 +402,8 @@ class DeserializeSimpleTypesTests {
|
||||
@Test
|
||||
fun arrayOfArrayOfInt() {
|
||||
class C(val c: Array<Array<Int>>)
|
||||
val c = C (arrayOf (arrayOf(1,2,3), arrayOf(4,5,6)))
|
||||
|
||||
val c = C(arrayOf(arrayOf(1, 2, 3), arrayOf(4, 5, 6)))
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
@ -421,7 +422,8 @@ class DeserializeSimpleTypesTests {
|
||||
@Test
|
||||
fun arrayOfIntArray() {
|
||||
class C(val c: Array<IntArray>)
|
||||
val c = C (arrayOf (IntArray(3), IntArray(3)))
|
||||
|
||||
val c = C(arrayOf(IntArray(3), IntArray(3)))
|
||||
c.c[0][0] = 1; c.c[0][1] = 2; c.c[0][2] = 3
|
||||
c.c[1][0] = 4; c.c[1][1] = 5; c.c[1][2] = 6
|
||||
|
||||
@ -444,22 +446,32 @@ class DeserializeSimpleTypesTests {
|
||||
class C(val c: Array<Array<IntArray>>)
|
||||
|
||||
val c = C(arrayOf(arrayOf(IntArray(3), IntArray(3), IntArray(3)),
|
||||
arrayOf(IntArray(3), IntArray(3), IntArray(3)),
|
||||
arrayOf(IntArray(3), IntArray(3), IntArray(3))))
|
||||
arrayOf(IntArray(3), IntArray(3), IntArray(3)),
|
||||
arrayOf(IntArray(3), IntArray(3), IntArray(3))))
|
||||
|
||||
for (i in 0..2) { for (j in 0..2) { for (k in 0..2) { c.c[i][j][k] = i + j + k } } }
|
||||
for (i in 0..2) {
|
||||
for (j in 0..2) {
|
||||
for (k in 0..2) {
|
||||
c.c[i][j][k] = i + j + k
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val serialisedC = TestSerializationOutput(VERBOSE, sf1).serialize(c)
|
||||
val deserializedC = DeserializationInput(sf1).deserialize(serialisedC)
|
||||
|
||||
for (i in 0..2) { for (j in 0..2) { for (k in 0..2) {
|
||||
assertEquals(c.c[i][j][k], deserializedC.c[i][j][k])
|
||||
}}}
|
||||
for (i in 0..2) {
|
||||
for (j in 0..2) {
|
||||
for (k in 0..2) {
|
||||
assertEquals(c.c[i][j][k], deserializedC.c[i][j][k])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedRepeatedTypes() {
|
||||
class A(val a : A?, val b: Int)
|
||||
class A(val a: A?, val b: Int)
|
||||
|
||||
var a = A(A(A(A(A(null, 1), 2), 3), 4), 5)
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.junit.Test
|
||||
import java.time.DayOfWeek
|
||||
|
||||
@ -10,12 +12,18 @@ import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import org.assertj.core.api.Assertions
|
||||
|
||||
class EnumTests {
|
||||
enum class Bras {
|
||||
TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class AnnotatedBras {
|
||||
TSHIRT, UNDERWIRE, PUSHUP, BRALETTE, STRAPLESS, SPORTS, BACKLESS, PADDED
|
||||
}
|
||||
|
||||
// The state of the OldBras enum when the tests in changedEnum1 were serialised
|
||||
// - use if the test file needs regenerating
|
||||
//enum class OldBras {
|
||||
@ -41,7 +49,7 @@ class EnumTests {
|
||||
}
|
||||
|
||||
|
||||
enum class BrasWithInit (val someList: List<Int>) {
|
||||
enum class BrasWithInit(val someList: List<Int>) {
|
||||
TSHIRT(emptyList()),
|
||||
UNDERWIRE(listOf(1, 2, 3)),
|
||||
PUSHUP(listOf(100, 200)),
|
||||
@ -82,7 +90,7 @@ class EnumTests {
|
||||
assertEquals(8, schema_bras.choices.size)
|
||||
Bras.values().forEach {
|
||||
val bra = it
|
||||
assertNotNull (schema_bras.choices.find { it.name == bra.name })
|
||||
assertNotNull(schema_bras.choices.find { it.name == bra.name })
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +115,7 @@ class EnumTests {
|
||||
assertEquals(8, schema_bras.choices.size)
|
||||
Bras.values().forEach {
|
||||
val bra = it
|
||||
assertNotNull (schema_bras.choices.find { it.name == bra.name })
|
||||
assertNotNull(schema_bras.choices.find { it.name == bra.name })
|
||||
}
|
||||
|
||||
// Test the actual deserialised object
|
||||
@ -116,13 +124,13 @@ class EnumTests {
|
||||
|
||||
@Test
|
||||
fun multiEnum() {
|
||||
data class Support (val top: Bras, val day : DayOfWeek)
|
||||
data class WeeklySupport (val tops: List<Support>)
|
||||
data class Support(val top: Bras, val day: DayOfWeek)
|
||||
data class WeeklySupport(val tops: List<Support>)
|
||||
|
||||
val week = WeeklySupport (listOf(
|
||||
Support (Bras.PUSHUP, DayOfWeek.MONDAY),
|
||||
Support (Bras.UNDERWIRE, DayOfWeek.WEDNESDAY),
|
||||
Support (Bras.PADDED, DayOfWeek.SUNDAY)))
|
||||
val week = WeeklySupport(listOf(
|
||||
Support(Bras.PUSHUP, DayOfWeek.MONDAY),
|
||||
Support(Bras.UNDERWIRE, DayOfWeek.WEDNESDAY),
|
||||
Support(Bras.PADDED, DayOfWeek.SUNDAY)))
|
||||
|
||||
val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(week))
|
||||
|
||||
@ -138,7 +146,7 @@ class EnumTests {
|
||||
fun enumWithInit() {
|
||||
data class C(val c: BrasWithInit)
|
||||
|
||||
val c = C (BrasWithInit.PUSHUP)
|
||||
val c = C(BrasWithInit.PUSHUP)
|
||||
val obj = DeserializationInput(sf1).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(c))
|
||||
|
||||
assertEquals(c.c, obj.c)
|
||||
@ -149,7 +157,7 @@ class EnumTests {
|
||||
val path = EnumTests::class.java.getResource("EnumTests.changedEnum1")
|
||||
val f = File(path.toURI())
|
||||
|
||||
data class C (val a: OldBras)
|
||||
data class C(val a: OldBras)
|
||||
|
||||
// Original version of the class for the serialised version of this class
|
||||
//
|
||||
@ -169,7 +177,7 @@ class EnumTests {
|
||||
val path = EnumTests::class.java.getResource("EnumTests.changedEnum2")
|
||||
val f = File(path.toURI())
|
||||
|
||||
data class C (val a: OldBras2)
|
||||
data class C(val a: OldBras2)
|
||||
|
||||
// DO NOT CHANGE THIS, it's important we serialise with a value that doesn't
|
||||
// change position in the upated enum class
|
||||
@ -186,4 +194,74 @@ class EnumTests {
|
||||
// we expect this to throw
|
||||
DeserializationInput(sf1).deserialize(SerializedBytes<C>(sc2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enumNotWhitelistedFails() {
|
||||
data class C(val c: Bras)
|
||||
|
||||
class WL(val allowed: String) : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
return type.name == allowed
|
||||
}
|
||||
}
|
||||
|
||||
val factory = SerializerFactory(WL(classTestName("C")), ClassLoader.getSystemClassLoader())
|
||||
|
||||
Assertions.assertThatThrownBy {
|
||||
TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE))
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enumWhitelisted() {
|
||||
data class C(val c: Bras)
|
||||
|
||||
class WL : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>): Boolean {
|
||||
return type.name == "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$enumWhitelisted\$C" ||
|
||||
type.name == "net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras"
|
||||
}
|
||||
}
|
||||
|
||||
val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader())
|
||||
|
||||
// if it all works, this won't explode
|
||||
TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enumAnnotated() {
|
||||
@CordaSerializable data class C(val c: AnnotatedBras)
|
||||
|
||||
class WL : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) = false
|
||||
}
|
||||
|
||||
val factory = SerializerFactory(WL(), ClassLoader.getSystemClassLoader())
|
||||
|
||||
// if it all works, this won't explode
|
||||
TestSerializationOutput(VERBOSE, factory).serialize(C(AnnotatedBras.UNDERWIRE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeNonWhitlistedEnum() {
|
||||
data class C(val c: Bras)
|
||||
|
||||
class WL(val allowed: List<String>) : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) = type.name in allowed
|
||||
}
|
||||
|
||||
// first serialise the class using a context in which Bras are whitelisted
|
||||
val factory = SerializerFactory(WL(listOf(classTestName("C"),
|
||||
"net.corda.nodeapi.internal.serialization.amqp.EnumTests\$Bras")),
|
||||
ClassLoader.getSystemClassLoader())
|
||||
val bytes = TestSerializationOutput(VERBOSE, factory).serialize(C(Bras.UNDERWIRE))
|
||||
|
||||
// then take that serialised object and attempt to deserialize it in a context that
|
||||
// disallows the Bras enum
|
||||
val factory2 = SerializerFactory(WL(listOf(classTestName("C"))), ClassLoader.getSystemClassLoader())
|
||||
Assertions.assertThatThrownBy {
|
||||
DeserializationInput(factory2).deserialize(bytes)
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ class EvolvabilityTests {
|
||||
// f.writeBytes(sc.bytes)
|
||||
|
||||
// new version of the class, in this case the order of the parameters has been swapped
|
||||
data class C (val b: Int, val a: Int)
|
||||
data class C(val b: Int, val a: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
@ -56,7 +56,7 @@ class EvolvabilityTests {
|
||||
// f.writeBytes(sc.bytes)
|
||||
|
||||
// new version of the class, in this case the order of the parameters has been swapped
|
||||
data class C (val b: String, val a: Int)
|
||||
data class C(val b: String, val a: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
@ -79,13 +79,13 @@ class EvolvabilityTests {
|
||||
// f.writeBytes(sc.bytes)
|
||||
// println ("Path = $path")
|
||||
|
||||
data class C (val a: Int, val b: Int?)
|
||||
data class C(val a: Int, val b: Int?)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
|
||||
|
||||
assertEquals (A, deserializedC.a)
|
||||
assertEquals (null, deserializedC.b)
|
||||
assertEquals(A, deserializedC.a)
|
||||
assertEquals(null, deserializedC.b)
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
@ -104,7 +104,7 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path")
|
||||
|
||||
// new version of the class, in this case a new parameter has been added (b)
|
||||
data class C (val a: Int, val b: Int)
|
||||
data class C(val a: Int, val b: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
|
||||
@ -132,13 +132,13 @@ class EvolvabilityTests {
|
||||
// f.writeBytes(scc.bytes)
|
||||
// println ("Path = $path")
|
||||
|
||||
data class CC (val b: String, val d: Int)
|
||||
data class CC(val b: String, val d: Int)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (B, deserializedCC.b)
|
||||
assertEquals (D, deserializedCC.d)
|
||||
assertEquals(B, deserializedCC.b)
|
||||
assertEquals(D, deserializedCC.d)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
@ -185,16 +185,16 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class CC (val a: Int, val b: String) {
|
||||
data class CC(val a: Int, val b: String) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (a: Int) : this (a, "hello")
|
||||
constructor (a: Int) : this(a, "hello")
|
||||
}
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (A, deserializedCC.a)
|
||||
assertEquals ("hello", deserializedCC.b)
|
||||
assertEquals(A, deserializedCC.a)
|
||||
assertEquals("hello", deserializedCC.b)
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
@ -214,9 +214,9 @@ class EvolvabilityTests {
|
||||
// f.writeBytes(scc.bytes)
|
||||
// println ("Path = $path")
|
||||
|
||||
data class CC (val a: Int, val b: String) {
|
||||
data class CC(val a: Int, val b: String) {
|
||||
// constructor annotation purposefully omitted
|
||||
constructor (a: Int) : this (a, "hello")
|
||||
constructor (a: Int) : this(a, "hello")
|
||||
}
|
||||
|
||||
// we expect this to throw as we should not find any constructors
|
||||
@ -242,20 +242,20 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class CC (val a: Int, val b: Int, val c: String, val d: String) {
|
||||
data class CC(val a: Int, val b: Int, val c: String, val d: String) {
|
||||
// ensure none of the original parameters align with the initial
|
||||
// construction order
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble")
|
||||
constructor (c: String, a: Int, b: Int) : this(a, b, c, "wibble")
|
||||
}
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (A, deserializedCC.a)
|
||||
assertEquals (B, deserializedCC.b)
|
||||
assertEquals (C, deserializedCC.c)
|
||||
assertEquals ("wibble", deserializedCC.d)
|
||||
assertEquals(A, deserializedCC.a)
|
||||
assertEquals(B, deserializedCC.b)
|
||||
assertEquals(C, deserializedCC.c)
|
||||
assertEquals("wibble", deserializedCC.d)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -277,20 +277,20 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path")
|
||||
|
||||
// b is removed, d is added
|
||||
data class CC (val a: Int, val c: String, val d: String) {
|
||||
data class CC(val a: Int, val c: String, val d: String) {
|
||||
// ensure none of the original parameters align with the initial
|
||||
// construction order
|
||||
@Suppress("UNUSED")
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (c: String, a: Int) : this (a, c, "wibble")
|
||||
constructor (c: String, a: Int) : this(a, c, "wibble")
|
||||
}
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
|
||||
|
||||
assertEquals (A, deserializedCC.a)
|
||||
assertEquals (C, deserializedCC.c)
|
||||
assertEquals ("wibble", deserializedCC.d)
|
||||
assertEquals(A, deserializedCC.a)
|
||||
assertEquals(C, deserializedCC.c)
|
||||
assertEquals("wibble", deserializedCC.d)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -322,13 +322,15 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path1")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
|
||||
data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (b: Int, a: Int) : this (-1, -1, b, a, -1)
|
||||
constructor (b: Int, a: Int) : this(-1, -1, b, a, -1)
|
||||
|
||||
@DeprecatedConstructorForDeserialization(2)
|
||||
constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1)
|
||||
constructor (a: Int, c: Int, b: Int) : this(-1, c, b, a, -1)
|
||||
|
||||
@DeprecatedConstructorForDeserialization(3)
|
||||
constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d)
|
||||
constructor (a: Int, b: Int, c: Int, d: Int) : this(-1, c, b, a, d)
|
||||
}
|
||||
|
||||
val sb1 = File(path1.toURI()).readBytes()
|
||||
@ -376,15 +378,16 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path")
|
||||
|
||||
// Add a parameter to inner but keep outer unchanged
|
||||
data class Inner (val a: Int, val b: String?)
|
||||
data class Outer (val a: Int, val b: Inner)
|
||||
data class Inner(val a: Int, val b: String?)
|
||||
|
||||
data class Outer(val a: Int, val b: Inner)
|
||||
|
||||
val sc2 = f.readBytes()
|
||||
val outer = DeserializationInput(sf).deserialize(SerializedBytes<Outer>(sc2))
|
||||
|
||||
assertEquals (oa, outer.a)
|
||||
assertEquals (ia, outer.b.a)
|
||||
assertEquals (null, outer.b.b)
|
||||
assertEquals(oa, outer.a)
|
||||
assertEquals(ia, outer.b.a)
|
||||
assertEquals(null, outer.b.b)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -416,15 +419,18 @@ class EvolvabilityTests {
|
||||
// println ("Path = $path1")
|
||||
|
||||
@Suppress("UNUSED")
|
||||
data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {
|
||||
data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {
|
||||
@DeprecatedConstructorForDeserialization(1)
|
||||
constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1)
|
||||
constructor (b: Int, c: Int) : this(b, c, -1, -1, -1, -1)
|
||||
|
||||
@DeprecatedConstructorForDeserialization(2)
|
||||
constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1)
|
||||
constructor (b: Int, c: Int, d: Int) : this(b, c, d, -1, -1, -1)
|
||||
|
||||
@DeprecatedConstructorForDeserialization(3)
|
||||
constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1)
|
||||
constructor (b: Int, c: Int, d: Int, e: Int) : this(b, c, d, e, -1, -1)
|
||||
|
||||
@DeprecatedConstructorForDeserialization(4)
|
||||
constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1)
|
||||
constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this(b, c, d, e, f, -1)
|
||||
}
|
||||
|
||||
val sb1 = File(path1.toURI()).readBytes()
|
||||
|
@ -50,7 +50,7 @@ class SerializationOutputTests {
|
||||
|
||||
data class testShort(val s: Short)
|
||||
|
||||
data class testBoolean(val b : Boolean)
|
||||
data class testBoolean(val b: Boolean)
|
||||
|
||||
interface FooInterface {
|
||||
val pub: Int
|
||||
@ -145,13 +145,13 @@ class SerializationOutputTests {
|
||||
|
||||
data class PolymorphicProperty(val foo: FooInterface?)
|
||||
|
||||
private inline fun<reified T : Any> serdes(obj: T,
|
||||
factory: SerializerFactory = SerializerFactory (
|
||||
AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
freshDeserializationFactory: SerializerFactory = SerializerFactory(
|
||||
AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
expectedEqual: Boolean = true,
|
||||
expectDeserializedEqual: Boolean = true): T {
|
||||
private inline fun <reified T : Any> serdes(obj: T,
|
||||
factory: SerializerFactory = SerializerFactory(
|
||||
AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
freshDeserializationFactory: SerializerFactory = SerializerFactory(
|
||||
AllWhitelist, ClassLoader.getSystemClassLoader()),
|
||||
expectedEqual: Boolean = true,
|
||||
expectDeserializedEqual: Boolean = true): T {
|
||||
val ser = SerializationOutput(factory)
|
||||
val bytes = ser.serialize(obj)
|
||||
|
||||
@ -446,10 +446,10 @@ class SerializationOutputTests {
|
||||
try {
|
||||
try {
|
||||
throw IOException("Layer 1")
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
throw IllegalStateException("Layer 2", t)
|
||||
}
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
val desThrowable = serdesThrowableWithInternalInfo(t, factory, factory2, false)
|
||||
assertSerializedThrowableEquivalent(t, desThrowable)
|
||||
}
|
||||
@ -476,12 +476,12 @@ class SerializationOutputTests {
|
||||
try {
|
||||
try {
|
||||
throw IOException("Layer 1")
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
val e = IllegalStateException("Layer 2")
|
||||
e.addSuppressed(t)
|
||||
throw e
|
||||
}
|
||||
} catch(t: Throwable) {
|
||||
} catch (t: Throwable) {
|
||||
val desThrowable = serdesThrowableWithInternalInfo(t, factory, factory2, false)
|
||||
assertSerializedThrowableEquivalent(t, desThrowable)
|
||||
}
|
||||
@ -534,7 +534,22 @@ class SerializationOutputTests {
|
||||
}
|
||||
}
|
||||
|
||||
val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract"
|
||||
@Test
|
||||
fun `test custom object`() {
|
||||
serdes(FooContract)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Cannot serialize due to known Kotlin/serialization limitation")
|
||||
fun `test custom anonymous object`() {
|
||||
val anonymous: Contract = object : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
}
|
||||
}
|
||||
serdes(anonymous)
|
||||
}
|
||||
|
||||
private val FOO_PROGRAM_ID = "net.corda.nodeapi.internal.serialization.amqp.SerializationOutputTests.FooContract"
|
||||
class FooState : ContractState {
|
||||
override val participants: List<AbstractParty> = emptyList()
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ class SerializeAndReturnSchemaTest {
|
||||
@Test
|
||||
fun getSchema() {
|
||||
data class C(val a: Int, val b: Int)
|
||||
|
||||
val a = 1
|
||||
val b = 2
|
||||
|
||||
|
@ -0,0 +1,144 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import java.lang.reflect.Type
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class InStatic : Exception("Help!, help!, I'm being repressed")
|
||||
|
||||
class C {
|
||||
companion object {
|
||||
init {
|
||||
throw InStatic()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To re-setup the resource file for the tests
|
||||
// * deserializeTest
|
||||
// * deserializeTest2
|
||||
// comment out the companion object from here, comment out the test code and uncomment
|
||||
// the generation code, then re-run the test and copy the file shown in the output print
|
||||
// to the resource directory
|
||||
class C2(var b: Int) {
|
||||
/*
|
||||
companion object {
|
||||
init {
|
||||
throw InStatic()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
class StaticInitialisationOfSerializedObjectTest {
|
||||
@Test(expected = java.lang.ExceptionInInitializerError::class)
|
||||
fun itBlowsUp() {
|
||||
C()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun KotlinObjectWithCompanionObject() {
|
||||
data class D(val c: C)
|
||||
|
||||
val sf = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
|
||||
val typeMap = sf::class.java.getDeclaredField("serializersByType")
|
||||
typeMap.isAccessible = true
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val serialisersByType = typeMap.get(sf) as ConcurrentHashMap<Type, AMQPSerializer<Any>>
|
||||
|
||||
// pre building a serializer, we shouldn't have anything registered
|
||||
assertEquals(0, serialisersByType.size)
|
||||
|
||||
// build a serializer for type D without an instance of it to serialise, since
|
||||
// we can't actually construct one
|
||||
sf.get(null, D::class.java)
|
||||
|
||||
// post creation of the serializer we should have one element in the map, this
|
||||
// proves we didn't statically construct an instance of C when building the serializer
|
||||
assertEquals(1, serialisersByType.size)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun deserializeTest() {
|
||||
data class D(val c: C2)
|
||||
|
||||
val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest")
|
||||
val f = File(path.toURI())
|
||||
|
||||
// Original version of the class for the serialised version of this class
|
||||
//
|
||||
//val sf1 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
//val sc = SerializationOutput(sf1).serialize(D(C2(20)))
|
||||
//f.writeBytes(sc.bytes)
|
||||
//println (path)
|
||||
|
||||
class WL : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) =
|
||||
type.name == "net.corda.nodeapi.internal.serialization.amqp" +
|
||||
".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D"
|
||||
}
|
||||
|
||||
val sf2 = SerializerFactory(WL(), ClassLoader.getSystemClassLoader())
|
||||
val bytes = f.readBytes()
|
||||
|
||||
Assertions.assertThatThrownBy {
|
||||
DeserializationInput(sf2).deserialize(SerializedBytes<D>(bytes))
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
// Version of a serializer factory that will allow the class carpenter living on the
|
||||
// factory to have a different whitelist applied to it than the factory
|
||||
class TestSerializerFactory(wl1: ClassWhitelist, wl2: ClassWhitelist) :
|
||||
SerializerFactory(wl1, ClassLoader.getSystemClassLoader()) {
|
||||
override val classCarpenter = ClassCarpenter(ClassLoader.getSystemClassLoader(), wl2)
|
||||
}
|
||||
|
||||
// This time have the serilization factory and the carpenter use different whitelists
|
||||
@Test
|
||||
fun deserializeTest2() {
|
||||
data class D(val c: C2)
|
||||
|
||||
val path = EvolvabilityTests::class.java.getResource("StaticInitialisationOfSerializedObjectTest.deserializeTest2")
|
||||
val f = File(path.toURI())
|
||||
|
||||
// Original version of the class for the serialised version of this class
|
||||
//
|
||||
//val sf1 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
|
||||
//val sc = SerializationOutput(sf1).serialize(D(C2(20)))
|
||||
//f.writeBytes(sc.bytes)
|
||||
//println (path)
|
||||
|
||||
// whitelist to be used by the serialisation factory
|
||||
class WL1 : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) =
|
||||
type.name == "net.corda.nodeapi.internal.serialization.amqp" +
|
||||
".StaticInitialisationOfSerializedObjectTest\$deserializeTest\$D"
|
||||
}
|
||||
|
||||
// whitelist to be used by the carpenter
|
||||
class WL2 : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) = true
|
||||
}
|
||||
|
||||
val sf2 = TestSerializerFactory(WL1(), WL2())
|
||||
val bytes = f.readBytes()
|
||||
|
||||
// Deserializing should throw because C is not on the whitelist NOT because
|
||||
// we ever went anywhere near statically constructing it prior to not actually
|
||||
// creating an instance of it
|
||||
Assertions.assertThatThrownBy {
|
||||
DeserializationInput(sf2).deserialize(SerializedBytes<D>(bytes))
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import org.junit.Test
|
||||
import java.beans.Introspector
|
||||
@ -323,7 +324,6 @@ class ClassCarpenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun `int array`() {
|
||||
val className = "iEnjoyPotato"
|
||||
val schema = ClassSchema(
|
||||
@ -356,7 +356,6 @@ class ClassCarpenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun `integer array`() {
|
||||
val className = "iEnjoyFlan"
|
||||
val schema = ClassSchema(
|
||||
@ -366,16 +365,15 @@ class ClassCarpenterTest {
|
||||
val clazz = cc.build(schema)
|
||||
|
||||
val i = clazz.constructors[0].newInstance(arrayOf(1, 2, 3)) as SimpleFieldAccess
|
||||
val arr = clazz.getMethod("getA").invoke(i)
|
||||
val arr: Array<Int> = uncheckedCast(clazz.getMethod("getA").invoke(i))
|
||||
|
||||
assertEquals(1, (arr as Array<Int>)[0])
|
||||
assertEquals(1, arr[0])
|
||||
assertEquals(2, arr[1])
|
||||
assertEquals(3, arr[2])
|
||||
assertEquals("$className{a=[1, 2, 3]}", i.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun `int array with ints`() {
|
||||
val className = "iEnjoyCrumble"
|
||||
val schema = ClassSchema(
|
||||
@ -395,7 +393,6 @@ class ClassCarpenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun `multiple int arrays`() {
|
||||
val className = "iEnjoyJam"
|
||||
val schema = ClassSchema(
|
||||
@ -417,7 +414,6 @@ class ClassCarpenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun `string array`() {
|
||||
val className = "iEnjoyToast"
|
||||
val schema = ClassSchema(
|
||||
@ -427,7 +423,7 @@ class ClassCarpenterTest {
|
||||
val clazz = cc.build(schema)
|
||||
|
||||
val i = clazz.constructors[0].newInstance(arrayOf("toast", "butter", "jam"))
|
||||
val arr = clazz.getMethod("getA").invoke(i) as Array<String>
|
||||
val arr: Array<String> = uncheckedCast(clazz.getMethod("getA").invoke(i))
|
||||
|
||||
assertEquals("toast", arr[0])
|
||||
assertEquals("butter", arr[1])
|
||||
@ -435,7 +431,6 @@ class ClassCarpenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun `string arrays`() {
|
||||
val className = "iEnjoyToast"
|
||||
val schema = ClassSchema(
|
||||
@ -452,8 +447,8 @@ class ClassCarpenterTest {
|
||||
"and on the side",
|
||||
arrayOf("some pickles", "some fries"))
|
||||
|
||||
val arr1 = clazz.getMethod("getA").invoke(i) as Array<String>
|
||||
val arr2 = clazz.getMethod("getC").invoke(i) as Array<String>
|
||||
val arr1: Array<String> = uncheckedCast(clazz.getMethod("getA").invoke(i))
|
||||
val arr2: Array<String> = uncheckedCast(clazz.getMethod("getC").invoke(i))
|
||||
|
||||
assertEquals("bread", arr1[0])
|
||||
assertEquals("spread", arr1[1])
|
||||
|
@ -0,0 +1,103 @@
|
||||
package net.corda.nodeapi.internal.serialization.carpenter
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.io.NotSerializableException
|
||||
|
||||
class ClassCarpenterWhitelistTest {
|
||||
|
||||
// whitelisting a class on the class path will mean we will carpente up a class that
|
||||
// contains it as a member
|
||||
@Test
|
||||
fun whitelisted() {
|
||||
data class A(val a: Int)
|
||||
|
||||
class WL : ClassWhitelist {
|
||||
private val allowedClasses = hashSetOf<String>(
|
||||
A::class.java.name
|
||||
)
|
||||
|
||||
override fun hasListed(type: Class<*>): Boolean = type.name in allowedClasses
|
||||
}
|
||||
|
||||
val cc = ClassCarpenter(whitelist = WL())
|
||||
|
||||
// if this works, the test works, if it throws then we're in a world of pain, we could
|
||||
// go further but there are a lot of other tests that test weather we can build
|
||||
// carpented objects
|
||||
cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Currently the carpenter doesn't inspect it's whitelist so will carpent anything" +
|
||||
"it's asked relying on the serializer factory to not ask for anything")
|
||||
fun notWhitelisted() {
|
||||
data class A(val a: Int)
|
||||
|
||||
class WL : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) = false
|
||||
}
|
||||
|
||||
val cc = ClassCarpenter(whitelist = WL())
|
||||
|
||||
// Class A isn't on the whitelist, so we should fail to carpent it
|
||||
Assertions.assertThatThrownBy {
|
||||
cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java))))
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
// despite now being whitelisted and on the class path, we will carpent this because
|
||||
// it's marked as CordaSerializable
|
||||
@Test
|
||||
fun notWhitelistedButAnnotated() {
|
||||
@CordaSerializable data class A(val a: Int)
|
||||
|
||||
class WL : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) = false
|
||||
}
|
||||
|
||||
val cc = ClassCarpenter(whitelist = WL())
|
||||
|
||||
// again, simply not throwing here is enough to show the test worked and the carpenter
|
||||
// didn't reject the type even though it wasn't on the whitelist because it was
|
||||
// annotated properly
|
||||
cc.build(ClassSchema("thing", mapOf("a" to NonNullableField(A::class.java))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Currently the carpenter doesn't inspect it's whitelist so will carpent anything" +
|
||||
"it's asked relying on the serializer factory to not ask for anything")
|
||||
fun notWhitelistedButCarpented() {
|
||||
// just have the white list reject *Everything* except ints
|
||||
class WL : ClassWhitelist {
|
||||
override fun hasListed(type: Class<*>) = type.name == "int"
|
||||
}
|
||||
|
||||
val cc = ClassCarpenter(whitelist = WL())
|
||||
|
||||
val schema1a = ClassSchema("thing1a", mapOf("a" to NonNullableField(Int::class.java)))
|
||||
|
||||
// thing 1 won't be set as corda serializable, meaning we won't build schema 2
|
||||
schema1a.unsetCordaSerializable()
|
||||
|
||||
val clazz1a = cc.build(schema1a)
|
||||
val schema2 = ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1a)))
|
||||
|
||||
// thing 2 references thing 1 which wasn't carpented as corda s erializable and thus
|
||||
// this will fail
|
||||
Assertions.assertThatThrownBy {
|
||||
cc.build(schema2)
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
|
||||
// create a second type of schema1, this time leave it as corda serialzable
|
||||
val schema1b = ClassSchema("thing1b", mapOf("a" to NonNullableField(Int::class.java)))
|
||||
|
||||
val clazz1b = cc.build(schema1b)
|
||||
|
||||
// since schema 1b was created as CordaSerializable this will work
|
||||
ClassSchema("thing2", mapOf("a" to NonNullableField(clazz1b)))
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user