Skip to content

Commit

Permalink
PI-1635 Stream Alfresco responses (#2637)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-bcl authored Nov 15, 2023
1 parent 8db4552 commit 5811ae6
Show file tree
Hide file tree
Showing 8 changed files with 37 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@EnableAsync
@Configuration
class ThreadConfig {
class ThreadConfig : WebMvcConfigurer {
@Value("\${async-task-executor.threads.core:8}")
private val coreThreads = 0

Expand All @@ -28,8 +30,13 @@ class ThreadConfig {
executor.maxPoolSize = maxThreads
executor.setThreadNamePrefix("AsyncTask: ")
executor.setTaskDecorator { ContextRunnable(it) }
executor.initialize()
return executor
}

override fun configureAsyncSupport(configurer: AsyncSupportConfigurer) {
configurer.setTaskExecutor(asyncTaskExecutor())
}
}

internal class ContextRunnable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class WireMockInitialiser : ApplicationContextInitializer<ConfigurableApplicatio
WireMockConfiguration()
.port(wmPort)
.usingFilesUnderClasspath("simulations")
.maxLoggedResponseSize(100_000)
)
wireMockServer.start()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
},
"response": {
"headers": {
"Content-Type": "application/octet-stream",
"Content-Disposition": "attachment; filename=\"doc1\"; filename*=UTF-8''doc1",
"content-type": "application/msword;charset=UTF-8",
"content-length": "14812",
"content-disposition": "attachment; filename=\"doc1\"; filename*=UTF-8''doc1",
"accept-ranges": "bytes",
"content-range": "bytes 0-14811/14812",
"cache-control": "max-age=0, must-revalidate",
"etag": "1699630109776",
"last-modified": "Fri, 10 Nov 2023 15:28:29 GMT",
"Custom-Alfresco-Header": "should be ignored"
},
"status": 200,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.request
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.util.ResourceUtils
import uk.gov.justice.digital.hmpps.data.generator.PersonGenerator
Expand Down Expand Up @@ -65,6 +67,8 @@ internal class IntegrationTest {
@Test
fun `document can be downloaded`() {
mockMvc.perform(get("/document/uuid1").accept("application/octet-stream").withOAuth2Token(wireMockServer))
.andExpect(request().asyncStarted())
.andDo(MvcResult::getAsyncResult)
.andExpect(status().is2xxSuccessful)
.andExpect(header().string("Content-Type", "application/octet-stream"))
.andExpect(header().string("Content-Disposition", "attachment; filename=\"=?UTF-8?Q?OFFENDER-related_document?=\"; filename*=UTF-8''OFFENDER-related%20document"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package uk.gov.justice.digital.hmpps.client

import feign.Response
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.core.io.Resource
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import uk.gov.justice.digital.hmpps.config.FeignConfig

@FeignClient(name = "alfresco", url = "\${integrations.alfresco.url}", configuration = [FeignConfig::class])
interface AlfrescoClient {
@GetMapping(value = ["/fetch/{id}"])
fun getDocument(@PathVariable id: String): ResponseEntity<Resource>
fun getDocument(@PathVariable id: String): Response
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package uk.gov.justice.digital.hmpps.controller

import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import uk.gov.justice.digital.hmpps.exception.NotFoundException
import uk.gov.justice.digital.hmpps.service.DocumentService

@RestController
Expand All @@ -25,9 +23,5 @@ class DocumentController(private val documentService: DocumentService) {

@GetMapping(value = ["/document/{id}"])
@Operation(summary = "Download document content")
fun downloadDocument(@PathVariable id: String) = try {
documentService.downloadDocument(id)
} catch (e: NotFoundException) {
ResponseEntity.notFound()
}
fun downloadDocument(@PathVariable id: String) = documentService.downloadDocument(id)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package uk.gov.justice.digital.hmpps.service

import org.springframework.core.io.Resource
import feign.Response
import org.springframework.http.ContentDisposition
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpHeaders.CONTENT_DISPOSITION
import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
import uk.gov.justice.digital.hmpps.client.AlfrescoClient
import uk.gov.justice.digital.hmpps.datetime.EuropeLondon
import uk.gov.justice.digital.hmpps.entity.CourtAppearance
Expand All @@ -27,18 +29,19 @@ class DocumentService(
private val documentRepository: DocumentRepository,
private val alfrescoClient: AlfrescoClient
) {
fun downloadDocument(id: String): ResponseEntity<Resource> {
fun downloadDocument(id: String): ResponseEntity<StreamingResponseBody> {
val filename = documentRepository.findNameByAlfrescoId(id) ?: throw NotFoundException("Document", "alfrescoId", id)
val response = alfrescoClient.getDocument(id)
return when {
response.statusCode.is2xxSuccessful -> ResponseEntity.ok()
return when (response.status()) {
200 -> ResponseEntity.ok()
.headers { it.putAll(response.sanitisedHeaders()) }
.header(CONTENT_DISPOSITION, ContentDisposition.attachment().filename(filename, UTF_8).build().toString())
.body(response.body)
.contentType(APPLICATION_OCTET_STREAM)
.body(StreamingResponseBody { response.body().asInputStream().copyTo(it) })

response.statusCode.is4xxClientError -> throw NotFoundException("Document content", "alfrescoId", id)
404 -> throw NotFoundException("Document content", "alfrescoId", id)

else -> throw RuntimeException("Failed to download document. Alfresco responded with ${response.statusCode}.")
else -> throw RuntimeException("Failed to download document. Alfresco responded with ${response.status()}.")
}
}

Expand Down Expand Up @@ -87,12 +90,11 @@ class DocumentService(
private val Disposal.description get() = "${type.description}${lengthString?.let { " ($it)" } ?: ""}"
private val Disposal.lengthString get() = length?.let { "$length ${lengthUnits!!.description}" }
private fun List<CourtAppearance>.latestOutcome() = filter { it.outcome != null }.maxByOrNull { it.date }?.outcome
private fun <T> ResponseEntity<T>.sanitisedHeaders() = headers.filterKeys {
private fun Response.sanitisedHeaders(): Map<String, List<String>> = headers().filterKeys {
it in listOf(
HttpHeaders.CONTENT_LENGTH,
HttpHeaders.CONTENT_TYPE,
HttpHeaders.ETAG,
HttpHeaders.LAST_MODIFIED
)
}
}.mapValues { it.value.toList() }
}
2 changes: 1 addition & 1 deletion projects/dps-and-delius/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ spring:
default:
logger-level: full
connect-timeout: 5000
read-timeout: 5000
read-timeout: 30000
default-request-headers:
Accept: application/json
Content-Type: application/json
Expand Down

0 comments on commit 5811ae6

Please sign in to comment.