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

Story/document upload in bestandsdelen #1587

Merged
merged 39 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
aec0def
feature document upload in bestandsdelen
Jul 11, 2024
1142823
Merge branch 'refs/heads/next-minor' into feature/document-upload-in-…
Jul 17, 2024
9c2d4b1
Merge branch 'refs/heads/next-minor' into feature/document-upload-in-…
Aug 9, 2024
f1f76b6
feature - document upload in parts
Aug 12, 2024
e9dc7bc
Merge branch 'next-minor' into feature/document-upload-in-bestandsdelen
Oct 1, 2024
1c96224
Merge branch 'next-minor' into feature/document-upload-in-bestandsdelen
Oct 1, 2024
9a28d9f
feature document upload in bestandsdelen
Oct 3, 2024
7db9ebf
feature document upload in bestandsdelen
Oct 3, 2024
536196f
feature document upload in bestandsdelen
Oct 3, 2024
1216b88
Merge branch 'next-minor' into story/document-upload-in-bestandsdelen
Rick-Ritense Oct 3, 2024
356292d
feature document upload in bestandsdelen
Oct 4, 2024
4495327
feature document upload in bestandsdelen
Oct 4, 2024
1eaa3d7
Merge remote-tracking branch 'origin/story/document-upload-in-bestand…
Oct 4, 2024
594c210
feature document upload in bestandsdelen
Oct 4, 2024
a94a252
Fixed failing tests
Oct 7, 2024
93febf3
Merge branch 'next-minor' into story/document-upload-in-bestandsdelen
valtimo-platform[bot] Oct 7, 2024
849a4df
Fixed import
Oct 7, 2024
3c5854f
Reading only the bytes that will be sent for that chunk instead of th…
IvarKoreman-Ritense Oct 8, 2024
c04ed37
Update zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/c…
Rick-Ritense Oct 8, 2024
9899438
Code review findings
Oct 8, 2024
780e25a
Merge remote-tracking branch 'origin/story/document-upload-in-bestand…
Oct 8, 2024
72c9449
Code review findings
Oct 8, 2024
e0a823d
Merge branch 'next-minor' into story/document-upload-in-bestandsdelen
valtimo-platform[bot] Oct 8, 2024
27742f0
Fixed a failing test
Oct 8, 2024
fc205e6
Merge remote-tracking branch 'origin/story/document-upload-in-bestand…
Oct 8, 2024
7d68a30
Replaced wildcard imports.
IvarKoreman-Ritense Oct 9, 2024
0463074
Code review findings
Oct 9, 2024
f0f1c59
Merge remote-tracking branch 'origin/story/document-upload-in-bestand…
Oct 9, 2024
d1718fe
Code review findings
Oct 9, 2024
eb76d17
Merge branch 'next-minor' into story/document-upload-in-bestandsdelen
valtimo-platform[bot] Oct 10, 2024
774e734
Ran IntelliJ code formatter on changed files
Oct 10, 2024
f86fc84
Merge remote-tracking branch 'origin/story/document-upload-in-bestand…
Oct 10, 2024
ca9c39b
Update zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/D…
ivo-ritense Oct 10, 2024
4b297be
Update zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/D…
ivo-ritense Oct 10, 2024
46c6489
Update zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/D…
ivo-ritense Oct 10, 2024
cc63182
Update zgw/documenten-api/src/main/kotlin/com/ritense/documentenapi/D…
ivo-ritense Oct 10, 2024
29e665b
Update zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/D…
ivo-ritense Oct 10, 2024
b93776a
Fixed the last test
Oct 10, 2024
b180e76
Update zgw/documenten-api/src/test/kotlin/com/ritense/documentenapi/c…
Rick-Ritense Oct 10, 2024
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
7 changes: 6 additions & 1 deletion app/gzac/src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ spring:
messages:
basename: i18n/messages
main.allow-bean-definition-overriding: true
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
jersey:
application-path: /api/camunda-rest
autoconfigure:
Expand Down Expand Up @@ -219,8 +223,9 @@ valtimo:
acceptedMimeTypes:
- text/plain
- application/pdf
- image/jpeg
- application/xml
- image/jpeg
- image/png
changelog:
pbac:
clear-tables: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.convertValue
import com.ritense.documentenapi.DocumentenApiPlugin.Companion.PLUGIN_KEY
import com.ritense.documentenapi.client.BestandsdelenRequest
import com.ritense.documentenapi.client.CreateDocumentRequest
import com.ritense.documentenapi.client.CreateDocumentResult
import com.ritense.documentenapi.client.DocumentInformatieObject
import com.ritense.documentenapi.client.DocumentLock
import com.ritense.documentenapi.client.DocumentStatusType
import com.ritense.documentenapi.client.DocumentenApiClient
import com.ritense.documentenapi.client.PatchDocumentRequest
Expand Down Expand Up @@ -135,7 +138,7 @@ class DocumentenApiPlugin(
val contentAsInputStream = storageService.getResourceContentAsInputStream(resourceId)
val metadata = storageService.getResourceMetadata(resourceId)

storeDocument(
val documentCreateResult = storeDocument(
execution = execution,
metadata = metadata,
titel = null,
Expand All @@ -146,8 +149,34 @@ class DocumentenApiPlugin(
inhoudAsInputStream = contentAsInputStream,
beschrijving = null,
informatieobjecttype = null,
storedDocumentUrl = DOCUMENT_URL_PROCESS_VAR,
storedDocumentUrl = "",
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
)

execution.setVariable(DOCUMENT_URL_PROCESS_VAR, documentCreateResult.url)
ivo-ritense marked this conversation as resolved.
Show resolved Hide resolved
}

@PluginAction(
key = "store-uploaded-document-in-parts",
title = "Store uploaded document in parts",
description = "Store an uploaded document in the Documenten API in parts using the bestandsdelen api",
activityTypes = [ActivityTypeWithEventName.SERVICE_TASK_START]
)
fun storeUploadedDocumentInParts(
execution: DelegateExecution
) {
val resourceId = execution.getVariable(RESOURCE_ID_PROCESS_VAR) as String?
?: throw IllegalStateException("Failed to store document. No process variable '$RESOURCE_ID_PROCESS_VAR' found.")
val contentAsInputStream = storageService.getResourceContentAsInputStream(resourceId)
val metadata = storageService.getResourceMetadata(resourceId)

val documentCreateResult = storeDocumentInParts(
execution = execution,
metadata = metadata,
bestandsnaam = metadata["filename"].toString(),
inhoudAsInputStream = contentAsInputStream,
)

execution.setVariable(DOCUMENT_URL_PROCESS_VAR, documentCreateResult.url)
}

@PluginAction(
Expand Down Expand Up @@ -245,7 +274,7 @@ class DocumentenApiPlugin(
beschrijving: String?,
informatieobjecttype: String?,
storedDocumentUrl: String,
) {
): CreateDocumentResult {
val vertrouwelijkheidaanduidingEnum = Vertrouwelijkheid.fromKey(
vertrouwelijkheidaanduiding ?: getMetadataField(
metadata,
Expand Down Expand Up @@ -295,6 +324,59 @@ class DocumentenApiPlugin(
"Failed to set the $DOWNLOAD_URL_PROCESS_VAR variable in the DelegateExecution", e
)
}

return documentCreateResult
}

/**
* Using the bestandsdelen api a document can be uploaded in chunks. This upload method entails several api calls
* to store a document:
* - First the document metadata is uploaded without the 'inhoud' parameter. The response of this method will
* contain a 'lock' parameter that must be used in the next call
* - Using the provided lock the contents of the file is uploaded to the bestandsdelen api
* - When the complete file is uploaded the unlock api must be called. This will unlock the document enabling it
* for download.
*/
private fun storeDocumentInParts(
execution: DelegateExecution,
metadata: Map<String, Any?>,
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
bestandsnaam: String?,
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
inhoudAsInputStream: InputStream,
):CreateDocumentResult {
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
val documentCreateResult = storeDocument(
execution = execution,
metadata = metadata,
titel = null,
vertrouwelijkheidaanduiding = null,
status = null,
taal = null,
bestandsnaam = bestandsnaam,
inhoudAsInputStream = InputStream.nullInputStream(),
beschrijving = null,
informatieobjecttype = null,
storedDocumentUrl = ""
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
)

val bestandsdelenRequest = BestandsdelenRequest(
inhoud = inhoudAsInputStream,
lock = documentCreateResult.getLockFromBestanddelen()
)

client.storeDocumentInParts(
authenticationPluginConfiguration,
url,
bestandsdelenRequest,
documentCreateResult,
bestandsnaam)

val documentLock = DocumentLock(documentCreateResult.getLockFromBestanddelen())
client.unlockDocument(
authenticationPluginConfiguration,
url,
documentLock,
documentCreateResult.getDocumentUUIDFromUrl())

return documentCreateResult
ivo-ritense marked this conversation as resolved.
Show resolved Hide resolved
}

private fun getDocumentenApiPluginByInformatieobjectUrl(informatieobjectUrl: URI): PluginConfiguration {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2015-2024 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.documentenapi.client

class Bestandsdelen(
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
val url: String,
val volgnummer: Int,
val omvang: Int,
val voltooid: Boolean,
val lock: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2015-2024 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ritense.documentenapi.client

import java.io.InputStream

class BestandsdelenRequest(
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
val inhoud: InputStream,
val lock:String
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2015-2024 Ritense BV, the Netherlands.
*
* Licensed under EUPL, Version 1.2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

class BestandsdelenResult(
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
var url: String?,
var lock: String?,
val voltooid: Boolean?,
val volgnummer: Int?,
val statusCode: Int?,
val errorMessage: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,24 @@ class CreateDocumentResult(
val bestandsnaam: String,
val bestandsomvang: Long,
val beginRegistratie: LocalDateTime,
)
val bestandsdelen: List<Bestandsdelen>?
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
) {
fun getBestandsdelenIdFromUrl(): String {
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
if (bestandsdelen.isNullOrEmpty()) {
return ""
}
return bestandsdelen[0].url.substring(bestandsdelen[0].url.lastIndexOf("/") + 1)
}

fun getLockFromBestanddelen(): String {
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
if (bestandsdelen.isNullOrEmpty()) {
return ""
}

return bestandsdelen[0].lock
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
}

fun getDocumentUUIDFromUrl(): String {
return url.substring(url.lastIndexOf("/") + 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.ritense.documentenapi.client

import BestandsdelenResult
import com.fasterxml.jackson.databind.ObjectMapper
import com.ritense.documentenapi.DocumentenApiAuthentication
import com.ritense.documentenapi.domain.DocumentenApiColumnKey
Expand All @@ -30,13 +31,17 @@ import com.ritense.outbox.OutboxService
import com.ritense.zgw.ClientTools
import com.ritense.zgw.ClientTools.Companion.optionalQueryParam
import com.ritense.zgw.Page
import mu.KotlinLogging
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.Pageable
import org.springframework.http.MediaType
import org.springframework.http.converter.ResourceHttpMessageConverter
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.RestClient
import org.springframework.web.client.body
import org.springframework.web.util.UriBuilder
Expand Down Expand Up @@ -72,6 +77,75 @@ class DocumentenApiClient(
return result
}

fun storeDocumentInParts(
authentication: DocumentenApiAuthentication,
baseUrl: URI,
request: BestandsdelenRequest,
createDocumentResult: CreateDocumentResult,
bestandsnaam: String?
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
): BestandsdelenResult {
requireNotNull(bestandsnaam) { "Bestandsnaam must be set otherwise uploading in bestanddelen will fail" }

// Inside the CreateDocumentResult there is an array of bestandsdelen.
// Each bestandsdeel needs to be sent separately
// So the documenten api determines the amount of chunks, not this application.
val fileBytes = request.inhoud.readAllBytes()
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
var response: BestandsdelenResult? = null
var start = 0

logger.info("Starting upload of file in {} chunks\n", createDocumentResult.bestandsdelen?.size)
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved

for(bestandsdeel in createDocumentResult.bestandsdelen!!) {
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Sending chunk #{} for a size of {}",
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
bestandsdeel.volgnummer,
bestandsdeel.omvang)

// Compute the correct end index of the chunk
val end = start + bestandsdeel.omvang
val chunk = fileBytes.copyOfRange(start, end)

val fileResource = object : ByteArrayResource(chunk) {
override fun getFilename(): String {
return bestandsnaam
}
}

val body: MultiValueMap<String, Any> = LinkedMultiValueMap()
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
body.add("inhoud", fileResource)
body.add("lock", request.lock)

response = restClient(authentication)
.put()
.uri {
ClientTools.baseUrlToBuilder(it, baseUrl)
.path("bestandsdelen/{uuid}")
.build(bestandsdeel.url.substring(bestandsdeel.url.lastIndexOf("/") + 1))
}
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.body<BestandsdelenResult>()!!

start = end
}

return response ?: throw IllegalStateException("Upload in chunks failed")
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
}

fun unlockDocument(authentication: DocumentenApiAuthentication, baseUrl: URI,
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
documentLock: DocumentLock, bestandsdelenId: String) {
restClient(authentication)
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
.post()
.uri { ClientTools.baseUrlToBuilder(it, baseUrl)
.path("enkelvoudiginformatieobjecten/{uuid}/unlock")
.build(bestandsdelenId)
}
.contentType(MediaType.APPLICATION_JSON)
.body(documentLock)
.retrieve()
.toBodilessEntity()
}

fun getInformatieObject(
authentication: DocumentenApiAuthentication,
baseUrl: URI,
Expand All @@ -96,6 +170,7 @@ class DocumentenApiClient(
objectMapper.valueToTree(result)
)
}

return result
}

Expand All @@ -107,12 +182,8 @@ class DocumentenApiClient(
): org.springframework.data.domain.Page<DocumentInformatieObject> {
// because the documenten api only supports a fixed page size, we will try to calculate the page we need to request
// the only page sizes that are supported are those that can fit n times in the itemsPerPage
if (ITEMS_PER_PAGE % pageable.pageSize != 0) {
throw IllegalArgumentException("Page size is not supported")
}
if (documentSearchRequest.zaakUrl == null) {
throw IllegalArgumentException("Zaak URL is required")
}
require(ITEMS_PER_PAGE % pageable. pageSize == 0) { "Page size is not supported" }
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
requireNotNull(documentSearchRequest.zaakUrl) { "Zaak URL is required" }

val pageToRequest = ((pageable.pageSize * pageable.pageNumber) / ITEMS_PER_PAGE) + 1
val result =
Expand Down Expand Up @@ -297,5 +368,6 @@ class DocumentenApiClient(

companion object {
const val ITEMS_PER_PAGE = 100
var logger = KotlinLogging.logger{}
Rick-Ritense marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading