diff --git a/NEWS.md b/NEWS.md
index 169645b9..5f63434b 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,10 @@
+## 2024-04-16 v3.2.1
+
+[Full Changelog](https://github.com/folio-org/mod-data-export-spring/compare/v3.2.0...v3.2.1)
+
+### Bug fixes
+* [MODEXPS-261](https://folio-org.atlassian.net/browse/MODEXPS-261) "Request has expired" or "Expired token" errors
+
## 2024-03-19 v3.2.0
[Full Changelog](https://github.com/folio-org/mod-data-export-spring/compare/v3.1.0...v3.2.0)
diff --git a/pom.xml b/pom.xml
index b50c102a..a41108d4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
org.folio
mod-data-export-spring
Data Export Spring module
- 3.2.1-SNAPSHOT
+ 3.3.0-SNAPSHOT
jar
diff --git a/src/main/java/org/folio/des/controller/JobsController.java b/src/main/java/org/folio/des/controller/JobsController.java
index c70e5789..de7e22c3 100644
--- a/src/main/java/org/folio/des/controller/JobsController.java
+++ b/src/main/java/org/folio/des/controller/JobsController.java
@@ -66,9 +66,9 @@ public ResponseEntity resendExportedFile(UUID jobId) {
}
@Override
- public ResponseEntity downloadExportedFileByJobId(UUID id) {
- log.info("downloadExportedFileByJobId:: with id={}.", id);
- return ResponseEntity.ok(new InputStreamResource(service.downloadExportedFile(id)));
+ public ResponseEntity downloadExportedFileByJobId(UUID id, String key) {
+ log.info("downloadExportedFileByJobId:: with id={}, key={}.", id, key);
+ return ResponseEntity.ok(new InputStreamResource(service.downloadExportedFile(id, key)));
}
@Override
diff --git a/src/main/java/org/folio/des/service/JobService.java b/src/main/java/org/folio/des/service/JobService.java
index 8759d8ba..29330e32 100644
--- a/src/main/java/org/folio/des/service/JobService.java
+++ b/src/main/java/org/folio/des/service/JobService.java
@@ -59,8 +59,9 @@ public interface JobService {
/**
* Downloading the exported file. A job can have only one exported file.
* @param jobId the job id
+ * @param key the key of the file in the storage
* @return Input stream of exported file.
*/
- InputStream downloadExportedFile(UUID jobId);
+ InputStream downloadExportedFile(UUID jobId, String key);
}
diff --git a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java
index 4fab1cd8..19642b5c 100644
--- a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java
+++ b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java
@@ -1,5 +1,6 @@
package org.folio.des.service.impl;
+import static java.util.Objects.nonNull;
import static org.folio.des.domain.dto.ExportType.BULK_EDIT_IDENTIFIERS;
import static org.folio.des.domain.dto.ExportType.BULK_EDIT_QUERY;
import static org.folio.des.domain.dto.ExportType.BULK_EDIT_UPDATE;
@@ -247,7 +248,7 @@ public void deleteJobs(List jobs) {
}
@Override
- public InputStream downloadExportedFile(UUID jobId) {
+ public InputStream downloadExportedFile(UUID jobId, String key) {
log.debug("downloadExportedFile:: download exported files for jobId={}.", jobId);
Job job = getJobEntity(jobId);
if (CollectionUtils.isEmpty(job.getFileNames())) {
@@ -256,14 +257,14 @@ public InputStream downloadExportedFile(UUID jobId) {
}
log.debug("Refreshing download url for jobId: {}", job.getId());
try {
- PresignedUrl presignedUrl = exportWorkerClient.getRefreshedPresignedUrl(job.getFiles().get(0));
+ PresignedUrl presignedUrl = getPresignedUrl(job, key);
URL url = new URL(presignedUrl.getUrl());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(CONNECTION_TIMEOUT);
return conn.getInputStream();
} catch (Exception e) {
- log.error("Error downloading a file: {} for jobId: {}", e.getMessage(), job.getId());
+ log.error("Error downloading a file: {} for jobId: {} and key: {}", e.getMessage(), job.getId(), key);
throw new FileDownloadException(String.format("Error downloading a file: %s", e));
}
}
@@ -311,4 +312,11 @@ private String getUserName(FolioExecutionContext context) {
Optional userInfo = StringUtils.isBlank(jwt) ? Optional.empty() : JWTokenUtils.parseToken(jwt);
return StringUtils.substring(userInfo.map(JWTokenUtils.UserInfo::getUserName).orElse(null), 0, 50);
}
+
+ private PresignedUrl getPresignedUrl(Job job, String key) {
+ if (nonNull(key)) {
+ return exportWorkerClient.getRefreshedPresignedUrl(key);
+ }
+ return exportWorkerClient.getRefreshedPresignedUrl(job.getFiles().get(0));
+ }
}
diff --git a/src/main/resources/swagger.api/jobs.yaml b/src/main/resources/swagger.api/jobs.yaml
index 280b70b9..d6479e34 100644
--- a/src/main/resources/swagger.api/jobs.yaml
+++ b/src/main/resources/swagger.api/jobs.yaml
@@ -177,6 +177,12 @@ paths:
description: UUID of the job
schema:
$ref: "#/components/schemas/UUID"
+ - name: key
+ in: query
+ required: false
+ description: Key of the file in storage to be downloaded
+ schema:
+ type: string
responses:
"200":
description: Export file successfully retrieved
diff --git a/src/test/java/org/folio/des/controller/JobsControllerTest.java b/src/test/java/org/folio/des/controller/JobsControllerTest.java
index 4fa54e2a..3e13db7b 100644
--- a/src/test/java/org/folio/des/controller/JobsControllerTest.java
+++ b/src/test/java/org/folio/des/controller/JobsControllerTest.java
@@ -99,8 +99,8 @@ void getJobs() throws Exception {
matchAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON_VALUE),
- jsonPath("$.totalRecords", is(8)),
- jsonPath("$.jobRecords", hasSize(8))));
+ jsonPath("$.totalRecords", is(9)),
+ jsonPath("$.jobRecords", hasSize(9))));
}
@Test
@@ -115,7 +115,7 @@ void findSortedJobs() throws Exception {
matchAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON_VALUE),
- jsonPath("$.totalRecords", is(8)),
+ jsonPath("$.totalRecords", is(9)),
jsonPath("$.jobRecords", hasSize(3))));
}
@@ -131,7 +131,7 @@ void findSortedJobsByExportMethodName() throws Exception {
matchAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON_VALUE),
- jsonPath("$.totalRecords", is(8)),
+ jsonPath("$.totalRecords", is(9)),
jsonPath("$.jobRecords", hasSize(3))));
}
@@ -177,8 +177,8 @@ void excludeJobById() throws Exception {
matchAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON_VALUE),
- jsonPath("$.totalRecords", is(7)),
- jsonPath("$.jobRecords", hasSize(7))));
+ jsonPath("$.totalRecords", is(8)),
+ jsonPath("$.jobRecords", hasSize(8))));
}
@Test
@@ -193,8 +193,8 @@ void findJobsBySourceOrDesc() throws Exception {
matchAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON_VALUE),
- jsonPath("$.totalRecords", is(7)),
- jsonPath("$.jobRecords", hasSize(7))));
+ jsonPath("$.totalRecords", is(8)),
+ jsonPath("$.jobRecords", hasSize(8))));
}
@Test
@@ -239,8 +239,8 @@ void findJobsByQuery() throws Exception {
matchAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON_VALUE),
- jsonPath("$.totalRecords", is(3)),
- jsonPath("$.jobRecords", hasSize(3))));
+ jsonPath("$.totalRecords", is(4)),
+ jsonPath("$.jobRecords", hasSize(4))));
}
@Test
@@ -289,6 +289,23 @@ void shouldFailedDownloadWithBadRequest() throws Exception {
status().is5xxServerError()));
}
+ @Test
+ @DisplayName("Should succeed download file with key to storage")
+ void shouldSucceedDownloadWithKey() throws Exception {
+ PresignedUrl presignedUrl = new PresignedUrl();
+ presignedUrl.setUrl("http://localhost:" + WIRE_MOCK_PORT + "/TestFile.csv");
+ when(exportWorkerClient.getRefreshedPresignedUrl(anyString())).thenReturn(presignedUrl);
+ mockMvc
+ .perform(
+ get("/data-export-spring/jobs/22ae5d0f-6425-82a1-a361-1bc9b88e8172/download?key=TestFile.csv")
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .headers(defaultHeaders()))
+ .andExpect(
+ matchAll(
+ status().is2xxSuccessful(),
+ content().bytes("Test file content".getBytes())));
+ }
+
@Test
@DisplayName("Can not fetch job with wrong id")
void notFoundJob() throws Exception {
diff --git a/src/test/resources/job.sql b/src/test/resources/job.sql
index b7069048..30c8d156 100644
--- a/src/test/resources/job.sql
+++ b/src/test/resources/job.sql
@@ -48,6 +48,20 @@ VALUES ('42ae5d0f-6425-82a1-a361-1bc9b88e8172', '000101', 'test-desc', 'data-exp
'data-export-system-user', 'Fees & Fines Bursar Report', null, 'COMPLETED', '{
"exitCode": "COMPLETED"
}');
+INSERT INTO diku_mod_data_export_spring.job (id, name, description, source, is_system_source, type,
+ export_type_specific_parameters, status, files, file_names,
+ start_time, end_time, created_date, created_by_user_id,
+ created_by_username, updated_date, updated_by_user_id,
+ updated_by_username, output_format, error_details,
+ batch_status, exit_status)
+VALUES ('22ae5d0f-6425-82a1-a361-1bc9b88e8172', '000102', 'test-desc', 'data-export-system-user', true,
+ 'BULK_EDIT_IDENTIFIERS',
+ '{}',
+ 'SUCCESSFUL', '["http://localhost/test-url/"]', '["TestFile.csv"]', '2021-03-16 09:29:54.250000', '2021-03-16 09:30:09.831000', '2021-03-16 09:29:54.170000',
+ null, 'data-export-system-user', '2021-03-16 09:30:09.968000', null,
+ 'data-export-system-user', 'Bulk Edit report', null, 'COMPLETED', '{
+ "exitCode": "COMPLETED"
+ }');
INSERT INTO diku_mod_data_export_spring.job (id, name, description, source, is_system_source, type,
export_type_specific_parameters, status, files,
start_time, end_time, created_date, created_by_user_id,
diff --git a/src/test/resources/mappings/files.json b/src/test/resources/mappings/files.json
new file mode 100644
index 00000000..49a83606
--- /dev/null
+++ b/src/test/resources/mappings/files.json
@@ -0,0 +1,17 @@
+{
+ "mappings": [
+ {
+ "request": {
+ "method": "GET",
+ "url": "/TestFile.csv"
+ },
+ "response": {
+ "status": 200,
+ "body": "Test file content",
+ "headers": {
+ "Content-Type": "text/plain"
+ }
+ }
+ }
+ ]
+}