CORDA-1238: Moved the blob inspector out of experimental and wired it to JackonSupport (#3224)

The existing output format was not complete and so was deleted to avoid it becoming a tech debt. We can always resurrect it at a later point.
This commit is contained in:
Shams Asari 2018-05-24 18:26:55 +01:00 committed by GitHub
parent 15b262f25f
commit 4e0378de9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 461 additions and 1168 deletions

4
.idea/compiler.xml generated
View File

@ -71,8 +71,6 @@
<module name="example-code_integrationTest" target="1.8" />
<module name="example-code_main" target="1.8" />
<module name="example-code_test" target="1.8" />
<module name="experimental-blobinspector_main" target="1.8" />
<module name="experimental-blobinspector_test" target="1.8" />
<module name="experimental-kryo-hook_main" target="1.8" />
<module name="experimental-kryo-hook_test" target="1.8" />
<module name="experimental_main" target="1.8" />
@ -173,6 +171,8 @@
<module name="testing-test-common_test" target="1.8" />
<module name="testing-test-utils_main" target="1.8" />
<module name="testing-test-utils_test" target="1.8" />
<module name="tools-blobinspector_main" target="1.8" />
<module name="tools-blobinspector_test" target="1.8" />
<module name="tools_main" target="1.8" />
<module name="tools_test" target="1.8" />
<module name="trader-demo_integrationTest" target="1.8" />

View File

@ -79,6 +79,7 @@ buildscript {
ext.protonj_version = '0.27.1'
ext.snappy_version = '0.4'
ext.fast_classpath_scanner_version = '2.12.3'
ext.jcabi_manifests_version = '1.1'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'

View File

@ -6,11 +6,8 @@ apply plugin: 'com.jfrog.artifactory'
dependencies {
compile project(':serialization')
testCompile project(':test-utils')
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Jackson and its plugins: parsing to/from JSON and other textual formats.
compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"
// Yaml is useful for parsing strings to method calls.
@ -19,7 +16,9 @@ dependencies {
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
compile "com.google.guava:guava:$guava_version"
testCompile project(':test-utils')
testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testCompile "junit:junit:$junit_version"
}

View File

@ -21,10 +21,7 @@ import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.*
import net.corda.core.internal.CertRole
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.uncheckedCast
@ -55,12 +52,13 @@ import javax.security.auth.x500.X500Principal
*
* Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML.
*/
@Suppress("DEPRECATION")
@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate")
object JacksonSupport {
// If you change this API please update the docs in the docsite (json.rst)
@DoNotImplement
interface PartyObjectMapper {
val isFullParties: Boolean
fun wellKnownPartyFromX500Name(name: CordaX500Name): Party?
fun partyFromKey(owningKey: PublicKey): Party?
fun partiesFromName(query: String): Set<Party>
@ -68,9 +66,11 @@ object JacksonSupport {
}
@Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createDefaultMapper"))
class RpcObjectMapper(val rpc: CordaRPCOps,
factory: JsonFactory,
val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
class RpcObjectMapper
@JvmOverloads constructor(val rpc: CordaRPCOps,
factory: JsonFactory,
val fuzzyIdentityMatch: Boolean,
override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) {
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name)
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
@ -78,9 +78,11 @@ object JacksonSupport {
}
@Deprecated("This is an internal class, do not use")
class IdentityObjectMapper(val identityService: IdentityService,
factory: JsonFactory,
val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
class IdentityObjectMapper
@JvmOverloads constructor(val identityService: IdentityService,
factory: JsonFactory,
val fuzzyIdentityMatch: Boolean,
override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) {
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = identityService.wellKnownPartyFromX500Name(name)
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
@ -88,7 +90,9 @@ object JacksonSupport {
}
@Deprecated("This is an internal class, do not use", replaceWith = ReplaceWith("JacksonSupport.createNonRpcMapper"))
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
class NoPartyObjectMapper
@JvmOverloads constructor(factory: JsonFactory,
override val isFullParties: Boolean = false) : PartyObjectMapper, ObjectMapper(factory) {
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = null
override fun partyFromKey(owningKey: PublicKey): Party? = null
override fun partiesFromName(query: String): Set<Party> = emptySet()
@ -102,22 +106,33 @@ object JacksonSupport {
/**
* Creates a Jackson ObjectMapper that uses RPC to deserialise parties from string names.
*
* If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely
* @param fuzzyIdentityMatch If false, fields mapped to [Party] objects must be in X.500 name form and precisely
* match an identity known from the network map. If true, the name is matched more leniently but if the match
* is ambiguous a [JsonParseException] is thrown.
*
* @param fullParties If true then [Party] objects will be serialised as JSON objects, with the owning key serialised
* in addition to the name. For [PartyAndCertificate] objects the cert path will be included.
*/
@JvmStatic
@JvmOverloads
fun createDefaultMapper(rpc: CordaRPCOps,
factory: JsonFactory = JsonFactory(),
fuzzyIdentityMatch: Boolean = false): ObjectMapper {
return configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch))
fuzzyIdentityMatch: Boolean = false,
fullParties: Boolean = false): ObjectMapper {
return configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch, fullParties))
}
/** For testing or situations where deserialising parties is not required */
/**
* For testing or situations where deserialising parties is not required
*
* @param fullParties If true then [Party] objects will be serialised as JSON objects, with the owning key serialised
* in addition to the name. For [PartyAndCertificate] objects the cert path will be included.
*/
@JvmStatic
@JvmOverloads
fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory))
fun createNonRpcMapper(factory: JsonFactory = JsonFactory(), fullParties: Boolean = false): ObjectMapper {
return configureMapper(NoPartyObjectMapper(factory, fullParties))
}
/**
* Creates a Jackson ObjectMapper that uses an [IdentityService] directly inside the node to deserialise parties from string names.
@ -197,7 +212,14 @@ object JacksonSupport {
.filter { Modifier.isStatic(it.modifiers) && it.type == KeyPurposeId::class.java }
.associateBy({ (it.get(null) as KeyPurposeId).id }, { it.name })
val knownExtensions = setOf("2.5.29.15", "2.5.29.37", "2.5.29.19", "2.5.29.17", "2.5.29.18", CordaOID.X509_EXTENSION_CORDA_ROLE)
val knownExtensions = setOf(
"2.5.29.15",
"2.5.29.17",
"2.5.29.18",
"2.5.29.19",
"2.5.29.37",
CordaOID.X509_EXTENSION_CORDA_ROLE
)
override fun serialize(value: X509Certificate, gen: JsonGenerator, serializers: SerializerProvider) {
gen.jsonObject {
@ -208,17 +230,20 @@ object JacksonSupport {
writeObjectField("issuer", value.issuerX500Principal)
writeObjectField("notBefore", value.notBefore)
writeObjectField("notAfter", value.notAfter)
writeObjectField("cordaCertRole", CertRole.extract(value))
writeObjectField("issuerUniqueID", value.issuerUniqueID)
writeObjectField("subjectUniqueID", value.subjectUniqueID)
writeObjectField("keyUsage", value.keyUsage?.asList()?.mapIndexedNotNull { i, flag -> if (flag) keyUsages[i] else null })
writeObjectField("extendedKeyUsage", value.extendedKeyUsage.map { keyPurposeIds.getOrDefault(it, it) })
jsonObject("basicConstraints") {
writeBooleanField("isCA", value.basicConstraints != -1)
writeObjectField("pathLength", value.basicConstraints.let { if (it != Int.MAX_VALUE) it else null })
val isCa = value.basicConstraints != -1
writeBooleanField("isCA", isCa)
if (isCa) {
writeObjectField("pathLength", value.basicConstraints.let { if (it != Int.MAX_VALUE) it else null })
}
}
writeObjectField("subjectAlternativeNames", value.subjectAlternativeNames)
writeObjectField("issuerAlternativeNames", value.issuerAlternativeNames)
writeObjectField("cordaCertRole", CertRole.extract(value))
writeObjectField("otherCriticalExtensions", value.criticalExtensionOIDs - knownExtensions)
writeObjectField("otherNonCriticalExtensions", value.nonCriticalExtensionOIDs - knownExtensions)
writeBinaryField("encoded", value.encoded)
@ -229,8 +254,12 @@ object JacksonSupport {
private class X509CertificateDeserializer : JsonDeserializer<X509Certificate>() {
private val certFactory = CertificateFactory.getInstance("X.509")
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): X509Certificate {
val encoded = parser.readValueAsTree<ObjectNode>()["encoded"]
return certFactory.generateCertificate(encoded.binaryValue().inputStream()) as X509Certificate
val encoded = if (parser.currentToken == JsonToken.START_OBJECT) {
parser.readValueAsTree<ObjectNode>()["encoded"].binaryValue()
} else {
parser.binaryValue
}
return certFactory.generateCertificate(encoded.inputStream()) as X509Certificate
}
}
@ -274,9 +303,13 @@ object JacksonSupport {
@Deprecated("This is an internal class, do not use")
object PartySerializer : JsonSerializer<Party>() {
override fun serialize(value: Party, generator: JsonGenerator, provider: SerializerProvider) {
// TODO Add configurable option to output this as an object which includes the owningKey
generator.writeObject(value.name)
override fun serialize(value: Party, gen: JsonGenerator, provider: SerializerProvider) {
val mapper = gen.codec as PartyObjectMapper
if (mapper.isFullParties) {
gen.writeObject(PartyAnalogue(value.name, value.owningKey))
} else {
gen.writeObject(value.name)
}
}
}
@ -284,28 +317,39 @@ object JacksonSupport {
object PartyDeserializer : JsonDeserializer<Party>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Party {
val mapper = parser.codec as PartyObjectMapper
// The comma character is invalid in Base58, and required as a separator for X.500 names. As Corda
// X.500 names all involve at least three attributes (organisation, locality, country), they must
// include a comma. As such we can use it as a distinguisher between the two types.
return if ("," in parser.text) {
val principal = CordaX500Name.parse(parser.text)
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
return if (parser.currentToken == JsonToken.START_OBJECT) {
val analogue = parser.readValueAs<PartyAnalogue>()
Party(analogue.name, analogue.owningKey)
} else {
val nameMatches = mapper.partiesFromName(parser.text)
when {
nameMatches.isEmpty() -> {
val publicKey = parser.readValueAs<PublicKey>()
mapper.partyFromKey(publicKey)
?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}")
}
nameMatches.size == 1 -> nameMatches.first()
else -> throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " +
nameMatches.map { it.name }.joinToString(" ... or ... "))
// The comma character is invalid in Base58, and required as a separator for X.500 names. As Corda
// X.500 names all involve at least three attributes (organisation, locality, country), they must
// include a comma. As such we can use it as a distinguisher between the two types.
if ("," in parser.text) {
val principal = CordaX500Name.parse(parser.text)
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
} else {
lookupByNameSegment(mapper, parser)
}
}
}
private fun lookupByNameSegment(mapper: PartyObjectMapper, parser: JsonParser): Party {
val nameMatches = mapper.partiesFromName(parser.text)
return when {
nameMatches.isEmpty() -> {
val publicKey = parser.readValueAs<PublicKey>()
mapper.partyFromKey(publicKey)
?: throw JsonParseException(parser, "Could not find a Party with key ${publicKey.toStringShort()}")
}
nameMatches.size == 1 -> nameMatches.first()
else -> throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " +
nameMatches.map { it.name }.joinToString(" ... or ... "))
}
}
}
private class PartyAnalogue(val name: CordaX500Name, val owningKey: PublicKey)
@Deprecated("This is an internal class, do not use")
object CordaX500NameDeserializer : JsonDeserializer<CordaX500Name>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): CordaX500Name {

View File

@ -29,9 +29,10 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.SerializerFactory
import net.corda.serialization.internal.amqp.constructorForDeserialization
import net.corda.serialization.internal.amqp.createSerializerFactoryFactory
import net.corda.serialization.internal.amqp.hasCordaSerializable
import net.corda.serialization.internal.amqp.propertiesForSerialization
import java.security.PublicKey
import java.security.cert.CertPath
class CordaModule : SimpleModule("corda-core") {
override fun setupModule(context: SetupContext) {
@ -39,7 +40,7 @@ class CordaModule : SimpleModule("corda-core") {
context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier())
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateSerializerMixin::class.java)
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
context.setMixInAnnotations(CordaX500Name::class.java, CordaX500NameMixin::class.java)
context.setMixInAnnotations(Amount::class.java, AmountMixin::class.java)
@ -53,7 +54,7 @@ class CordaModule : SimpleModule("corda-core") {
context.setMixInAnnotations(DigitalSignature.WithKey::class.java, ByteSequenceWithPropertiesMixin::class.java)
context.setMixInAnnotations(DigitalSignatureWithCert::class.java, ByteSequenceWithPropertiesMixin::class.java)
context.setMixInAnnotations(TransactionSignature::class.java, ByteSequenceWithPropertiesMixin::class.java)
context.setMixInAnnotations(SignedTransaction::class.java, SignedTransactionMixin2::class.java)
context.setMixInAnnotations(SignedTransaction::class.java, SignedTransactionMixin::class.java)
context.setMixInAnnotations(WireTransaction::class.java, JacksonSupport.WireTransactionMixin::class.java)
context.setMixInAnnotations(NodeInfo::class.java, NodeInfoMixin::class.java)
}
@ -69,12 +70,15 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
override fun changeProperties(config: SerializationConfig,
beanDesc: BeanDescription,
beanProperties: MutableList<BeanPropertyWriter>): MutableList<BeanPropertyWriter> {
// TODO We're assuming here that Jackson gives us a superset of all the properties. Either confirm this or
// make sure the returned beanProperties are exactly the AMQP properties
if (beanDesc.beanClass.isAnnotationPresent(CordaSerializable::class.java)) {
if (hasCordaSerializable(beanDesc.beanClass)) {
val ctor = constructorForDeserialization(beanDesc.beanClass)
val amqpProperties = propertiesForSerialization(ctor, beanDesc.beanClass, serializerFactory).serializationOrder
beanProperties.removeIf { bean -> amqpProperties.none { amqp -> amqp.serializer.name == bean.name } }
val amqpProperties = propertiesForSerialization(ctor, beanDesc.beanClass, serializerFactory)
.serializationOrder
.map { it.serializer.name }
beanProperties.removeIf { it.name !in amqpProperties }
(amqpProperties - beanProperties.map { it.name }).let {
check(it.isEmpty()) { "Jackson didn't provide serialisers for $it" }
}
}
return beanProperties
}
@ -85,26 +89,31 @@ private class CordaSerializableBeanSerializerModifier : BeanSerializerModifier()
private interface NetworkHostAndPortMixin
private class NetworkHostAndPortDeserializer : JsonDeserializer<NetworkHostAndPort>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext) = NetworkHostAndPort.parse(parser.text)
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): NetworkHostAndPort {
return NetworkHostAndPort.parse(parser.text)
}
}
@JsonSerialize(using = PartyAndCertificateSerializer::class)
// TODO Add deserialization which follows the same lookup logic as Party
private interface PartyAndCertificateSerializerMixin
private interface PartyAndCertificateMixin
private class PartyAndCertificateSerializer : JsonSerializer<PartyAndCertificate>() {
override fun serialize(value: PartyAndCertificate, gen: JsonGenerator, serializers: SerializerProvider) {
gen.jsonObject {
writeObjectField("name", value.name)
writeObjectField("owningKey", value.owningKey)
// TODO Add configurable option to output the certPath
val mapper = gen.codec as JacksonSupport.PartyObjectMapper
if (mapper.isFullParties) {
gen.writeObject(PartyAndCertificateWrapper(value.name, value.certPath))
} else {
gen.writeObject(value.party)
}
}
}
private class PartyAndCertificateWrapper(val name: CordaX500Name, val certPath: CertPath)
@JsonSerialize(using = SignedTransactionSerializer::class)
@JsonDeserialize(using = SignedTransactionDeserializer::class)
private interface SignedTransactionMixin2
private interface SignedTransactionMixin
private class SignedTransactionSerializer : JsonSerializer<SignedTransaction>() {
override fun serialize(value: SignedTransaction, gen: JsonGenerator, serializers: SerializerProvider) {

View File

@ -14,10 +14,7 @@ import net.corda.core.contracts.Amount
import net.corda.core.cordapp.CordappProvider
import net.corda.core.crypto.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.*
import net.corda.core.internal.DigitalSignatureWithCert
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
@ -247,10 +244,20 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString())
}
@Test
fun `Party serialization with isFullParty = true`() {
partyObjectMapper.isFullParties = true
val json = mapper.valueToTree<ObjectNode>(MINI_CORP.party)
val (name, owningKey) = json.assertHasOnlyFields("name", "owningKey")
assertThat(name.valueAs<CordaX500Name>(mapper)).isEqualTo(MINI_CORP.name)
assertThat(owningKey.valueAs<PublicKey>(mapper)).isEqualTo(MINI_CORP.publicKey)
}
@Test
fun `Party deserialization on full name`() {
fun convertToParty() = mapper.convertValue<Party>(TextNode(MINI_CORP.name.toString()))
// Check that it fails if it can't find the party
assertThatThrownBy { convertToParty() }
partyObjectMapper.identities += MINI_CORP.party
@ -261,6 +268,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
fun `Party deserialization on part of name`() {
fun convertToParty() = mapper.convertValue<Party>(TextNode(MINI_CORP.name.organisation))
// Check that it fails if it can't find the party
assertThatThrownBy { convertToParty() }
partyObjectMapper.identities += MINI_CORP.party
@ -271,12 +279,24 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
fun `Party deserialization on public key`() {
fun convertToParty() = mapper.convertValue<Party>(TextNode(MINI_CORP.publicKey.toBase58String()))
// Check that it fails if it can't find the party
assertThatThrownBy { convertToParty() }
partyObjectMapper.identities += MINI_CORP.party
assertThat(convertToParty()).isEqualTo(MINI_CORP.party)
}
@Test
fun `Party deserialization on name and key`() {
val party = mapper.convertValue<Party>(mapOf(
"name" to MINI_CORP.name,
"owningKey" to MINI_CORP.publicKey
))
// Party.equals is only defined on the public key so we must check the name as well
assertThat(party.name).isEqualTo(MINI_CORP.name)
assertThat(party.owningKey).isEqualTo(MINI_CORP.publicKey)
}
@Test
fun PublicKey() {
val json = mapper.valueToTree<TextNode>(MINI_CORP.publicKey)
@ -316,15 +336,31 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
}
@Test
fun `PartyAndCertificate serialisation`() {
val json = mapper.valueToTree<ObjectNode>(MINI_CORP.identity)
val (name, owningKey) = json.assertHasOnlyFields("name", "owningKey")
assertThat(name.valueAs<CordaX500Name>(mapper)).isEqualTo(MINI_CORP.name)
assertThat(owningKey.valueAs<PublicKey>(mapper)).isEqualTo(MINI_CORP.publicKey)
fun `PartyAndCertificate serialization`() {
val json = mapper.valueToTree<TextNode>(MINI_CORP.identity)
assertThat(json.textValue()).isEqualTo(MINI_CORP.name.toString())
}
@Test
fun `NodeInfo serialisation`() {
fun `PartyAndCertificate serialization with isFullParty = true`() {
partyObjectMapper.isFullParties = true
val json = mapper.valueToTree<ObjectNode>(MINI_CORP.identity)
println(mapper.writeValueAsString(json))
val (name, certPath) = json.assertHasOnlyFields("name", "certPath")
assertThat(name.valueAs<CordaX500Name>(mapper)).isEqualTo(MINI_CORP.name)
assertThat(certPath.valueAs<CertPath>(mapper)).isEqualTo(MINI_CORP.identity.certPath)
}
@Test
fun `PartyAndCertificate deserialization on cert path`() {
val certPathJson = mapper.valueToTree<JsonNode>(MINI_CORP.identity.certPath)
val partyAndCert = mapper.convertValue<PartyAndCertificate>(mapOf("certPath" to certPathJson))
// PartyAndCertificate.equals is defined on the Party so we must check the certPath directly
assertThat(partyAndCert.certPath).isEqualTo(MINI_CORP.identity.certPath)
}
@Test
fun `NodeInfo serialization`() {
val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
val json = mapper.valueToTree<ObjectNode>(nodeInfo)
val (addresses, legalIdentitiesAndCerts, platformVersion, serial) = json.assertHasOnlyFields(
@ -339,14 +375,14 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
}
legalIdentitiesAndCerts.run {
assertThat(this).hasSize(1)
assertThat(this[0]["name"].valueAs<CordaX500Name>(mapper)).isEqualTo(ALICE_NAME)
assertThat(this[0].valueAs<CordaX500Name>(mapper)).isEqualTo(ALICE_NAME)
}
assertThat(platformVersion.intValue()).isEqualTo(nodeInfo.platformVersion)
assertThat(serial.longValue()).isEqualTo(nodeInfo.serial)
}
@Test
fun `NodeInfo deserialisation on name`() {
fun `NodeInfo deserialization on name`() {
val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
fun convertToNodeInfo() = mapper.convertValue<NodeInfo>(TextNode(ALICE_NAME.toString()))
@ -359,7 +395,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
}
@Test
fun `NodeInfo deserialisation on public key`() {
fun `NodeInfo deserialization on public key`() {
val (nodeInfo) = createNodeInfoAndSigned(ALICE_NAME)
fun convertToNodeInfo() = mapper.convertValue<NodeInfo>(TextNode(nodeInfo.legalIdentities[0].owningKey.toBase58String()))
@ -386,7 +422,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
}
@Test
fun X509Certificate() {
fun `X509Certificate serialization`() {
val cert: X509Certificate = MINI_CORP.identity.certificate
val json = mapper.valueToTree<ObjectNode>(cert)
println(mapper.writeValueAsString(json))
@ -397,7 +433,13 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
assertThat(json["notAfter"].valueAs<Date>(mapper)).isEqualTo(cert.notAfter)
assertThat(json["notBefore"].valueAs<Date>(mapper)).isEqualTo(cert.notBefore)
assertThat(json["encoded"].binaryValue()).isEqualTo(cert.encoded)
assertThat(mapper.convertValue<X509Certificate>(json).encoded).isEqualTo(cert.encoded)
}
@Test
fun `X509Certificate deserialization`() {
val cert: X509Certificate = MINI_CORP.identity.certificate
assertThat(mapper.convertValue<X509Certificate>(mapOf("encoded" to cert.encoded))).isEqualTo(cert)
assertThat(mapper.convertValue<X509Certificate>(BinaryNode(cert.encoded))).isEqualTo(cert)
}
@Test
@ -448,6 +490,7 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
}
private class TestPartyObjectMapper : JacksonSupport.PartyObjectMapper {
override var isFullParties: Boolean = false
val identities = ArrayList<Party>()
val nodes = ArrayList<NodeInfo>()
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? {

View File

@ -80,6 +80,17 @@ import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
val Throwable.rootCause: Throwable get() = cause?.rootCause ?: this
val Throwable.rootMessage: String? get() {
var message = this.message
var throwable = cause
while (throwable != null) {
if (throwable.message != null) {
message = throwable.message
}
throwable = throwable.cause
}
return message
}
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)

View File

@ -209,7 +209,8 @@ object SerializationDefaults {
/**
* Convenience extension method for deserializing a ByteSequence, utilising the defaults.
*/
inline fun <reified T : Any> ByteSequence.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T {
inline fun <reified T : Any> ByteSequence.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): T {
return serializationFactory.deserialize(this, T::class.java, context)
}
@ -218,31 +219,40 @@ inline fun <reified T : Any> ByteSequence.deserialize(serializationFactory: Seri
* It might be helpful to know [SerializationContext] to use the same encoding in the reply.
*/
inline fun <reified T : Any> ByteSequence.deserializeWithCompatibleContext(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext<T> {
context: SerializationContext = serializationFactory.defaultContext): ObjectWithCompatibleContext<T> {
return serializationFactory.deserializeWithCompatibleContext(this, T::class.java, context)
}
/**
* Convenience extension method for deserializing SerializedBytes with type matching, utilising the defaults.
*/
inline fun <reified T : Any> SerializedBytes<T>.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T {
inline fun <reified T : Any> SerializedBytes<T>.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): T {
return serializationFactory.deserialize(this, T::class.java, context)
}
/**
* Convenience extension method for deserializing a ByteArray, utilising the defaults.
*/
inline fun <reified T : Any> ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.sequence().deserialize(serializationFactory, context)
inline fun <reified T : Any> ByteArray.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): T {
require(isNotEmpty()) { "Empty bytes" }
return this.sequence().deserialize(serializationFactory, context)
}
/**
* Convenience extension method for deserializing a JDBC Blob, utilising the defaults.
*/
inline fun <reified T : Any> Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): T = this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context)
inline fun <reified T : Any> Blob.deserialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): T {
return this.getBytes(1, this.length().toInt()).deserialize(serializationFactory, context)
}
/**
* Convenience extension method for serializing an object of type T, utilising the defaults.
*/
fun <T : Any> T.serialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory, context: SerializationContext = serializationFactory.defaultContext): SerializedBytes<T> {
fun <T : Any> T.serialize(serializationFactory: SerializationFactory = SerializationFactory.defaultFactory,
context: SerializationContext = serializationFactory.defaultContext): SerializedBytes<T> {
return serializationFactory.serialize(this, context)
}

View File

@ -0,0 +1,63 @@
Blob Inspector
==============
There are many benefits to having a custom binary serialisation format (see :doc:`serialization` for details) but one
disadvantage is the inability to view the contents in a human-friendly manner. The blob inspector tool alleviates this issue
by allowing the contents of a binary blob file (or URL end-point) to be output in either YAML or JSON. It uses
``JacksonSupport`` to do this (see :doc:`json`).
The latest version of the tool can be downloaded from `here <https://www.corda.net/downloads/>`_.
To run simply pass in the file or URL as the first parameter:
``java -jar blob-inspector.jar <file or URL>``
Use the ``--help`` flag for a full list of command line options.
``SerializedBytes`
~~~~~~~~~~~~~~~~~~
One thing to note is that the binary blob may contain embedded ``SerializedBytes`` objects. Rather than printing these
out as a Base64 string, the blob inspector will first materialise them into Java objects and then output those. You will
see this when dealing with classes such as ``SignedData`` or other structures that attach a signature, such as the
``nodeInfo-*`` files or the ``network-parameters`` file in the node's directory. For example, the output of a node-info
file may look like:
.. container:: codeset
.. sourcecode:: yaml
net.corda.nodeapi.internal.SignedNodeInfo
---
raw:
class: "net.corda.core.node.NodeInfo"
deserialized:
addresses:
- "localhost:10011"
legalIdentitiesAndCerts:
- "O=BankOfCorda, L=New York, C=US"
platformVersion: 4
serial: 1527074180971
signatures:
- !!binary |
dmoAnnzcv0MzRN+3ZSCDcCJIAbXnoYy5mFWB3Nijndzu/dzIoYdIawINXbNSY/5z2XloDK01vZRV
TreFZCbZAg==
.. sourcecode:: json
net.corda.nodeapi.internal.SignedNodeInfo
{
"raw" : {
"class" : "net.corda.core.node.NodeInfo",
"deserialized" : {
"addresses" : [ "localhost:10011" ],
"legalIdentitiesAndCerts" : [ "O=BankOfCorda, L=New York, C=US" ],
"platformVersion" : 4,
"serial" : 1527074180971
}
},
"signatures" : [ "dmoAnnzcv0MzRN+3ZSCDcCJIAbXnoYy5mFWB3Nijndzu/dzIoYdIawINXbNSY/5z2XloDK01vZRVTreFZCbZAg==" ]
}
Notice the file is actually a serialised ``SignedNodeInfo`` object, which has a ``raw`` property of type ``SerializedBytes<NodeInfo>``.
This property is materialised into a ``NodeInfo`` and is output under the ``deserialized`` field.

View File

@ -34,12 +34,19 @@ Unreleased
* ``Party`` objects can be deserialised by looking up their public key, in addition to their name
* ``NodeInfo`` objects are serialised as an object and can be looked up using the same mechanism as ``Party``
* ``NetworkHostAndPort`` serialised according to its ``toString()``
* ``PartyAndCertificate`` is serialised as an object containing the name and owning key
* ``SerializedBytes`` is serialised by converting the bytes into the object it represents, which is then serialised into
a JSON/YAML object
* ``CertPath`` and ``X509Certificate`` are serialised as objects and can be deserialised back
* ``PartyAndCertificate`` is serialised as the name
* ``SerializedBytes`` is serialised by materialising the bytes into the object it represents, and then serialising that
object into YAML/JSON
* ``X509Certificate`` is serialised as an object with key fields such as ``issuer``, ``publicKey``, ``serialNumber``, etc.
The encoded bytes are also serialised into the ``encoded`` field. This can be used to deserialise an ``X509Certificate``
back.
* ``CertPath`` objects are serialised as a list of ``X509Certificate`` objects.
* ``SignedTransaction`` is serialised into its ``txBits`` and ``signatures`` and can be deserialised back
* ``fullParties`` boolean parameter added to ``JacksonSupport.createDefaultMapper`` and ``createNonRpcMapper``. If ``true``
then ``Party`` objects are serialised as JSON objects with the ``name`` and ``owningKey`` fields. For ``PartyAndCertificate``
the ``certPath`` is serialised.
* Several members of ``JacksonSupport`` have been deprecated to highlight that they are internal and not to be used.
* The Vault Criteria API has been extended to take a more precise specification of which class contains a field. This

View File

@ -4,6 +4,7 @@ Tools
.. toctree::
:maxdepth: 1
blob-inspector
network-simulator
demobench
node-explorer

View File

@ -1,52 +0,0 @@
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName = 'net.corda.blobinspector.MainKt'
dependencies {
compile project(':core')
compile project(':node-api')
compile "commons-cli:commons-cli:$commons_cli_version"
testCompile project(':test-utils')
testCompile "junit:junit:$junit_version"
}
/**
* To run from within gradle use
*
* ./gradlew -PrunArgs="<cmd> <line> <args>" :experimental:blobinspector:run
*
* For example, to parse a file from the command line and print out the deserialized properties
*
* ./gradlew -PrunArgs="-f <path/to/file> -d" :experimental:blobinspector:run
*
* at the command line.
*/
run {
if (project.hasProperty('runArgs')) {
args = [ project.findProperty('runArgs').toString().split(" ") ].flatten()
}
if (System.properties.getProperty('consoleLogLevel') != null) {
logging.captureStandardOutput(LogLevel.valueOf(System.properties.getProperty('consoleLogLevel')))
logging.captureStandardError(LogLevel.valueOf(System.properties.getProperty('consoleLogLevel')))
systemProperty "consoleLogLevel", System.properties.getProperty('consoleLogLevel')
}
}
/**
* Build a executable jar
*/
jar {
baseName 'blobinspector'
manifest {
attributes(
'Automatic-Module-Name': 'net.corda.experimental.blobinspector',
'Main-Class': 'net.corda.blobinspector.MainKt'
)
}
}

View File

@ -1,405 +0,0 @@
package net.corda.blobinspector
import net.corda.core.crypto.SecureHash
import net.corda.core.serialization.EncodingWhitelist
import net.corda.core.serialization.SerializationEncoding
import net.corda.core.utilities.ByteSequence
import net.corda.serialization.internal.SerializationFactoryImpl
import net.corda.serialization.internal.amqp.CompositeType
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.RestrictedType
import net.corda.serialization.internal.amqp.TypeNotation
import net.corda.serialization.internal.amqp.amqpMagic
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.Symbol
/**
* Print a string to the console only if the verbose config option is set.
*/
fun String.debug(config: Config) {
if (config.verbose) {
println(this)
}
}
/**
*
*/
interface Stringify {
fun stringify(sb: IndentingStringBuilder)
}
/**
* Makes classnames easier to read by stripping off the package names from the class and separating nested
* classes
*
* For example:
*
* net.corda.blobinspector.Class1<net.corda.blobinspector.Class2>
* Class1 <Class2>
*
* net.corda.blobinspector.Class1<net.corda.blobinspector.Class2, net.corda.blobinspector.Class3>
* Class1 <Class2, Class3>
*
* net.corda.blobinspector.Class1<net.corda.blobinspector.Class2<net.corda.blobinspector.Class3>>
* Class1 <Class2 <Class3>>
*
* net.corda.blobinspector.Class1<net.corda.blobinspector.Class2<net.corda.blobinspector.Class3>>
* Class1 :: C <Class2 <Class3>>
*/
fun String.simplifyClass(): String {
return if (this.endsWith('>')) {
val templateStart = this.indexOf('<')
val clazz = (this.substring(0, templateStart))
val params = this.substring(templateStart+1, this.length-1).split(',').joinToString { it.simplifyClass() }
"${clazz.simplifyClass()} <$params>"
}
else {
substring(this.lastIndexOf('.') + 1).replace("$", " :: ")
}
}
/**
* Represents the deserialized form of the property of an Object
*
* @param name
* @param type
*/
abstract class Property(
val name: String,
val type: String) : Stringify
/**
* Derived class of [Property], represents properties of an object that are non compelex, such
* as any POD type or String
*/
class PrimProperty(
name: String,
type: String,
private val value: String) : Property(name, type) {
override fun toString(): String = "$name : $type : $value"
override fun stringify(sb: IndentingStringBuilder) {
sb.appendln("$name : $type : $value")
}
}
/**
* Derived class of [Property] that represents a binary blob. Specifically useful because printing
* a stream of bytes onto the screen isn't very use friendly
*/
class BinaryProperty(
name: String,
type: String,
val value: ByteArray) : Property(name, type) {
override fun toString(): String = "$name : $type : <<<BINARY BLOB>>>"
override fun stringify(sb: IndentingStringBuilder) {
sb.appendln("$name : $type : <<<BINARY BLOB>>>")
}
}
/**
* Derived class of [Property] that represent a list property. List could be either PoD types or
* composite types.
*/
class ListProperty(
name: String,
type: String,
private val values: MutableList<Any> = mutableListOf()) : Property(name, type) {
override fun stringify(sb: IndentingStringBuilder) {
sb.apply {
when {
values.isEmpty() -> appendln("$name : $type : [ << EMPTY LIST >> ]")
values.first() is Stringify -> {
appendln("$name : $type : [")
values.forEach {
(it as Stringify).stringify(this)
}
appendln("]")
}
else -> {
appendln("$name : $type : [")
values.forEach {
appendln(it.toString())
}
appendln("]")
}
}
}
}
}
class MapProperty(
name: String,
type: String,
private val map: MutableMap<*, *>
) : Property(name, type) {
override fun stringify(sb: IndentingStringBuilder) {
if (map.isEmpty()) {
sb.appendln("$name : $type : { << EMPTY MAP >> }")
return
}
// TODO this will not produce pretty output
sb.apply {
appendln("$name : $type : {")
map.forEach {
try {
(it.key as Stringify).stringify(this)
} catch (e: ClassCastException) {
append (it.key.toString() + " : ")
}
try {
(it.value as Stringify).stringify(this)
} catch (e: ClassCastException) {
appendln("\"${it.value.toString()}\"")
}
}
appendln("}")
}
}
}
/**
* Derived class of [Property] that represents class properties that are themselves instances of
* some complex type.
*/
class InstanceProperty(
name: String,
type: String,
val value: Instance) : Property(name, type) {
override fun stringify(sb: IndentingStringBuilder) {
sb.append("$name : ")
value.stringify(sb)
}
}
/**
* Represents an instance of a composite type.
*/
class Instance(
val name: String,
val type: String,
val fields: MutableList<Property> = mutableListOf()) : Stringify {
override fun stringify(sb: IndentingStringBuilder) {
sb.apply {
appendln("${name.simplifyClass()} : {")
fields.forEach {
it.stringify(this)
}
appendln("}")
}
}
}
/**
*
*/
fun inspectComposite(
config: Config,
typeMap: Map<Symbol?, TypeNotation>,
obj: DescribedType): Instance {
if (obj.described !is List<*>) throw MalformedBlob("")
val name = (typeMap[obj.descriptor] as CompositeType).name
"composite: $name".debug(config)
val inst = Instance(
typeMap[obj.descriptor]?.name ?: "",
typeMap[obj.descriptor]?.label ?: "")
(typeMap[obj.descriptor] as CompositeType).fields.zip(obj.described as List<*>).forEach {
" field: ${it.first.name}".debug(config)
inst.fields.add(
if (it.second is DescribedType) {
" - is described".debug(config)
val d = inspectDescribed(config, typeMap, it.second as DescribedType)
when (d) {
is Instance ->
InstanceProperty(
it.first.name,
it.first.type,
d)
is List<*> -> {
" - List".debug(config)
ListProperty(
it.first.name,
it.first.type,
d as MutableList<Any>)
}
is Map<*, *> -> {
MapProperty(
it.first.name,
it.first.type,
d as MutableMap<*, *>)
}
else -> {
" skip it".debug(config)
return@forEach
}
}
} else {
" - is prim".debug(config)
when (it.first.type) {
// Note, as in the case of SHA256 we can treat particular binary types
// as different properties with a little coercion
"binary" -> {
if (name == "net.corda.core.crypto.SecureHash\$SHA256") {
PrimProperty(
it.first.name,
it.first.type,
SecureHash.SHA256((it.second as Binary).array).toString())
} else {
BinaryProperty(it.first.name, it.first.type, (it.second as Binary).array)
}
}
else -> PrimProperty(it.first.name, it.first.type, it.second.toString())
}
})
}
return inst
}
fun inspectRestricted(
config: Config,
typeMap: Map<Symbol?, TypeNotation>,
obj: DescribedType): Any {
return when ((typeMap[obj.descriptor] as RestrictedType).source) {
"list" -> inspectRestrictedList(config, typeMap, obj)
"map" -> inspectRestrictedMap(config, typeMap, obj)
else -> throw NotImplementedError()
}
}
fun inspectRestrictedList(
config: Config,
typeMap: Map<Symbol?, TypeNotation>,
obj: DescribedType
) : List<Any> {
if (obj.described !is List<*>) throw MalformedBlob("")
return mutableListOf<Any>().apply {
(obj.described as List<*>).forEach {
when (it) {
is DescribedType -> add(inspectDescribed(config, typeMap, it))
is RestrictedType -> add(inspectRestricted(config, typeMap, it))
else -> add (it.toString())
}
}
}
}
fun inspectRestrictedMap(
config: Config,
typeMap: Map<Symbol?, TypeNotation>,
obj: DescribedType
) : Map<Any, Any> {
if (obj.described !is Map<*,*>) throw MalformedBlob("")
return mutableMapOf<Any, Any>().apply {
(obj.described as Map<*, *>).forEach {
val key = when (it.key) {
is DescribedType -> inspectDescribed(config, typeMap, it.key as DescribedType)
is RestrictedType -> inspectRestricted(config, typeMap, it.key as RestrictedType)
else -> it.key.toString()
}
val value = when (it.value) {
is DescribedType -> inspectDescribed(config, typeMap, it.value as DescribedType)
is RestrictedType -> inspectRestricted(config, typeMap, it.value as RestrictedType)
else -> it.value.toString()
}
this[key] = value
}
}
}
/**
* Every element of the blob stream will be a ProtonJ [DescribedType]. When inspecting the blob stream
* the two custom Corda types we're interested in are [CompositeType]'s, representing the instance of
* some object (class), and [RestrictedType]'s, representing containers and enumerations.
*
* @param config The configuration object that controls the behaviour of the BlobInspector
* @param typeMap
* @param obj
*/
fun inspectDescribed(
config: Config,
typeMap: Map<Symbol?, TypeNotation>,
obj: DescribedType): Any {
"${obj.descriptor} in typeMap? = ${obj.descriptor in typeMap}".debug(config)
return when (typeMap[obj.descriptor]) {
is CompositeType -> {
"* It's composite".debug(config)
inspectComposite(config, typeMap, obj)
}
is RestrictedType -> {
"* It's restricted".debug(config)
inspectRestricted(config, typeMap, obj)
}
else -> {
"${typeMap[obj.descriptor]?.name} is neither Composite or Restricted".debug(config)
}
}
}
internal object NullEncodingWhitelist : EncodingWhitelist {
override fun acceptEncoding(encoding: SerializationEncoding) = false
}
// TODO : Refactor to generically poerate on arbitrary blobs, not a single workflow
fun inspectBlob(config: Config, blob: ByteArray) {
val bytes = ByteSequence.of(blob)
val headerSize = SerializationFactoryImpl.magicSize
// TODO written to only understand one version, when we support multiple this will need to change
val headers = listOf(ByteSequence.of(amqpMagic.bytes))
val blobHeader = bytes.take(headerSize)
if (blobHeader !in headers) {
throw MalformedBlob("Blob is not a Corda AMQP serialised object graph")
}
val e = DeserializationInput.getEnvelope(bytes, NullEncodingWhitelist)
if (config.schema) {
println(e.schema)
}
if (config.transforms) {
println(e.transformsSchema)
}
val typeMap = e.schema.types.associateBy({ it.descriptor.name }, { it })
if (config.data) {
val inspected = inspectDescribed(config, typeMap, e.obj as DescribedType)
println("\n${IndentingStringBuilder().apply { (inspected as Instance).stringify(this) }}")
(inspected as Instance).fields.find {
it.type.startsWith("net.corda.core.serialization.SerializedBytes<")
}?.let {
"Found field of SerializedBytes".debug(config)
(it as InstanceProperty).value.fields.find { it.name == "bytes" }?.let { raw ->
inspectBlob(config, (raw as BinaryProperty).value)
}
}
}
}

View File

@ -1,40 +0,0 @@
package net.corda.blobinspector
import java.io.File
import java.net.URL
/**
*
*/
class FileBlobHandler(config_: Config) : BlobHandler(config_) {
private val path = File(URL((config_ as FileConfig).file).toURI())
override fun getBytes(): ByteArray {
return path.readBytes()
}
}
/**
*
*/
class InMemoryBlobHandler(config_: Config) : BlobHandler(config_) {
private val localBytes = (config_ as InMemoryConfig).blob?.bytes ?: kotlin.ByteArray(0)
override fun getBytes(): ByteArray = localBytes
}
/**
*
*/
abstract class BlobHandler(val config: Config) {
companion object {
fun make(config: Config): BlobHandler {
return when (config.mode) {
Mode.file -> FileBlobHandler(config)
Mode.inMem -> InMemoryBlobHandler(config)
}
}
}
abstract fun getBytes(): ByteArray
}

View File

@ -1,137 +0,0 @@
package net.corda.blobinspector
import org.apache.commons.cli.CommandLine
import net.corda.core.serialization.SerializedBytes
import org.apache.commons.cli.Option
import org.apache.commons.cli.Options
/**
* Enumeration of the modes in which the blob inspector can be run.
*
* @property make lambda function that takes no parameters and returns a specific instance of the configuration
* object for that mode.
*
* @property options A lambda function that takes no parameters and returns an [Options] instance that define
* the command line flags related to this mode. For example ``file`` mode would have an option to pass in
* the name of the file to read.
*
*/
enum class Mode(
val make : () -> Config,
val options : (Options) -> Unit
) {
file(
{
FileConfig(Mode.file)
},
{ o ->
o.apply{
addOption(
Option ("f", "file", true, "path to file").apply {
isRequired = true
}
)
}
}
),
inMem(
{
InMemoryConfig(Mode.inMem)
},
{
// The in memory only mode has no specific option assocaited with it as it's intended for
// testing purposes only within the unit test framework and not use on the command line
}
)
}
/**
* Configuration data class for the Blob Inspector.
*
* @property mode
*/
abstract class Config (val mode: Mode) {
var schema: Boolean = false
var transforms: Boolean = false
var data: Boolean = false
var verbose: Boolean = false
abstract fun populateSpecific(cmdLine: CommandLine)
abstract fun withVerbose() : Config
fun populate(cmdLine: CommandLine) {
schema = cmdLine.hasOption('s')
transforms = cmdLine.hasOption('t')
data = cmdLine.hasOption('d')
verbose = cmdLine.hasOption('v')
populateSpecific(cmdLine)
}
fun options() = Options().apply {
// install generic options
addOption(Option("s", "schema", false, "print the blob's schema").apply {
isRequired = false
})
addOption(Option("t", "transforms", false, "print the blob's transforms schema").apply {
isRequired = false
})
addOption(Option("d", "data", false, "Display the serialised data").apply {
isRequired = false
})
addOption(Option("v", "verbose", false, "Enable debug output").apply {
isRequired = false
})
// install the mode specific options
mode.options(this)
}
}
/**
* Configuration object when running in "File" mode, i.e. the object has been specified at
* the command line
*/
class FileConfig (
mode: Mode
) : Config(mode) {
var file: String = "unset"
override fun populateSpecific(cmdLine : CommandLine) {
file = cmdLine.getParsedOptionValue("f") as String
}
override fun withVerbose() : FileConfig {
return FileConfig(mode).apply {
this.schema = schema
this.transforms = transforms
this.data = data
this.verbose = true
}
}
}
/**
* Placeholder config objet used when running unit tests and the inspected blob is being fed in
* via some mechanism directly. Normally this will be the direct serialisation of an object in a unit
* test and then dumping that blob into the inspector for visual comparison of the output
*/
class InMemoryConfig (
mode: Mode
) : Config(mode) {
var blob: SerializedBytes<*>? = null
override fun populateSpecific(cmdLine: CommandLine) {
throw UnsupportedOperationException("In memory config is for testing only and cannot set specific flags")
}
override fun withVerbose(): Config {
throw UnsupportedOperationException("In memory config is for testing headlessly, cannot be verbose")
}
}

View File

@ -1,3 +0,0 @@
package net.corda.blobinspector
class MalformedBlob(msg: String) : Exception(msg)

View File

@ -1,44 +0,0 @@
package net.corda.blobinspector
/**
* Wrapper around a [StringBuilder] that automates the indenting of lines as they're appended to facilitate
* pretty printing of deserialized blobs.
*
* @property sb The wrapped [StringBuilder]
* @property indenting Boolean flag that indicates weather we need to pad the start of whatever text
* currently being added to the string.
* @property indent How deeply the next line should be offset from the first column
*/
class IndentingStringBuilder(s: String = "", private val offset: Int = 4) {
private val sb = StringBuilder(s)
private var indenting = true
private var indent = 0
private fun wrap(ln: String, appender: (String) -> Unit) {
if ((ln.endsWith("}") || ln.endsWith("]")) && indent > 0 && ln.length == 1) {
indent -= offset
}
appender(ln)
if (ln.endsWith("{") || ln.endsWith("[")) {
indent += offset
}
}
fun appendln(ln: String) {
wrap(ln) { s -> sb.appendln("${"".padStart(if (indenting) indent else 0, ' ')}$s") }
indenting = true
}
fun append(ln: String) {
indenting = false
wrap(ln) { s -> sb.append("${"".padStart(indent, ' ')}$s") }
}
override fun toString(): String {
return sb.toString()
}
}

View File

@ -1,81 +0,0 @@
package net.corda.blobinspector
import org.apache.commons.cli.*
import java.lang.IllegalArgumentException
/**
* Mode isn't a required property as we default it to [Mode.file]
*/
private fun modeOption() = Option("m", "mode", true, "mode, file is the default").apply {
isRequired = false
}
/**
*
* Parse the command line arguments looking for the main mode into which the application is
* being put. Note, this defaults to [Mode.file] if not set meaning we will look for a file path
* being passed as a parameter and parse that file.
*
* @param args reflects the command line arguments
*
* @return An instantiated but unpopulated [Config] object instance suitable for the mode into
* which we've been placed. This Config object should be populated via [loadModeSpecificOptions]
*/
fun getMode(args: Array<String>): Config {
// For now we only care what mode we're being put in, we can build the rest of the args and parse them
// later
val options = Options().apply {
addOption(modeOption())
}
val cmd = try {
DefaultParser().parse(options, args, true)
} catch (e: org.apache.commons.cli.ParseException) {
println(e)
HelpFormatter().printHelp("blobinspector", options)
throw IllegalArgumentException("OH NO!!!")
}
return try {
Mode.valueOf(cmd.getParsedOptionValue("m") as? String ?: "file")
} catch (e: IllegalArgumentException) {
Mode.file
}.make()
}
/**
*
* @param config an instance of a [Config] specialisation suitable for the mode into which
* the application has been put.
* @param args The command line arguments
*/
fun loadModeSpecificOptions(config: Config, args: Array<String>) {
config.apply {
// load that modes specific command line switches, needs to include the mode option
val modeSpecificOptions = config.options().apply {
addOption(modeOption())
}
populate(try {
DefaultParser().parse(modeSpecificOptions, args, false)
} catch (e: org.apache.commons.cli.ParseException) {
println("Error: ${e.message}")
HelpFormatter().printHelp("blobinspector", modeSpecificOptions)
System.exit(1)
return
})
}
}
/**
* Executable entry point
*/
fun main(args: Array<String>) {
println("<<< WARNING: this tool is experimental and under active development >>>")
getMode(args).let { mode ->
loadModeSpecificOptions(mode, args)
BlobHandler.make(mode)
}.apply {
inspectBlob(config, getBytes())
}
}

View File

@ -1,87 +0,0 @@
package net.corda.blobinspector
import java.net.URI
import org.junit.Test
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
class FileParseTests {
@Suppress("UNUSED")
var localPath: URI = projectRootDir.toUri().resolve(
"tools/blobinspector/src/test/resources/net/corda/blobinspector")
fun setupArgsWithFile(path: String) = Array(5) {
when (it) {
0 -> "-m"
1 -> "file"
2 -> "-f"
3 -> path
4 -> "-d"
else -> "error"
}
}
private val filesToTest = listOf(
"FileParseTests.1Int",
"FileParseTests.2Int",
"FileParseTests.3Int",
"FileParseTests.1String",
"FileParseTests.1Composite",
"FileParseTests.2Composite",
"FileParseTests.IntList",
"FileParseTests.StringList",
"FileParseTests.MapIntString",
"FileParseTests.MapIntClass"
)
fun testFile(file: String) {
val path = FileParseTests::class.java.getResource(file)
val args = setupArgsWithFile(path.toString())
val handler = getMode(args).let { mode ->
loadModeSpecificOptions(mode, args)
BlobHandler.make(mode)
}
inspectBlob(handler.config, handler.getBytes())
}
@Test
fun simpleFiles() {
filesToTest.forEach { testFile(it) }
}
@Test
fun specificTest() {
testFile(filesToTest[4])
testFile(filesToTest[5])
testFile(filesToTest[6])
}
@Test
fun networkParams() {
val file = "networkParams"
val path = FileParseTests::class.java.getResource(file)
val verbose = false
val args = verbose.let {
if (it)
Array(4) {
when (it) { 0 -> "-f"; 1 -> path.toString(); 2 -> "-d"; 3 -> "-vs"; else -> "error"
}
}
else
Array(3) {
when (it) { 0 -> "-f"; 1 -> path.toString(); 2 -> "-d"; else -> "error"
}
}
}
val handler = getMode(args).let { mode ->
loadModeSpecificOptions(mode, args)
BlobHandler.make(mode)
}
inspectBlob(handler.config, handler.getBytes())
}
}

View File

@ -1,91 +0,0 @@
package net.corda.blobinspector
import net.corda.core.serialization.SerializedBytes
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.SerializationOutput
import net.corda.serialization.internal.amqp.SerializerFactory
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
import org.junit.Test
class InMemoryTests {
private val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())
private fun inspect (b: SerializedBytes<*>) {
BlobHandler.make(
InMemoryConfig(Mode.inMem).apply { blob = b; data = true}
).apply {
inspectBlob(config, getBytes())
}
}
@Test
fun test1() {
data class C (val a: Int, val b: Long, val c: String)
inspect (SerializationOutput(factory).serialize(C(100, 567L, "this is a test"), AMQP_P2P_CONTEXT))
}
@Test
fun test2() {
data class C (val i: Int, val c: C?)
inspect (SerializationOutput(factory).serialize(C(1, C(2, C(3, C(4, null)))), AMQP_P2P_CONTEXT))
}
@Test
fun test3() {
data class C (val a: IntArray, val b: Array<String>)
val a = IntArray(10) { i -> i }
val c = C(a, arrayOf("aaa", "bbb", "ccc"))
inspect (SerializationOutput(factory).serialize(c, AMQP_P2P_CONTEXT))
}
@Test
fun test4() {
data class Elem(val e1: Long, val e2: String)
data class Wrapper (val name: String, val elementes: List<Elem>)
inspect (SerializationOutput(factory).serialize(
Wrapper("Outer Class",
listOf(
Elem(1L, "First element"),
Elem(2L, "Second element"),
Elem(3L, "Third element")
)), AMQP_P2P_CONTEXT))
}
@Test
fun test4b() {
data class Elem(val e1: Long, val e2: String)
data class Wrapper (val name: String, val elementes: List<List<Elem>>)
inspect (SerializationOutput(factory).serialize(
Wrapper("Outer Class",
listOf (
listOf(
Elem(1L, "First element"),
Elem(2L, "Second element"),
Elem(3L, "Third element")
),
listOf(
Elem(4L, "Fourth element"),
Elem(5L, "Fifth element"),
Elem(6L, "Sixth element")
)
)), AMQP_P2P_CONTEXT))
}
@Test
fun test5() {
data class C (val a: Map<String, String>)
inspect (SerializationOutput(factory).serialize(
C(mapOf(
"a" to "a a a",
"b" to "b b b",
"c" to "c c c")),
AMQP_P2P_CONTEXT
))
}
}

View File

@ -1,83 +0,0 @@
package net.corda.blobinspector
import org.junit.Test
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import kotlin.test.assertFalse
class ModeParse {
@Test
fun fileIsSetToFile() {
val opts1 = Array(2) {
when (it) {
0 -> "-m"
1 -> "file"
else -> "error"
}
}
assertEquals(Mode.file, getMode(opts1).mode)
}
@Test
fun nothingIsSetToFile() {
val opts1 = Array(0) { "" }
assertEquals(Mode.file, getMode(opts1).mode)
}
@Test
fun filePathIsSet() {
val opts1 = Array(4) {
when (it) {
0 -> "-m"
1 -> "file"
2 -> "-f"
3 -> "path/to/file"
else -> "error"
}
}
val config = getMode(opts1)
assertTrue(config is FileConfig)
assertEquals(Mode.file, config.mode)
assertEquals("unset", (config as FileConfig).file)
loadModeSpecificOptions(config, opts1)
assertEquals("path/to/file", config.file)
}
@Test
fun schemaIsSet() {
Array(2) {
when (it) { 0 -> "-f"; 1 -> "path/to/file"; else -> "error"
}
}.let { options ->
getMode(options).apply {
loadModeSpecificOptions(this, options)
assertFalse(schema)
}
}
Array(3) {
when (it) { 0 -> "--schema"; 1 -> "-f"; 2 -> "path/to/file"; else -> "error"
}
}.let {
getMode(it).apply {
loadModeSpecificOptions(this, it)
assertTrue(schema)
}
}
Array(3) {
when (it) { 0 -> "-f"; 1 -> "path/to/file"; 2 -> "-s"; else -> "error"
}
}.let {
getMode(it).apply {
loadModeSpecificOptions(this, it)
assertTrue(schema)
}
}
}
}

View File

@ -1,28 +0,0 @@
package net.corda.blobinspector
import org.junit.Test
class SimplifyClassTests {
@Test
fun test1() {
data class A(val a: Int)
println(A::class.java.name)
println(A::class.java.name.simplifyClass())
}
@Test
fun test2() {
val p = this.javaClass.`package`.name
println("$p.Class1<$p.Class2>")
println("$p.Class1<$p.Class2>".simplifyClass())
println("$p.Class1<$p.Class2, $p.Class3>")
println("$p.Class1<$p.Class2, $p.Class3>".simplifyClass())
println("$p.Class1<$p.Class2<$p.Class3>>")
println("$p.Class1<$p.Class2<$p.Class3>>".simplifyClass())
println("$p.Class1<$p.Class2<$p.Class3>>")
println("$p.Class1\$C<$p.Class2<$p.Class3>>".simplifyClass())
}
}

View File

@ -98,7 +98,7 @@ dependencies {
compile "org.fusesource.jansi:jansi:$jansi_version"
// Manifests: for reading stuff from the manifest file
compile "com.jcabi:jcabi-manifests:1.1"
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
compile("com.intellij:forms_rt:7.0.3") {
exclude group: "asm"

View File

@ -8,13 +8,12 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver
import com.esotericsoftware.kryo.util.Util
import net.corda.core.internal.writer
import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationContext
import net.corda.core.utilities.contextLogger
import net.corda.serialization.internal.AttachmentsClassLoader
import net.corda.serialization.internal.MutableClassWhitelist
import net.corda.serialization.internal.TransientClassWhiteList
import net.corda.serialization.internal.amqp.hasAnnotationInHierarchy
import net.corda.serialization.internal.amqp.hasCordaSerializable
import java.io.PrintWriter
import java.lang.reflect.Modifier
import java.lang.reflect.Modifier.isAbstract
@ -127,7 +126,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) || whitelist.hasAnnotationInHierarchy(type))
&& hasCordaSerializable(type)
}
// Need to clear out class names from attachments.

View File

@ -525,14 +525,17 @@ fun ClassWhitelist.requireWhitelisted(type: Type) {
}
}
fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = (hasListed(clazz) || hasAnnotationInHierarchy(clazz))
fun ClassWhitelist.isNotWhitelisted(clazz: Class<*>) = !(this.isWhitelisted(clazz))
fun ClassWhitelist.isWhitelisted(clazz: Class<*>) = hasListed(clazz) || hasCordaSerializable(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 {
/**
* Check the given [Class] has the [CordaSerializable] annotation, either directly or inherited from any of its super
* classes or interfaces.
*/
fun hasCordaSerializable(type: Class<*>): Boolean {
return type.isAnnotationPresent(CordaSerializable::class.java)
|| type.interfaces.any { hasAnnotationInHierarchy(it) }
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
|| type.interfaces.any(::hasCordaSerializable)
|| (type.superclass != null && hasCordaSerializable(type.superclass))
}
/**
@ -555,27 +558,28 @@ fun ClassWhitelist.hasAnnotationInHierarchy(type: Class<*>): Boolean {
*
* As such, if objectInstance fails access, revert to Java reflection and try that
*/
fun Class<*>.objectInstance() =
try {
this.kotlin.objectInstance
} catch (e: IllegalAccessException) {
// Check it really is an object (i.e. it has no constructor)
if (constructors.isNotEmpty()) null
else {
try {
this.getDeclaredField("INSTANCE")?.let { field ->
// and must be marked as both static and final (>0 means they're set)
if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null
else {
val accessibility = field.isAccessible
field.isAccessible = true
val obj = field.get(null)
field.isAccessible = accessibility
obj
}
fun Class<*>.objectInstance(): Any? {
return try {
this.kotlin.objectInstance
} catch (e: IllegalAccessException) {
// Check it really is an object (i.e. it has no constructor)
if (constructors.isNotEmpty()) null
else {
try {
this.getDeclaredField("INSTANCE")?.let { field ->
// and must be marked as both static and final (>0 means they're set)
if (modifiers and Modifier.STATIC == 0 || modifiers and Modifier.FINAL == 0) null
else {
val accessibility = field.isAccessible
field.isAccessible = true
val obj = field.get(null)
field.isAccessible = accessibility
obj
}
} catch (e: NoSuchFieldException) {
null
}
} catch (e: NoSuchFieldException) {
null
}
}
}
}

View File

@ -36,6 +36,7 @@ include 'tools:demobench'
include 'tools:loadtest'
include 'tools:graphs'
include 'tools:bootstrapper'
include 'tools:blobinspector'
include 'tools:shell'
include 'example-code'
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")

View File

@ -0,0 +1,27 @@
apply plugin: 'java'
apply plugin: 'kotlin'
dependencies {
compile project(':client:jackson')
compile 'info.picocli:picocli:3.0.0'
compile "org.slf4j:slf4j-nop:$slf4j_version"
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
testCompile project(':test-utils')
testCompile "junit:junit:$junit_version"
}
jar {
from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) {
exclude "META-INF/*.SF"
exclude "META-INF/*.DSA"
exclude "META-INF/*.RSA"
}
baseName 'blobinspector'
manifest {
attributes(
'Automatic-Module-Name': 'net.corda.blobinspector',
'Main-Class': 'net.corda.blobinspector.MainKt'
)
}
}

View File

@ -0,0 +1,125 @@
package net.corda.blobinspector
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.jcabi.manifests.Manifests
import net.corda.client.jackson.JacksonSupport
import net.corda.core.internal.rootMessage
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
import net.corda.core.serialization.internal._contextSerializationEnv
import net.corda.core.utilities.sequence
import net.corda.serialization.internal.AMQP_P2P_CONTEXT
import net.corda.serialization.internal.CordaSerializationMagic
import net.corda.serialization.internal.SerializationFactoryImpl
import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.amqpMagic
import picocli.CommandLine
import picocli.CommandLine.*
import java.net.MalformedURLException
import java.net.URL
import java.nio.file.Paths
import kotlin.system.exitProcess
fun main(args: Array<String>) {
val main = Main()
try {
CommandLine.run(main, *args)
} catch (e: ExecutionException) {
val throwable = e.cause ?: e
if (main.verbose) {
throwable.printStackTrace()
} else {
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
}
exitProcess(1)
}
}
@Command(
name = "Blob Inspector",
versionProvider = VersionProvider::class,
mixinStandardHelpOptions = true, // add --help and --version options,
showDefaultValues = true,
description = ["Inspect AMQP serialised binary blobs"]
)
class Main : Runnable {
@Parameters(index = "0", paramLabel = "SOURCE", description = ["URL or file path to the blob"], converter = [SourceConverter::class])
private var source: URL? = null
@Option(names = ["--format"], paramLabel = "type", description = ["Output format. Possible values: [YAML, JSON]"])
private var formatType: FormatType = FormatType.YAML
@Option(names = ["--full-parties"],
description = ["Display the owningKey and certPath properties of Party and PartyAndReference objects respectively"])
private var fullParties: Boolean = false
@Option(names = ["--schema"], description = ["Print the blob's schema first"])
private var schema: Boolean = false
@Option(names = ["--verbose"], description = ["Enable verbose output"])
var verbose: Boolean = false
override fun run() {
val bytes = source!!.readBytes().run {
require(size > amqpMagic.size) { "Insufficient bytes for AMQP blob" }
sequence()
}
require(bytes.take(amqpMagic.size) == amqpMagic) { "Not an AMQP blob" }
if (schema) {
val envelope = DeserializationInput.getEnvelope(bytes)
println(envelope.schema)
println()
}
initialiseSerialization()
val factory = when (formatType) {
FormatType.YAML -> YAMLFactory()
FormatType.JSON -> JsonFactory()
}
val mapper = JacksonSupport.createNonRpcMapper(factory, fullParties)
val deserialized = bytes.deserialize<Any>()
println(deserialized.javaClass.name)
mapper.writeValue(System.out, deserialized)
}
private fun initialiseSerialization() {
_contextSerializationEnv.set(SerializationEnvironmentImpl(
SerializationFactoryImpl().apply {
registerScheme(AMQPInspectorSerializationScheme)
},
AMQP_P2P_CONTEXT
))
}
}
private object AMQPInspectorSerializationScheme : AbstractAMQPSerializationScheme(emptyList()) {
override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean {
return magic == amqpMagic && target == SerializationContext.UseCase.P2P
}
override fun rpcClientSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException()
override fun rpcServerSerializerFactory(context: SerializationContext) = throw UnsupportedOperationException()
}
private class SourceConverter : ITypeConverter<URL> {
override fun convert(value: String): URL {
return try {
URL(value)
} catch (e: MalformedURLException) {
Paths.get(value).toUri().toURL()
}
}
}
private class VersionProvider : IVersionProvider {
override fun getVersion(): Array<String> = arrayOf(Manifests.read("Corda-Release-Version"))
}
private enum class FormatType { YAML, JSON }