Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 26 #27

Merged
merged 7 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -99,6 +100,8 @@ interface CordaNodeState : PartyLocator {
fun <T : ContractState> trackStates(query: CordaVaultQuery<T>): CordaDataFeed<T>

fun <ReturnType: Any> initiateFlow(instruction: CordaFlowInstruction<FlowLogic<ReturnType>>): CordaFlowHandle<ReturnType>

fun createAttachment(attachment: CordaNodeAttachment): SecureHash
}

/**
Expand Down Expand Up @@ -157,6 +160,14 @@ data class CordaFlowInstruction<FlowClass: FlowLogic<Any>>(
)
}

@ModuleAPI(since = "0.1")
data class CordaNodeAttachment(
val inputStream: InputStream,
val uploader: String,
val filename: String,
val dataType: 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +112,26 @@ class ClientNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVa

return get<RPCFlowInitiator<ReturnType>>().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)
}
}
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
}
}

/**
Expand All @@ -131,7 +158,7 @@ class RPCFlowInitiator<ReturnType: Any> : FlowInitiator<ReturnType>(), 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -99,6 +106,26 @@ class CordaNodeStateImpl : CordaNodeStateInner, CordaptorComponent, CordaNodeVau

return get<LocalFlowInitiator<ReturnType>>().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)
}
}
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
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ 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 org.junit.jupiter.api.assertDoesNotThrow
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
Expand Down Expand Up @@ -41,6 +45,7 @@ class CordaptorAPITestSuite(
testStateQuery(client, stateRef)
testVaultQueryViaGET(client)
testVaultQueryViaPOST(client)
testNodeAttachmentViaPOST(client)
}

private fun testOpenAPISpecification(client: HttpClient) {
Expand Down Expand Up @@ -211,9 +216,30 @@ 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("application/json", response.mediaType)
assertEquals(HttpServletResponse.SC_OK, response.status)
assertDoesNotThrow { SecureHash.parse(response.contentAsString.replace("\"", "")) }
}

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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": ["nodeAttachment"]
}
},
"/node/version": {
"get": {
"operationId": "getNodeVersion",
Expand Down
5 changes: 5 additions & 0 deletions reference-cordapp/src/genericApiTest/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
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
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
Expand Up @@ -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"
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,18 +14,13 @@ 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.math.BigDecimal
import java.math.RoundingMode
Expand Down Expand Up @@ -100,6 +96,43 @@ class BigDecimalSerializer
}
}

class CordaNodeAttachmentSerializer : MultiPartFormDataSerializer<CordaNodeAttachment> {
override fun fromJson(value: JsonValue): CordaNodeAttachment {
throw UnsupportedOperationException("Don't know not to restore an untyped object from JSON")
}

override fun toJson(obj: CordaNodeAttachment, generator: JsonGenerator) {
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()

}

/**
* Serializes a [Currency] as a JSON string representing its ISO code.
* Mainly used as part of the implementation for serializer of [Amount], but
Expand Down Expand Up @@ -418,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
Loading