Skip to content

Commit

Permalink
resolves #26 Endpoint and serializer for corda node attachment using …
Browse files Browse the repository at this point in the history
…multipart form data content
  • Loading branch information
parthbond180 committed Nov 17, 2021
1 parent 250e66f commit 3a1132f
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ data class CordaFlowInstruction<FlowClass: FlowLogic<Any>>(
data class CordaNodeAttachment(
val inputStream: InputStream,
val uploader: String,
val filename: String
val filename: String,
val dataType: String
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class ClientNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVa
return FileInputStream(zipName).use { fileInputStream ->
val hash = rpc.uploadAttachmentWithMetadata(
jar = fileInputStream,
uploader = attachment.uploader,
uploader = attachment.dataType,
filename = attachment.filename
)
Files.deleteIfExists(Paths.get(zipName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class CordaNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVau
return FileInputStream(zipName).use { fileInputStream ->
val attachmentHash = appServiceHub.attachments.importAttachment(
jar = fileInputStream,
uploader = attachment.uploader,
uploader = attachment.dataType,
filename = attachment.filename
)
Files.deleteIfExists(Paths.get(zipName))
Expand Down
2 changes: 2 additions & 0 deletions rest-endpoint/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ dependencies {
implementation "org.koin:koin-core:$koin_version"
implementation "io.reactivex.rxjava3:rxjava:$rxjava3_version"

implementation 'org.apache.httpcomponents:httpmime:4.5.13'

compileOnly "net.corda:corda-core:$corda_core_release_version"
compileOnly "net.corda:corda-serialization:$corda_core_release_version"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package tech.b180.cordaptor.rest

import io.undertow.server.handlers.form.FormData
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature
Expand All @@ -13,21 +14,14 @@ import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.toBase58
import net.corda.core.utilities.toSHA256Bytes
import net.corda.serialization.internal.model.LocalConstructorParameterInformation
import net.corda.serialization.internal.model.LocalTypeInformation
import net.corda.serialization.internal.model.PropertyName
import tech.b180.cordaptor.corda.CordaFlowInstruction
import tech.b180.cordaptor.corda.CordaNodeAttachment
import tech.b180.cordaptor.corda.CordaNodeState
import tech.b180.cordaptor.shaded.javax.json.JsonNumber
import tech.b180.cordaptor.shaded.javax.json.JsonObject
import tech.b180.cordaptor.shaded.javax.json.JsonString
import tech.b180.cordaptor.shaded.javax.json.JsonValue
import tech.b180.cordaptor.shaded.javax.json.*
import tech.b180.cordaptor.shaded.javax.json.stream.JsonGenerator
import java.io.InputStream
import java.math.BigDecimal
import java.math.RoundingMode
import java.security.PublicKey
Expand Down Expand Up @@ -102,20 +96,41 @@ class BigDecimalSerializer
}
}

class CordaNodeAttachmentSerializer : CustomSerializer<CordaNodeAttachment>,
SerializationFactory.PrimitiveTypeSerializer<CordaNodeAttachment>("string") {
class CordaNodeAttachmentSerializer : MultiPartFormDataSerializer<CordaNodeAttachment> {
override fun fromJson(value: JsonValue): CordaNodeAttachment {
println(value);
return when (value.valueType) {
// provide limited number of type conversions
JsonValue.ValueType.STRING -> value as CordaNodeAttachment;
else -> throw AssertionError("Expected number, got ${value.valueType} with value $value")
}
throw UnsupportedOperationException("Don't know not to restore an untyped object from JSON")
}

override fun toJson(obj: CordaNodeAttachment, generator: JsonGenerator) {
// generator.write(obj)
throw UnsupportedOperationException("Don't know not to restore an untyped object from JSON")
}

override fun fromMultiPartFormData(data: FormData): CordaNodeAttachment {
val file = data.getFirst("data")
if (file.isFileItem && file.fileItem != null) {
return CordaNodeAttachment(
dataType = data.getFirst("dataType").value,
uploader = data.getFirst("uploader").value,
filename = file.fileItem.file.fileName.toString(),
inputStream = file.fileItem.inputStream)
}
else{
throw SerializationException("Exception during multipart form data deserialization")
}
}

override val valueType: SerializerKey
get() = SerializerKey.forType(CordaNodeAttachment::class.java)

override fun generateSchema(generator: JsonSchemaGenerator): JsonObject =
Json.createObjectBuilder().
add("type", "object").
addObject("properties"){
add("uploader", OpenAPI.PrimitiveTypes.NON_EMPTY_STRING).
add("dataType", OpenAPI.PrimitiveTypes.NON_EMPTY_STRING).
add("data", OpenAPI.PrimitiveTypes.BINARY_STRING)
}.build()

}

/**
Expand Down Expand Up @@ -436,7 +451,7 @@ class CordaLinearPointerSerializer(
)

override fun initializeInstance(values: Map<String, Any?>): LinearPointer<*> {
val pointer = (values["pointer"] as? UniqueIdentifier) ?: throw AssertionError("Missing value in mandatory field 'pointer'");
val pointer = (values["pointer"] as? UniqueIdentifier) ?: throw AssertionError("Missing value in mandatory field 'pointer'")
return LinearPointer(pointer = pointer , type = LinearState::class.java)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.internal.operators.single.SingleJust
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.form.FormParserFactory
import io.undertow.util.*
import tech.b180.cordaptor.kernel.CordaptorComponent
import tech.b180.cordaptor.kernel.ModuleAPI
Expand All @@ -14,6 +15,7 @@ import java.beans.Transient
import java.io.OutputStreamWriter
import java.io.StringReader


@ModuleAPI(since = "0.1")
enum class OperationErrorType(val protocolStatusCode: Int) {
GENERIC_ERROR(StatusCodes.INTERNAL_SERVER_ERROR),
Expand Down Expand Up @@ -606,9 +608,19 @@ class OperationEndpointHandler<RequestType: Any, ResponseType: Any>(
}

val endpointRequest = try {
val requestPayload = when(exchange.requestHeaders.getFirst(Headers.CONTENT_TYPE)){
"application/json" -> {
val requestJsonPayload = Json.createReader(StringReader(payloadString)).readObject()
requestSerializer.fromJson(requestJsonPayload)
}
"multipart/form-data" -> {
val parser = FormParserFactory.builder().build().createParser(exchange)
val data = parser.parseBlocking()
(requestSerializer as MultiPartFormDataSerializer).fromMultiPartFormData(data)
}
else -> throw UnsupportedOperationException("Content-Type is Unsupported")

val requestJsonPayload = Json.createReader(StringReader(payloadString)).readObject()
val requestPayload = requestSerializer.fromJson(requestJsonPayload)
}

HttpRequestWithPayload(exchange, subject, requestPayload)

Expand All @@ -624,6 +636,12 @@ class OperationEndpointHandler<RequestType: Any, ResponseType: Any>(
sendError(exchange, EndpointErrorMessage("Unable to deserialize the request payload",
cause = e, errorType = OperationErrorType.BAD_REQUEST))
return
} catch (e: java.lang.UnsupportedOperationException) {

logger.debug("Exception during payload deserialization, which will be returned to the client", e)
sendError(exchange, EndpointErrorMessage("Request Content-Type is Unsupported",
cause = e, errorType = OperationErrorType.BAD_REQUEST))
return
}

// invoke operation in the worker thread
Expand Down
20 changes: 14 additions & 6 deletions rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ data class OpenAPI(
val openapi = VERSION

companion object {
const val VERSION = "3.0.3"

const val JSON_CONTENT_TYPE: ContentType = "application/json"

const val COMPONENTS_SCHEMA_PREFIX = "#/components/schemas/"
const val VERSION = "3.0.3"
const val JSON_CONTENT_TYPE: ContentType = "application/json"
const val MULTI_PART_FORM_DATA_CONTENT_TYPE: ContentType = "multipart/form-data"
const val COMPONENTS_SCHEMA_PREFIX = "#/components/schemas/"
}

/** [https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#infoObject] */
Expand Down Expand Up @@ -192,8 +191,11 @@ data class OpenAPI(
val description: String? = null
) {
companion object {
fun createJsonRequest(schema: JsonObject, required: Boolean) =
fun createJsonRequest(schema: JsonObject, required: Boolean) =
RequestBody(content = sortedMapOf(JSON_CONTENT_TYPE to MediaType(schema)), required = required)

fun createMultiPartFormDataRequest(schema: JsonObject, required: Boolean) =
RequestBody(content = sortedMapOf(MULTI_PART_FORM_DATA_CONTENT_TYPE to MediaType(schema)), required = required)
}
}

Expand Down Expand Up @@ -275,5 +277,11 @@ data class OpenAPI(
.add("type", "string")
.add("format", "url")
.build()

val BINARY_STRING: JsonObject = Json.createObjectBuilder()
.add("type", "string")
.add("format", "binary")
.build()

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package tech.b180.cordaptor.rest

import io.undertow.server.handlers.form.FormData
import net.corda.core.contracts.TransactionState
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.CachingCustomSerializerRegistry
Expand Down Expand Up @@ -554,6 +555,10 @@ interface JsonSchemaGenerator {
@ModuleAPI(since = "0.1")
interface CustomSerializer<T> : JsonSerializer<T>

interface MultiPartFormDataSerializer<T>: CustomSerializer<T>{
fun fromMultiPartFormData(data: FormData) : T
}

/**
* Alternative to [CustomSerializer] when custom serializers need to be created for
* parameterized types where parameters are determined at runtime and the schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tech.b180.cordaptor.rest

import io.mockk.every
import io.mockk.mockkClass
import io.undertow.server.handlers.form.FormData
import net.corda.core.contracts.Amount
import net.corda.core.contracts.LinearPointer
import net.corda.core.contracts.LinearState
Expand All @@ -16,23 +17,24 @@ import net.corda.core.utilities.toBase58
import net.corda.core.utilities.toSHA256Bytes
import net.corda.finance.flows.AbstractCashFlow
import net.corda.testing.core.TestIdentity
import org.apache.commons.io.IOUtils
import org.junit.Rule
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.test.KoinTest
import org.koin.test.KoinTestRule
import tech.b180.cordaptor.corda.*
import tech.b180.cordaptor.kernel.lazyGetAll
import java.io.File
import java.io.FileInputStream
import java.math.BigDecimal
import java.math.RoundingMode
import java.nio.file.Paths
import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.reflect.full.allSuperclasses
import kotlin.test.*


class CordaTypesTest : KoinTest {

companion object {
Expand Down Expand Up @@ -245,30 +247,41 @@ class CordaTypesTest : KoinTest {

@Test
fun `test corda linear pointer serialization`() {
val serializer = getKoin().getSerializer(LinearPointer::class, SimpleLinearState::class);
val uuid = UniqueIdentifier();
assertEquals("""{
|"pointer": {"id": "$uuid"},
|"type":"tech.b180.cordaptor.rest.SimpleLinearState"}""".trimMargin().asJsonValue(),
serializer.toJsonString(LinearPointer(pointer = uuid, type= SimpleLinearState::class.java)).asJsonValue())

assertEquals(LinearPointer(pointer = uuid, type = SimpleLinearState::class.java),
serializer.fromJson("""{"pointer": {"id": "$uuid"}, "type":"tech.b180.cordaptor.rest.SimpleLinearState"}""".asJsonObject()))
val serializer = getKoin().getSerializer(LinearPointer::class, SimpleLinearState::class)
val uuid = UniqueIdentifier()
assertEquals("""{
|"pointer": {"id": "$uuid"},
|"type":"tech.b180.cordaptor.rest.SimpleLinearState"}""".trimMargin().asJsonValue(),
serializer.toJsonString(LinearPointer(pointer = uuid, type= SimpleLinearState::class.java)).asJsonValue())

assertEquals(LinearPointer(pointer = uuid, type = SimpleLinearState::class.java),
serializer.fromJson("""{"pointer": {"id": "$uuid"}, "type":"tech.b180.cordaptor.rest.SimpleLinearState"}""".asJsonObject()))
}

@Test
fun `test corda node attachment serialization`() {
val stream = FileInputStream(File("C:\\Users\\Demo\\Downloads\\October.csv"));
val testInputStream = CordaTypesTest::class.java.classLoader.getResourceAsStream("testData.csv")
val testPath = Paths.get(CordaTypesTest::class.java.classLoader.getResource("testData.csv").toURI())
val serializer = getKoin().getSerializer(CordaNodeAttachment::class) as MultiPartFormDataSerializer


val formData = FormData(4)
formData.add("dataType", "testDataType")
formData.add("filename", "testData.csv")
formData.add("uploader", "User")
formData.add("data", testPath, "testData.csv", null)

val expectedCordaNodeAttachment = CordaNodeAttachment(
inputStream = testInputStream,
uploader= "User",
filename= "testData.csv",
dataType = "testDataType")

val serializer = getKoin().getSerializer(CordaNodeAttachment::class);
assertEquals("""{
|"inputStream": {"lastMod": 1635792185900,"lastModDate": "2021-11-01T18:43:05.900Z","name": "demo.csv","size": 37735,"type": "application/pdf"},
|"uploader":"test",
|"filename": "demo"}""".trimMargin().asJsonValue(),
serializer.toJsonString(CordaNodeAttachment(inputStream = stream, uploader= "test", filename= "demo")).asJsonValue())
val serializerOutputCordaNodeAttachment = serializer.fromMultiPartFormData(formData)

assertEquals(CordaNodeAttachment(inputStream = stream, uploader= "test", filename= "demo"),
serializer.fromJson("""{"inputStream": {"lastMod": 1635792185900,"lastModDate": "2021-11-01T18:43:05.900Z","name": "demo.csv","size": 37735,"type": "application/pdf"}, "uploader":"test", "filename": "demo"}""".asJsonObject()))
assertEquals(true,
IOUtils.contentEquals(expectedCordaNodeAttachment.inputStream,
serializerOutputCordaNodeAttachment.inputStream))
}

@Test
Expand Down
5 changes: 5 additions & 0 deletions rest-endpoint/src/test/resources/testData.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
sec,at,country,cur,time,dur,mat,cr,demand_amount,held_value,portfolio,fund_name,pm_name,originator_name,client_name
RESIDENTIAL_MORTGAGE,B,US,USD,2,2,2,AAA,1562700.62,1562700618,P003,Global Opportunities 3,Sue Gamber,Alex Denton,Pension Co
RESIDENTIAL_MORTGAGE,B,US,USD,1,1,1,AAA,797469.18,797469178.3,P003,Global Opportunities 3,Sue Gamber,Alex Denton,Pension Co
RESIDENTIAL_MORTGAGE,B,US,USD,2,2,2,AAA,731693.05,731693046.2,P003,Global Opportunities 3,Sue Gamber,Alex Denton,Pension Co
RESIDENTIAL_MORTGAGE,B,US,USD,2,2,26,AAA,632034.14,632034140.7,P003,Global Opportunities 3,Sue Gamber,Alex Denton,Pension Co

0 comments on commit 3a1132f

Please sign in to comment.