mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
parent
15b262f25f
commit
4e0378de9c
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -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" />
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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? {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
63
docs/source/blob-inspector.rst
Normal file
63
docs/source/blob-inspector.rst
Normal 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.
|
@ -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
|
||||
|
@ -4,6 +4,7 @@ Tools
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
blob-inspector
|
||||
network-simulator
|
||||
demobench
|
||||
node-explorer
|
||||
|
@ -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'
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package net.corda.blobinspector
|
||||
|
||||
class MalformedBlob(msg: String) : Exception(msg)
|
@ -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()
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
27
tools/blobinspector/build.gradle
Normal file
27
tools/blobinspector/build.gradle
Normal 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'
|
||||
)
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
Loading…
Reference in New Issue
Block a user