From 7b23a207b806dd4bee36aba36894efe8a1813da8 Mon Sep 17 00:00:00 2001 From: Vivek Shah Date: Wed, 3 Nov 2021 22:01:44 +0530 Subject: [PATCH 1/7] #26: Add attachment endpoint. - Add create attachment interface for rpc and embedded mode. --- .../b180/cordaptor/corda/CordaNodeState.kt | 10 ++++ .../b180/cordaptor/rpc/ClientNodeState.kt | 28 ++++++++++ .../cordaptor/cordapp/InternalNodeState.kt | 27 ++++++++++ .../cordaptor/rest/CordaTypesSerializers.kt | 18 +++++++ .../tech/b180/cordaptor/rest/KoinModule.kt | 1 + .../b180/cordaptor/rest/NodeStateEndpoints.kt | 52 +++++++++++++++++++ .../b180/cordaptor/rest/CordaTypesTest.kt | 23 ++++++-- 7 files changed, 155 insertions(+), 4 deletions(-) diff --git a/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt b/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt index b1ed05f..a8cfed8 100644 --- a/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt +++ b/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt @@ -17,6 +17,7 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.transactions.SignedTransaction import tech.b180.cordaptor.kernel.ModuleAPI +import java.io.InputStream import java.security.PublicKey import java.time.Instant import java.util.* @@ -99,6 +100,8 @@ interface CordaNodeState : PartyLocator { fun trackStates(query: CordaVaultQuery): CordaDataFeed fun initiateFlow(instruction: CordaFlowInstruction>): CordaFlowHandle + + fun createAttachment(attachment: CordaNodeAttachment): SecureHash } /** @@ -157,6 +160,13 @@ data class CordaFlowInstruction>( ) } +@ModuleAPI(since = "0.1") +data class CordaNodeAttachment( + val inputStream: InputStream, + val uploader: String, + val filename: String +) + /** * Container for a result of executing Corda flow, which may be either * an object or an exception, alongside an [Instant] when the result was captured. diff --git a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt index c51c1b0..8190195 100644 --- a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt +++ b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt @@ -27,7 +27,14 @@ import org.slf4j.Logger import tech.b180.cordaptor.corda.* import tech.b180.cordaptor.kernel.CordaptorComponent import tech.b180.cordaptor.kernel.loggerFor +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.file.Files +import java.nio.file.Paths import java.security.PublicKey +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream /** * Implementation of [CordaNodeState] interface providing access to a state @@ -105,6 +112,27 @@ class ClientNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVa return get>().initiateFlow(instruction) } + + override fun createAttachment(attachment: CordaNodeAttachment): SecureHash { + val zipName = "$attachment.filename-${UUID.randomUUID()}.zip" + FileOutputStream(zipName).use { fileOutputStream -> + ZipOutputStream(fileOutputStream).use { zipOutputStream -> + val zipEntry = ZipEntry(attachment.filename) + zipOutputStream.putNextEntry(zipEntry) + attachment.inputStream.copyTo(zipOutputStream, 1024) + } + } + + return FileInputStream(zipName).use { fileInputStream -> + val hash = rpc.uploadAttachmentWithMetadata( + jar = fileInputStream, + uploader = attachment.uploader, + filename = attachment.filename + ) + Files.deleteIfExists(Paths.get(zipName)) + hash + } + } } /** diff --git a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt index 963afa4..aa23945 100644 --- a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt +++ b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt @@ -25,7 +25,14 @@ import org.slf4j.Logger import tech.b180.cordaptor.corda.* import tech.b180.cordaptor.kernel.CordaptorComponent import tech.b180.cordaptor.kernel.loggerFor +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.file.Files +import java.nio.file.Paths import java.security.PublicKey +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream /** * Implementation of [CordaNodeState] interface providing access to a state @@ -99,6 +106,26 @@ class CordaNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVau return get>().initiateFlow(instruction) } + + override fun createAttachment(attachment: CordaNodeAttachment): SecureHash { + val zipName = "$attachment.filename-${UUID.randomUUID()}.zip" + FileOutputStream(zipName).use { fileOutputStream -> + ZipOutputStream(fileOutputStream).use { zipOutputStream -> + val zipEntry = ZipEntry(attachment.filename) + zipOutputStream.putNextEntry(zipEntry) + attachment.inputStream.copyTo(zipOutputStream, 1024) + } + } + return FileInputStream(zipName).use { fileInputStream -> + val attachmentHash = appServiceHub.attachments.importAttachment( + jar = fileInputStream, + uploader = attachment.uploader, + filename = attachment.filename + ) + Files.deleteIfExists(Paths.get(zipName)) + attachmentHash + } + } } /** diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt index 090eb37..b1c3163 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt @@ -20,12 +20,14 @@ import net.corda.serialization.internal.model.LocalConstructorParameterInformati 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.stream.JsonGenerator +import java.io.InputStream import java.math.BigDecimal import java.math.RoundingMode import java.security.PublicKey @@ -100,6 +102,22 @@ class BigDecimalSerializer } } +class CordaNodeAttachmentSerializer : CustomSerializer, + SerializationFactory.PrimitiveTypeSerializer("string") { + 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") + } + } + + override fun toJson(obj: CordaNodeAttachment, generator: JsonGenerator) { +// generator.write(obj) + } +} + /** * Serializes a [Currency] as a JSON string representing its ISO code. * Mainly used as part of the implementation for serializer of [Amount], but diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/KoinModule.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/KoinModule.kt index cab12a5..cb79f91 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/KoinModule.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/KoinModule.kt @@ -90,6 +90,7 @@ class RestEndpointModuleProvider : ModuleProvider { single { JsonObjectSerializer() } bind CustomSerializer::class single { CordaOpaqueBytesSerializer() } bind CustomSerializer::class single { JavaDurationSerializer() } bind CustomSerializer::class + single { CordaNodeAttachmentSerializer() } bind CustomSerializer::class single { CordaFlowInstructionSerializerFactory(get()) } bind CustomSerializerFactory::class single { CordaAmountSerializerFactory(get()) } bind CustomSerializerFactory::class diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt index fd454ab..d2ecbad 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt @@ -17,6 +17,7 @@ import tech.b180.cordaptor.corda.* import tech.b180.cordaptor.kernel.CordaptorComponent import tech.b180.cordaptor.kernel.loggerFor import tech.b180.cordaptor.shaded.javax.json.JsonObject +import java.io.InputStream import java.util.* import java.util.concurrent.TimeUnit import kotlin.math.min @@ -228,6 +229,57 @@ class FlowInitiationEndpoint( ) } +/** + * API endpoint handler allowing to upload attachment on corda node. + */ +class NodeAttachmentEndpoint( + contextPath: String, + private val file: InputStream, + private val uploader: String, + private val filename: String +) : OperationEndpoint, CordaptorComponent, + ContextMappedResourceEndpoint(contextPath, true) { + + companion object { + private val logger = loggerFor() + } + + private val cordaNodeState: CordaNodeState by inject() + + override val responseType = SerializerKey(SecureHash::class) + override val requestType = SerializerKey(CordaNodeAttachment::class) + override val supportedMethods = OperationEndpoint.POST_ONLY + + override fun executeOperation( + request: RequestWithPayload + ): Single> { + val attachmentInstruction = request.payload + logger.debug("Attachment instruction {}", attachmentInstruction) + + val handle = cordaNodeState.createAttachment(attachment = attachmentInstruction) + return Single.just(Response(handle, StatusCodes.ACCEPTED, emptyList())) + } + + override fun generatePathInfoSpecification(schemaGenerator: JsonSchemaGenerator): OpenAPI.PathItem = + OpenAPI.PathItem( + post = OpenAPI.Operation( + summary = "Initiates and tracks execution of Corda attachment with given parameters", + operationId = "initiate" + ).withRequestBody( + OpenAPI.RequestBody.createJsonRequest( + schemaGenerator.generateSchema(requestType), + required = true + ) + ).withResponse( + OpenAPI.HttpStatusCode.OK, + OpenAPI.Response.createJsonResponse( + description = "Attachment uploaded successfully and its result is available", + schema = schemaGenerator.generateSchema(responseType) + ) + ).withForbiddenResponse().withTags(FLOW_INITIATION_TAG) + ) +} + /** * API endpoint handler allowing to retrieve latest snapshot of a flow by its run id. * This call will fail to instantiate if no module implementing flow results cache is present. diff --git a/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt b/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt index bed173a..34207e9 100644 --- a/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt +++ b/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt @@ -21,11 +21,10 @@ 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.CordaFlowInstruction -import tech.b180.cordaptor.corda.CordaFlowProgress -import tech.b180.cordaptor.corda.CordaFlowSnapshot -import tech.b180.cordaptor.corda.CordaNodeState +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.time.Duration @@ -44,6 +43,7 @@ class CordaTypesTest : KoinTest { // register custom serializers for the factory to discover single { BigDecimalSerializer() } bind CustomSerializer::class + single { CordaNodeAttachmentSerializer() } bind CustomSerializer::class single { CurrencySerializer() } bind CustomSerializer::class single { CordaUUIDSerializer() } bind CustomSerializer::class single { CordaSecureHashSerializer() } bind CustomSerializer::class @@ -256,6 +256,21 @@ class CordaTypesTest : KoinTest { 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 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()) + + 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())) + } + @Test fun `test corda flow instruction serialization`() { val serializer = getKoin().getSerializer(CordaFlowInstruction::class, TestFlow::class) From ad64489a62787e9951d02b7f68d0995799c50b88 Mon Sep 17 00:00:00 2001 From: Parth Shukla <52910688+parthbond180@users.noreply.github.com> Date: Tue, 9 Nov 2021 18:56:42 +0000 Subject: [PATCH 2/7] resolves #26 Endpoint and serializer for corda node attachment using multipart form data content --- .../b180/cordaptor/corda/CordaNodeState.kt | 3 +- .../b180/cordaptor/rpc/ClientNodeState.kt | 2 +- .../cordaptor/cordapp/InternalNodeState.kt | 2 +- rest-endpoint/build.gradle | 2 + .../cordaptor/rest/CordaTypesSerializers.kt | 51 +++++++++++------- .../b180/cordaptor/rest/GenericEndpoints.kt | 22 +++++++- .../tech/b180/cordaptor/rest/OpenAPI.kt | 20 ++++--- .../cordaptor/rest/SerializationFactory.kt | 5 ++ .../b180/cordaptor/rest/CordaTypesTest.kt | 53 ++++++++++++------- rest-endpoint/src/test/resources/testData.csv | 5 ++ 10 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 rest-endpoint/src/test/resources/testData.csv diff --git a/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt b/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt index a8cfed8..e02472a 100644 --- a/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt +++ b/corda-common/src/main/kotlin/tech/b180/cordaptor/corda/CordaNodeState.kt @@ -164,7 +164,8 @@ data class CordaFlowInstruction>( data class CordaNodeAttachment( val inputStream: InputStream, val uploader: String, - val filename: String + val filename: String, + val dataType: String ) /** diff --git a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt index 8190195..3b72e5c 100644 --- a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt +++ b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt @@ -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)) diff --git a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt index aa23945..8bc366d 100644 --- a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt +++ b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt @@ -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)) diff --git a/rest-endpoint/build.gradle b/rest-endpoint/build.gradle index 87934c6..30b1a2a 100644 --- a/rest-endpoint/build.gradle +++ b/rest-endpoint/build.gradle @@ -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" diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt index b1c3163..2007006 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/CordaTypesSerializers.kt @@ -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 @@ -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 @@ -102,20 +96,41 @@ class BigDecimalSerializer } } -class CordaNodeAttachmentSerializer : CustomSerializer, - SerializationFactory.PrimitiveTypeSerializer("string") { +class CordaNodeAttachmentSerializer : MultiPartFormDataSerializer { 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() + } /** @@ -436,7 +451,7 @@ class CordaLinearPointerSerializer( ) override fun initializeInstance(values: Map): 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) } } diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt index 83a013b..3753d3e 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt @@ -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 @@ -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), @@ -606,9 +608,19 @@ class OperationEndpointHandler( } 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) @@ -624,6 +636,12 @@ class OperationEndpointHandler( 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 diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt index 5c49297..2d85cdd 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt @@ -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] */ @@ -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) } } @@ -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() + } } diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/SerializationFactory.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/SerializationFactory.kt index 44a17c6..ff85711 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/SerializationFactory.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/SerializationFactory.kt @@ -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 @@ -554,6 +555,10 @@ interface JsonSchemaGenerator { @ModuleAPI(since = "0.1") interface CustomSerializer : JsonSerializer +interface MultiPartFormDataSerializer: CustomSerializer{ + 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 diff --git a/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt b/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt index 34207e9..8b3656b 100644 --- a/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt +++ b/rest-endpoint/src/test/kotlin/tech/b180/cordaptor/rest/CordaTypesTest.kt @@ -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 @@ -16,6 +17,7 @@ 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 @@ -23,16 +25,16 @@ 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 { @@ -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 diff --git a/rest-endpoint/src/test/resources/testData.csv b/rest-endpoint/src/test/resources/testData.csv new file mode 100644 index 0000000..36379ad --- /dev/null +++ b/rest-endpoint/src/test/resources/testData.csv @@ -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 From a7b59af667450be41cce59acb1ad303ceb8a42c3 Mon Sep 17 00:00:00 2001 From: Parth Shukla <52910688+parthbond180@users.noreply.github.com> Date: Tue, 9 Nov 2021 18:57:03 +0000 Subject: [PATCH 3/7] resolves #26 Endpoint and serializer for corda node attachment using multipart form data content --- .../tech/b180/cordaptor/rest/NodeStateEndpoints.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt index d2ecbad..3166d62 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt @@ -260,14 +260,14 @@ class NodeAttachmentEndpoint( return Single.just(Response(handle, StatusCodes.ACCEPTED, emptyList())) } - override fun generatePathInfoSpecification(schemaGenerator: JsonSchemaGenerator): OpenAPI.PathItem = - OpenAPI.PathItem( + override fun generatePathInfoSpecification(schemaGenerator: JsonSchemaGenerator): OpenAPI.PathItem { + return OpenAPI.PathItem( post = OpenAPI.Operation( - summary = "Initiates and tracks execution of Corda attachment with given parameters", - operationId = "initiate" + summary = "Uploads Corda attachment with given parameters", + operationId = "uploadAttachment" ).withRequestBody( - OpenAPI.RequestBody.createJsonRequest( - schemaGenerator.generateSchema(requestType), + OpenAPI.RequestBody.createMultiPartFormDataRequest( + schemaGenerator.generateSchema(requestType), //can be multiPartFormDataSchema required = true ) ).withResponse( @@ -278,6 +278,7 @@ class NodeAttachmentEndpoint( ) ).withForbiddenResponse().withTags(FLOW_INITIATION_TAG) ) + } } /** From c2752977e44d8e523613f778ea66d1e7ff889fe2 Mon Sep 17 00:00:00 2001 From: Parth Shukla <52910688+parthbond180@users.noreply.github.com> Date: Fri, 12 Nov 2021 16:17:57 +0000 Subject: [PATCH 4/7] resolves #26 Multi part form data parser and endpoint support --- .../cordaptor_test/CordaptorAPITestSuite.kt | 29 ++++- .../resources/ReferenceCordapp.api.json | 46 ++++++++ .../src/genericApiTest/resources/testData.csv | 5 + .../tech/b180/cordaptor/rest/Constants.kt | 1 + .../b180/cordaptor/rest/GenericEndpoints.kt | 108 ++++++++++++++---- .../b180/cordaptor/rest/NodeStateEndpoints.kt | 62 ++++++---- .../tech/b180/cordaptor/rest/Settings.kt | 6 +- .../src/main/resources/module-reference.conf | 4 + 8 files changed, 211 insertions(+), 50 deletions(-) create mode 100644 reference-cordapp/src/genericApiTest/resources/testData.csv diff --git a/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt b/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt index 0168129..1f49a11 100644 --- a/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt +++ b/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt @@ -3,11 +3,14 @@ package tech.b180.cordaptor_test import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import org.eclipse.jetty.client.HttpClient +import org.eclipse.jetty.client.util.MultiPartContentProvider +import org.eclipse.jetty.client.util.PathContentProvider import org.eclipse.jetty.client.util.StringContentProvider import org.eclipse.jetty.http.HttpHeader import tech.b180.ref_cordapp.DelayedProgressFlow import tech.b180.ref_cordapp.SimpleFlow import java.io.StringReader +import java.nio.file.Paths import java.time.Duration import java.time.Instant import javax.json.Json @@ -41,6 +44,7 @@ class CordaptorAPITestSuite( testStateQuery(client, stateRef) testVaultQueryViaGET(client) testVaultQueryViaPOST(client) + testNodeAttachmentViaPOST(client) } private fun testOpenAPISpecification(client: HttpClient) { @@ -211,9 +215,32 @@ class CordaptorAPITestSuite( assertEquals(1, page.getInt("totalStatesAvailable")) } + private fun testNodeAttachmentViaPOST(client: HttpClient) { + val req = client.POST( + "$baseUrl/node/uploadNodeAttachment") + + val multiPartContentProvider = MultiPartContentProvider() + + multiPartContentProvider.addFieldPart("filename", StringContentProvider("testData.csv"), null) + multiPartContentProvider.addFieldPart("dataType", StringContentProvider("testDataType"), null) + multiPartContentProvider.addFieldPart("uploader", StringContentProvider("User"), null) + multiPartContentProvider.addFilePart("data", "testData.csv", + PathContentProvider(Paths.get(CordaptorAPITestSuite::class.java.classLoader.getResource("testData.csv").toURI())), null) + + multiPartContentProvider.close() + req.content(multiPartContentProvider) + val response = req.send() + + assertEquals(HttpServletResponse.SC_OK, response.status) + assertEquals("application/json", response.mediaType) + + /*val page = response.contentAsString.asJsonObject() + assertEquals(1, page.getInt("totalStatesAvailable"))*/ + } + private fun testVaultQueryViaPOST(client: HttpClient) { val req = client.POST( - "$baseUrl/node/reference/SimpleLinearState/query") + "$baseUrl/node/reference/SimpleLinearState/query") val content = """{ |"contractStateClass":"tech.b180.ref_cordapp.SimpleLinearState", diff --git a/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json b/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json index d6dadc4..3b6115b 100644 --- a/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json +++ b/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json @@ -2063,6 +2063,52 @@ ] } }, + "/node/uploadNodeAttachment": { + "post": { + "operationId": "uploadNodeAttachment", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "uploader": { + "type": "string", + "minLength": 1 + }, + "dataType": { + "type": "string", + "minLength": 1 + }, + "data": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CordaSecureHash" + } + } + }, + "description": "Attachment uploaded successfully and its result is available" + }, + "403": { + "description": "Permission denied" + } + }, + "summary": "Uploads Corda attachment with given parameters", + "tags": ["flowInitiation"] + } + }, "/node/version": { "get": { "operationId": "getNodeVersion", diff --git a/reference-cordapp/src/genericApiTest/resources/testData.csv b/reference-cordapp/src/genericApiTest/resources/testData.csv new file mode 100644 index 0000000..cf46503 --- /dev/null +++ b/reference-cordapp/src/genericApiTest/resources/testData.csv @@ -0,0 +1,5 @@ +sec,at,country,cur,time,dur,mat,cr +RESIDENTIAL_MORTGAGE,B,US,USD,2,2,2,AAA +RESIDENTIAL_MORTGAGE,B,US,USD,1,1,1,AAA +RESIDENTIAL_MORTGAGE,B,US,USD,2,2,2,AAA +RESIDENTIAL_MORTGAGE,B,US,USD,2,2,26,AAA diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Constants.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Constants.kt index b7e18d6..b23bee5 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Constants.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Constants.kt @@ -8,6 +8,7 @@ const val OPERATION_GET_FLOW_SNAPSHOT = "getFlowSnapshot" const val OPERATION_GET_STATE_BY_REF = "getStateByRef" const val OPERATION_QUERY_STATES = "queryStates" const val OPERATION_GET_TX_BY_HASH = "getTransactionByHash" +const val OPERATION_UPLOAD_NODE_ATTACHMENT = "uploadNodeAttachment" // constants used as Koin qualifiers for security configuration factories const val SECURITY_CONFIGURATION_NONE = "none" diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt index 3753d3e..d57a19f 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt @@ -3,8 +3,11 @@ package tech.b180.cordaptor.rest import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.internal.operators.single.SingleJust +import io.undertow.server.HttpHandler import io.undertow.server.HttpServerExchange -import io.undertow.server.handlers.form.FormParserFactory +import io.undertow.server.handlers.BlockingHandler +import io.undertow.server.handlers.form.FormDataParser +import io.undertow.server.handlers.form.MultiPartParserDefinition import io.undertow.util.* import tech.b180.cordaptor.kernel.CordaptorComponent import tech.b180.cordaptor.kernel.ModuleAPI @@ -12,6 +15,7 @@ import tech.b180.cordaptor.kernel.loggerFor import tech.b180.cordaptor.shaded.javax.json.Json import tech.b180.cordaptor.shaded.javax.json.stream.JsonParsingException import java.beans.Transient +import java.io.IOException import java.io.OutputStreamWriter import java.io.StringReader @@ -606,30 +610,20 @@ class OperationEndpointHandler( "Empty request payload", errorType = OperationErrorType.BAD_REQUEST)) return } - - 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") - - } - - HttpRequestWithPayload(exchange, subject, requestPayload) - + try { + parseRequestAndExecuteOperation(subject, exchange, payloadString) } catch (e: JsonParsingException) { logger.debug("JSON parsing exception, which will be returned to the client", e) sendError(exchange, EndpointErrorMessage("Malformed JSON in the request payload", cause = e, errorType = OperationErrorType.BAD_REQUEST)) return + } catch (e: IOException) { + + logger.debug("Error parsing multipart/form-data request, which will be returned to the client", e) + sendError(exchange, EndpointErrorMessage("Malformed form data in the request payload", + cause = e, errorType = OperationErrorType.BAD_REQUEST)) + return } catch (e: SerializationException) { logger.debug("Exception during payload deserialization, which will be returned to the client", e) @@ -644,8 +638,41 @@ class OperationEndpointHandler( return } - // invoke operation in the worker thread - logger.debug("Invoking operation with request: {}", endpointRequest) + } + + private fun parseRequestAndExecuteOperation(subject: Subject, exchange: HttpServerExchange, payloadString: String){ + with(exchange.requestHeaders.getFirst(Headers.CONTENT_TYPE)){ + when{ + contains("application/json") -> { + val requestJsonPayload = Json.createReader( + StringReader( + payloadString + ) + ).readObject() + val endpointRequest = HttpRequestWithPayload(exchange, subject, requestSerializer.fromJson(requestJsonPayload)) + logger.debug("Invoking operation with request: {}", endpointRequest) + executeOperationFromEndpointRequest(exchange, endpointRequest) + } + contains("multipart/form-data") -> { + if (exchange.isInIoThread) { + //Blocking + exchange.dispatch(BlockingHandler(MultiPartFormHandler(requestSerializer, subject))) + + //Non-blocking +/* + val parser = MultiPartParserDefinition().create(exchange) + parser.parse(MultiPartFormHandlerNonBlocking(requestSerializer, subject)) +*/ + return + } + } + else -> throw UnsupportedOperationException("Content-Type is Unsupported") + } + } + } + + private fun executeOperationFromEndpointRequest(exchange: HttpServerExchange, + endpointRequest: HttpRequestWithPayload){ val endpointResponse = endpoint.executeOperation(endpointRequest) @@ -670,7 +697,7 @@ class OperationEndpointHandler( sendResponse(exchange, response) } else if (error != null) { sendError(exchange, (error as? EndpointOperationException)?.toErrorMessage() - ?: EndpointErrorMessage("Unexpected internal error", error)) + ?: EndpointErrorMessage("Unexpected internal error", error)) } } }) @@ -691,4 +718,41 @@ class OperationEndpointHandler( return "OperationEndpointHandler(requestType=${endpoint.requestType}, " + "responseType=${endpoint.responseType}, endpoint=${endpoint})" } + + inner class MultiPartFormHandler(private val requestSerializer: JsonSerializer, + private val subject: Subject): HttpHandler{ + override fun handleRequest(exchange: HttpServerExchange){ + val parser = MultiPartParserDefinition().create(exchange) + try{ + parser.parseBlocking() + val endpointRequest = HttpRequestWithPayload(exchange, subject, + (requestSerializer as MultiPartFormDataSerializer).fromMultiPartFormData( + exchange.getAttachment(FormDataParser.FORM_DATA) + ) + ) + executeOperationFromEndpointRequest(exchange, endpointRequest) + } catch (e : Throwable) { + throw IOException("Multi Part Form parsing failed") + } finally { + parser.close() + } + } + } + + inner class MultiPartFormHandlerNonBlocking(private val requestSerializer: JsonSerializer, + private val subject: Subject): HttpHandler{ + override fun handleRequest(exchange: HttpServerExchange){ + try{ + val endpointRequest = HttpRequestWithPayload(exchange, subject, + (requestSerializer as MultiPartFormDataSerializer).fromMultiPartFormData( + exchange.getAttachment(FormDataParser.FORM_DATA) + ) + ) + executeOperationFromEndpointRequest(exchange, endpointRequest) + } catch (e : Throwable) { + throw IOException("Multi Part Form parsing failed") + } + } + } + } diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt index 3166d62..ecffe57 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt @@ -43,14 +43,25 @@ class NodeStateAPIProvider(contextPath: String) : EndpointProvider, CordaptorCom init { val flowSnapshotsEnabled = settings.isFlowSnapshotsEndpointEnabled + val nodeAttachmentEndpointEnabled = settings.isNodeAttachmentEndpointEnabled if (!flowSnapshotsEnabled) { logger.info("Flow snapshots endpoint is disabled. Flow initiation operations will never return a Location header") } + if (!nodeAttachmentEndpointEnabled) { + logger.info("Node Attachment endpoint is disabled.") + } + operationEndpoints = mutableListOf() queryEndpoints = mutableListOf() for (cordapp in nodeCatalog.cordapps) { + + if(nodeAttachmentEndpointEnabled){ + val handlerPath = "$contextPath/uploadNodeAttachment" + operationEndpoints.add(NodeAttachmentEndpoint(handlerPath)) + } + for (flowInfo in cordapp.flows) { val handlerPath = "$contextPath/${cordapp.shortName}/${flowInfo.flowClass.simpleName}" @@ -233,10 +244,7 @@ class FlowInitiationEndpoint( * API endpoint handler allowing to upload attachment on corda node. */ class NodeAttachmentEndpoint( - contextPath: String, - private val file: InputStream, - private val uploader: String, - private val filename: String + contextPath: String ) : OperationEndpoint, CordaptorComponent, ContextMappedResourceEndpoint(contextPath, true) { @@ -253,31 +261,35 @@ class NodeAttachmentEndpoint( override fun executeOperation( request: RequestWithPayload ): Single> { - val attachmentInstruction = request.payload - logger.debug("Attachment instruction {}", attachmentInstruction) + if (!request.subject.isPermitted(OPERATION_UPLOAD_NODE_ATTACHMENT)) { + throw UnauthorizedOperationException(OPERATION_UPLOAD_NODE_ATTACHMENT) + } - val handle = cordaNodeState.createAttachment(attachment = attachmentInstruction) - return Single.just(Response(handle, StatusCodes.ACCEPTED, emptyList())) + val attachmentInstruction = request.payload + logger.debug("Attachment instruction {}", attachmentInstruction) + + val handle = cordaNodeState.createAttachment(attachment = attachmentInstruction) + return Single.just(Response(handle, StatusCodes.ACCEPTED, emptyList())) } override fun generatePathInfoSpecification(schemaGenerator: JsonSchemaGenerator): OpenAPI.PathItem { - return OpenAPI.PathItem( - post = OpenAPI.Operation( - summary = "Uploads Corda attachment with given parameters", - operationId = "uploadAttachment" - ).withRequestBody( - OpenAPI.RequestBody.createMultiPartFormDataRequest( - schemaGenerator.generateSchema(requestType), //can be multiPartFormDataSchema - required = true - ) - ).withResponse( - OpenAPI.HttpStatusCode.OK, - OpenAPI.Response.createJsonResponse( - description = "Attachment uploaded successfully and its result is available", - schema = schemaGenerator.generateSchema(responseType) - ) - ).withForbiddenResponse().withTags(FLOW_INITIATION_TAG) - ) + return OpenAPI.PathItem( + post = OpenAPI.Operation( + summary = "Uploads Corda attachment with given parameters", + operationId = "uploadNodeAttachment" + ).withRequestBody( + OpenAPI.RequestBody.createMultiPartFormDataRequest( + schemaGenerator.generateSchema(requestType), //can be multiPartFormDataSchema + required = true + ) + ).withResponse( + OpenAPI.HttpStatusCode.OK, + OpenAPI.Response.createJsonResponse( + description = "Attachment uploaded successfully and its result is available", + schema = schemaGenerator.generateSchema(responseType) + ) + ).withForbiddenResponse().withTags(FLOW_INITIATION_TAG) + ) } } diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Settings.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Settings.kt index 4d4c4c8..aaf8162 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Settings.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/Settings.kt @@ -25,14 +25,16 @@ data class Settings( val isSwaggerUIEnabled: Boolean, val isFlowSnapshotsEndpointEnabled: Boolean, val maxFlowInitiationTimeout: Duration, - val maxVaultQueryPageSize: Int + val maxVaultQueryPageSize: Int, + val isNodeAttachmentEndpointEnabled: Boolean ) { constructor(ourConfig: Config) : this( isOpenAPISpecificationEnabled = ourConfig.getBoolean("spec.enabled"), isSwaggerUIEnabled = ourConfig.getBoolean("swaggerUI.enabled"), isFlowSnapshotsEndpointEnabled = ourConfig.getBoolean("flowSnapshots.enabled"), maxFlowInitiationTimeout = ourConfig.getDuration("flowInitiation.maxTimeout"), - maxVaultQueryPageSize = ourConfig.getInt("vaultQueries.maxPageSize") + maxVaultQueryPageSize = ourConfig.getInt("vaultQueries.maxPageSize"), + isNodeAttachmentEndpointEnabled = ourConfig.getBoolean("nodeAttachment.enabled") ) } diff --git a/rest-endpoint/src/main/resources/module-reference.conf b/rest-endpoint/src/main/resources/module-reference.conf index b86e83e..62256f8 100644 --- a/rest-endpoint/src/main/resources/module-reference.conf +++ b/rest-endpoint/src/main/resources/module-reference.conf @@ -104,4 +104,8 @@ openAPI { # Absolute maximum timeout for the request to avoid wasting server resources maxTimeout = 10m } + + nodeAttachment { + enabled = true + } } \ No newline at end of file From d21354d85bb2fd2ae1ba1716fdeaab8651261b12 Mon Sep 17 00:00:00 2001 From: Parth Shukla <52910688+parthbond180@users.noreply.github.com> Date: Mon, 15 Nov 2021 19:43:22 +0000 Subject: [PATCH 5/7] resolves #26 Handled multi part form parsing separately --- .../b180/cordaptor/rest/GenericEndpoints.kt | 131 +++++++----------- .../b180/cordaptor/rest/NodeStateEndpoints.kt | 2 +- .../tech/b180/cordaptor/rest/OpenAPI.kt | 5 + 3 files changed, 54 insertions(+), 84 deletions(-) diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt index d57a19f..e495ad4 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/GenericEndpoints.kt @@ -3,11 +3,8 @@ package tech.b180.cordaptor.rest import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.internal.operators.single.SingleJust -import io.undertow.server.HttpHandler import io.undertow.server.HttpServerExchange -import io.undertow.server.handlers.BlockingHandler -import io.undertow.server.handlers.form.FormDataParser -import io.undertow.server.handlers.form.MultiPartParserDefinition +import io.undertow.server.handlers.form.FormParserFactory import io.undertow.util.* import tech.b180.cordaptor.kernel.CordaptorComponent import tech.b180.cordaptor.kernel.ModuleAPI @@ -597,33 +594,69 @@ class OperationEndpointHandler( if (tryHandlingAsQuery(exchange, subject)) { return } - // we have to use non-blocking IO to read request payload and not switch to a blocking mode // because API operations are performed asynchronously - val asyncReceiver = exchange.requestReceiver - asyncReceiver.receiveFullString({ e, s -> receiveRequestPayload(subject, e, s) }, Charsets.UTF_8) + // however, if the request is of MultiPart Form Data type then parsing is blocking in Undertow + val mimeType = exchange.requestHeaders.getFirst(Headers.CONTENT_TYPE) + if (mimeType != null && mimeType.startsWith(OpenAPI.MULTI_PART_FORM_DATA_CONTENT_TYPE)) { + receiveFormDataRequestPayload(subject, exchange) + } + else{ + val receiver = exchange.requestReceiver + receiver.receiveFullString({ e, s -> receiveJsonRequestPayload(subject, e, s) }, Charsets.UTF_8) + } + } + + private fun receiveFormDataRequestPayload(subject: Subject, exchange: HttpServerExchange) { + try { + exchange.startBlocking() + val parser = FormParserFactory.builder().build().createParser(exchange) + val formData = parser.parseBlocking() + val endpointRequest = HttpRequestWithPayload(exchange, subject, + (requestSerializer as MultiPartFormDataSerializer).fromMultiPartFormData(formData)) + executeOperationFromEndpointRequest(exchange, endpointRequest) + } catch (e: IOException) { + + logger.debug("Error parsing multipart/form-data request, which will be returned to the client", e) + sendError(exchange, EndpointErrorMessage("Malformed form data in the request payload", + cause = e, errorType = OperationErrorType.BAD_REQUEST)) + return + } catch (e: SerializationException) { + + logger.debug("Exception during payload deserialization, which will be returned to the client", e) + 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 + } } - private fun receiveRequestPayload(subject: Subject, exchange: HttpServerExchange, payloadString: String) { + private fun receiveJsonRequestPayload(subject: Subject, exchange: HttpServerExchange, payloadString: String) { if (payloadString.isEmpty()) { sendError(exchange, EndpointErrorMessage( "Empty request payload", errorType = OperationErrorType.BAD_REQUEST)) return } try { - parseRequestAndExecuteOperation(subject, exchange, payloadString) + val requestJsonPayload = Json.createReader( + StringReader( + payloadString + ) + ).readObject() + val endpointRequest = HttpRequestWithPayload(exchange, subject, requestSerializer.fromJson(requestJsonPayload)) + logger.debug("Invoking operation with request: {}", endpointRequest) + executeOperationFromEndpointRequest(exchange, endpointRequest) } catch (e: JsonParsingException) { logger.debug("JSON parsing exception, which will be returned to the client", e) sendError(exchange, EndpointErrorMessage("Malformed JSON in the request payload", cause = e, errorType = OperationErrorType.BAD_REQUEST)) return - } catch (e: IOException) { - - logger.debug("Error parsing multipart/form-data request, which will be returned to the client", e) - sendError(exchange, EndpointErrorMessage("Malformed form data in the request payload", - cause = e, errorType = OperationErrorType.BAD_REQUEST)) - return } catch (e: SerializationException) { logger.debug("Exception during payload deserialization, which will be returned to the client", e) @@ -637,38 +670,6 @@ class OperationEndpointHandler( cause = e, errorType = OperationErrorType.BAD_REQUEST)) return } - - } - - private fun parseRequestAndExecuteOperation(subject: Subject, exchange: HttpServerExchange, payloadString: String){ - with(exchange.requestHeaders.getFirst(Headers.CONTENT_TYPE)){ - when{ - contains("application/json") -> { - val requestJsonPayload = Json.createReader( - StringReader( - payloadString - ) - ).readObject() - val endpointRequest = HttpRequestWithPayload(exchange, subject, requestSerializer.fromJson(requestJsonPayload)) - logger.debug("Invoking operation with request: {}", endpointRequest) - executeOperationFromEndpointRequest(exchange, endpointRequest) - } - contains("multipart/form-data") -> { - if (exchange.isInIoThread) { - //Blocking - exchange.dispatch(BlockingHandler(MultiPartFormHandler(requestSerializer, subject))) - - //Non-blocking -/* - val parser = MultiPartParserDefinition().create(exchange) - parser.parse(MultiPartFormHandlerNonBlocking(requestSerializer, subject)) -*/ - return - } - } - else -> throw UnsupportedOperationException("Content-Type is Unsupported") - } - } } private fun executeOperationFromEndpointRequest(exchange: HttpServerExchange, @@ -719,40 +720,4 @@ class OperationEndpointHandler( "responseType=${endpoint.responseType}, endpoint=${endpoint})" } - inner class MultiPartFormHandler(private val requestSerializer: JsonSerializer, - private val subject: Subject): HttpHandler{ - override fun handleRequest(exchange: HttpServerExchange){ - val parser = MultiPartParserDefinition().create(exchange) - try{ - parser.parseBlocking() - val endpointRequest = HttpRequestWithPayload(exchange, subject, - (requestSerializer as MultiPartFormDataSerializer).fromMultiPartFormData( - exchange.getAttachment(FormDataParser.FORM_DATA) - ) - ) - executeOperationFromEndpointRequest(exchange, endpointRequest) - } catch (e : Throwable) { - throw IOException("Multi Part Form parsing failed") - } finally { - parser.close() - } - } - } - - inner class MultiPartFormHandlerNonBlocking(private val requestSerializer: JsonSerializer, - private val subject: Subject): HttpHandler{ - override fun handleRequest(exchange: HttpServerExchange){ - try{ - val endpointRequest = HttpRequestWithPayload(exchange, subject, - (requestSerializer as MultiPartFormDataSerializer).fromMultiPartFormData( - exchange.getAttachment(FormDataParser.FORM_DATA) - ) - ) - executeOperationFromEndpointRequest(exchange, endpointRequest) - } catch (e : Throwable) { - throw IOException("Multi Part Form parsing failed") - } - } - } - } diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt index ecffe57..b45cc6c 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt @@ -279,7 +279,7 @@ class NodeAttachmentEndpoint( operationId = "uploadNodeAttachment" ).withRequestBody( OpenAPI.RequestBody.createMultiPartFormDataRequest( - schemaGenerator.generateSchema(requestType), //can be multiPartFormDataSchema + schemaGenerator.generateSchema(requestType), required = true ) ).withResponse( diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt index 2d85cdd..8da30ba 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/OpenAPI.kt @@ -283,5 +283,10 @@ data class OpenAPI( .add("format", "binary") .build() + val BASE64_STRING: JsonObject = Json.createObjectBuilder() + .add("type", "string") + .add("format", "base64") + .build() + } } From c6e61db6b4e369609c0ea70b0e9d9378c30a46f2 Mon Sep 17 00:00:00 2001 From: Parth Shukla <52910688+parthbond180@users.noreply.github.com> Date: Mon, 15 Nov 2021 20:08:49 +0000 Subject: [PATCH 6/7] resolves #26 optimised test case for upload --- .../main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt | 4 ++-- .../kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt | 2 +- .../kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt index 3b72e5c..5621b9c 100644 --- a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt +++ b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt @@ -114,7 +114,7 @@ class ClientNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVa } override fun createAttachment(attachment: CordaNodeAttachment): SecureHash { - val zipName = "$attachment.filename-${UUID.randomUUID()}.zip" + val zipName = "${attachment.filename}-${UUID.randomUUID()}.zip" FileOutputStream(zipName).use { fileOutputStream -> ZipOutputStream(fileOutputStream).use { zipOutputStream -> val zipEntry = ZipEntry(attachment.filename) @@ -159,7 +159,7 @@ class RPCFlowInitiator : FlowInitiator(), Cordaptor val flowClass = instruction.flowClass.java logger.debug("Preparing to initiate flow {} over Corda RPC connection", flowClass) - val typeInfo: LocalTypeInformation = localTypeModel.inspect(flowClass); + val typeInfo: LocalTypeInformation = localTypeModel.inspect(flowClass) val constructorParameters = when (typeInfo){ is LocalTypeInformation.Composable -> (typeInfo as? LocalTypeInformation.Composable)?.constructor?.parameters diff --git a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt index 8bc366d..a3f7f45 100644 --- a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt +++ b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt @@ -108,7 +108,7 @@ class CordaNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVau } override fun createAttachment(attachment: CordaNodeAttachment): SecureHash { - val zipName = "$attachment.filename-${UUID.randomUUID()}.zip" + val zipName = "${attachment.filename}-${UUID.randomUUID()}.zip" FileOutputStream(zipName).use { fileOutputStream -> ZipOutputStream(fileOutputStream).use { zipOutputStream -> val zipEntry = ZipEntry(attachment.filename) diff --git a/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt b/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt index 1f49a11..713a669 100644 --- a/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt +++ b/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt @@ -7,6 +7,7 @@ import org.eclipse.jetty.client.util.MultiPartContentProvider import org.eclipse.jetty.client.util.PathContentProvider import org.eclipse.jetty.client.util.StringContentProvider import org.eclipse.jetty.http.HttpHeader +import org.junit.jupiter.api.assertDoesNotThrow import tech.b180.ref_cordapp.DelayedProgressFlow import tech.b180.ref_cordapp.SimpleFlow import java.io.StringReader @@ -231,8 +232,9 @@ class CordaptorAPITestSuite( req.content(multiPartContentProvider) val response = req.send() - assertEquals(HttpServletResponse.SC_OK, response.status) assertEquals("application/json", response.mediaType) + assertEquals(HttpServletResponse.SC_ACCEPTED, response.status) + assertDoesNotThrow { SecureHash.parse(response.contentAsString) } /*val page = response.contentAsString.asJsonObject() assertEquals(1, page.getInt("totalStatesAvailable"))*/ From 341e09a9996e7c90aa9ee2ea37c7fcabd8473d05 Mon Sep 17 00:00:00 2001 From: Parth Shukla <52910688+parthbond180@users.noreply.github.com> Date: Wed, 17 Nov 2021 16:06:51 +0000 Subject: [PATCH 7/7] resolves #26 corrected file clean up --- .../b180/cordaptor/rpc/ClientNodeState.kt | 19 +++++++++---------- .../cordaptor/cordapp/InternalNodeState.kt | 18 +++++++++--------- .../cordaptor_test/CordaptorAPITestSuite.kt | 7 ++----- .../resources/ReferenceCordapp.api.json | 2 +- .../b180/cordaptor/rest/NodeStateEndpoints.kt | 6 +++--- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt index 5621b9c..417b5cd 100644 --- a/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt +++ b/corda-rpc-client/src/main/kotlin/tech/b180/cordaptor/rpc/ClientNodeState.kt @@ -122,16 +122,15 @@ class ClientNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVa attachment.inputStream.copyTo(zipOutputStream, 1024) } } - - return FileInputStream(zipName).use { fileInputStream -> - val hash = rpc.uploadAttachmentWithMetadata( - jar = fileInputStream, - uploader = attachment.dataType, - filename = attachment.filename - ) - Files.deleteIfExists(Paths.get(zipName)) - hash - } + val inputStream = FileInputStream(zipName) + val hash = rpc.uploadAttachmentWithMetadata( + jar = inputStream, + uploader = attachment.dataType, + filename = attachment.filename + ) + inputStream.close() + Files.deleteIfExists(Paths.get(zipName)) + return hash } } diff --git a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt index a3f7f45..c6b349d 100644 --- a/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt +++ b/corda-service/src/main/kotlin/tech/b180/cordaptor/cordapp/InternalNodeState.kt @@ -116,15 +116,15 @@ class CordaNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVau attachment.inputStream.copyTo(zipOutputStream, 1024) } } - return FileInputStream(zipName).use { fileInputStream -> - val attachmentHash = appServiceHub.attachments.importAttachment( - jar = fileInputStream, - uploader = attachment.dataType, - filename = attachment.filename - ) - Files.deleteIfExists(Paths.get(zipName)) - attachmentHash - } + val inputStream = FileInputStream(zipName) + val hash = appServiceHub.attachments.importAttachment( + jar = inputStream, + uploader = attachment.dataType, + filename = attachment.filename + ) + inputStream.close() + Files.deleteIfExists(Paths.get(zipName)) + return hash } } diff --git a/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt b/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt index 713a669..61caeba 100644 --- a/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt +++ b/reference-cordapp/src/genericApiTest/kotlin/tech/b180/cordaptor_test/CordaptorAPITestSuite.kt @@ -233,11 +233,8 @@ class CordaptorAPITestSuite( val response = req.send() assertEquals("application/json", response.mediaType) - assertEquals(HttpServletResponse.SC_ACCEPTED, response.status) - assertDoesNotThrow { SecureHash.parse(response.contentAsString) } - - /*val page = response.contentAsString.asJsonObject() - assertEquals(1, page.getInt("totalStatesAvailable"))*/ + assertEquals(HttpServletResponse.SC_OK, response.status) + assertDoesNotThrow { SecureHash.parse(response.contentAsString.replace("\"", "")) } } private fun testVaultQueryViaPOST(client: HttpClient) { diff --git a/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json b/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json index 3b6115b..8bbc527 100644 --- a/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json +++ b/reference-cordapp/src/genericApiTest/resources/ReferenceCordapp.api.json @@ -2106,7 +2106,7 @@ } }, "summary": "Uploads Corda attachment with given parameters", - "tags": ["flowInitiation"] + "tags": ["nodeAttachment"] } }, "/node/version": { diff --git a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt index b45cc6c..38d87f8 100644 --- a/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt +++ b/rest-endpoint/src/main/kotlin/tech/b180/cordaptor/rest/NodeStateEndpoints.kt @@ -17,7 +17,6 @@ import tech.b180.cordaptor.corda.* import tech.b180.cordaptor.kernel.CordaptorComponent import tech.b180.cordaptor.kernel.loggerFor import tech.b180.cordaptor.shaded.javax.json.JsonObject -import java.io.InputStream import java.util.* import java.util.concurrent.TimeUnit import kotlin.math.min @@ -25,6 +24,7 @@ import kotlin.reflect.KClass const val FLOW_INITIATION_TAG = "flowInitiation" const val VAULT_QUERY_TAG = "vaultQuery" +const val NODE_ATTACHMENT_TAG = "nodeAttachment" /** * Factory class for specific Jetty handlers created for flows and contract states of CorDapps found on the node. @@ -269,7 +269,7 @@ class NodeAttachmentEndpoint( logger.debug("Attachment instruction {}", attachmentInstruction) val handle = cordaNodeState.createAttachment(attachment = attachmentInstruction) - return Single.just(Response(handle, StatusCodes.ACCEPTED, emptyList())) + return Single.just(Response(handle, StatusCodes.OK, emptyList())) } override fun generatePathInfoSpecification(schemaGenerator: JsonSchemaGenerator): OpenAPI.PathItem { @@ -288,7 +288,7 @@ class NodeAttachmentEndpoint( description = "Attachment uploaded successfully and its result is available", schema = schemaGenerator.generateSchema(responseType) ) - ).withForbiddenResponse().withTags(FLOW_INITIATION_TAG) + ).withForbiddenResponse().withTags(NODE_ATTACHMENT_TAG) ) } }