From a320b5a5e2fbb442d4ae159a7cbb88dc970f9096 Mon Sep 17 00:00:00 2001 From: Daniil Palagin Date: Wed, 17 Jul 2024 16:56:57 +0200 Subject: [PATCH 1/8] [kbss-cvut/record-manager-ui#184] Add excelImportServiceUrl param to ConfigParam --- src/main/java/cz/cvut/kbss/study/util/ConfigParam.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java b/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java index 9cb58769..dd5ca7fe 100644 --- a/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java +++ b/src/main/java/cz/cvut/kbss/study/util/ConfigParam.java @@ -10,6 +10,7 @@ public enum ConfigParam { FORM_GEN_SERVICE_URL("formGenServiceUrl"), ON_UPDATE_RECORD_SERVICE_URL("onRecordUpdateServiceUrl"), + EXCEL_IMPORT_SERVICE_URL("excelImportServiceUrl"), APP_CONTEXT("appContext"), From 538cb3a6cf2e558961b5be697f1899c5c1bac333 Mon Sep 17 00:00:00 2001 From: Daniil Palagin Date: Wed, 17 Jul 2024 20:43:07 +0200 Subject: [PATCH 2/8] [kbss-cvut/record-manager-ui#184] Add Excel type to importRecords --- .../study/rest/PatientRecordController.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java index 987c4b56..909d9c76 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java @@ -152,17 +152,26 @@ public ResponseEntity createRecord(@RequestBody PatientRecord record) { return new ResponseEntity<>(headers, HttpStatus.CREATED); } - @PostMapping(value = "/import", consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/import", consumes = MediaType.APPLICATION_JSON_VALUE, MediaType.MEDIA_TYPE_EXCEL) public RecordImportResult importRecords(@RequestBody List records, - @RequestParam(name = "phase", required = false) String phase) { + @RequestParam(name = "phase", required = false) String phase, + @RequestHeader(value = "Content-Type") String contentType + ) { + final RecordImportResult importResult; - if (phase != null) { - final RecordPhase targetPhase = RecordPhase.fromIriOrName(phase); - importResult = recordService.importRecords(records, targetPhase); - } else { - importResult = recordService.importRecords(records); + if(contentType.equals(MediaType.APPLICATION_JSON)){ + if (phase != null) { + final RecordPhase targetPhase = RecordPhase.fromIriOrName(phase); + importResult = recordService.importRecords(records, targetPhase); + } else { + importResult = recordService.importRecords(records); + } + LOG.trace("Records imported with result: {}.", importResult); + }else if(contentType.equals(MediaType.MEDIA_TYPE_EXCEL)){ + String excelImportServiceUrl = configReader.getConfig(ConfigParam.EXCEL_IMPORT_SERVICE_URL); + importResult = restTemplate.postForEntity(URI.create(publishServiceUrl), records, RecordImportResult.class).getBody(); + LOG.trace("Records imported with result: {}.", importResult); } - LOG.trace("Records imported with result: {}.", importResult); return importResult; } From 1d9341ffb7e3b4d1c398321a6314dec974a6969b Mon Sep 17 00:00:00 2001 From: Daniil Palagin Date: Fri, 19 Jul 2024 14:45:18 +0200 Subject: [PATCH 3/8] [kbss-cvut/record-manager-ui#184] Update importRecords endpoint for supporting excel file. --- .../study/rest/PatientRecordController.java | 68 ++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java index 909d9c76..a33488b7 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java @@ -1,5 +1,7 @@ package cz.cvut.kbss.study.rest; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.cvut.kbss.study.dto.PatientRecordDto; import cz.cvut.kbss.study.dto.RecordImportResult; import cz.cvut.kbss.study.exception.NotFoundException; @@ -20,15 +22,19 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.InputStreamResource; import org.springframework.data.domain.Page; import org.springframework.http.*; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.util.UriComponentsBuilder; +import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.*; @@ -152,27 +158,57 @@ public ResponseEntity createRecord(@RequestBody PatientRecord record) { return new ResponseEntity<>(headers, HttpStatus.CREATED); } - @PostMapping(value = "/import", consumes = MediaType.APPLICATION_JSON_VALUE, MediaType.MEDIA_TYPE_EXCEL) - public RecordImportResult importRecords(@RequestBody List records, - @RequestParam(name = "phase", required = false) String phase, - @RequestHeader(value = "Content-Type") String contentType - ) { + @PostMapping(value = "/import/{format}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public RecordImportResult importRecords(@RequestPart("file") MultipartFile file, + @PathVariable("format") String format, + @RequestParam(name = "phase", required = false) String phase) { + if(file.isEmpty()) + throw new IllegalArgumentException("Cannot import records, missing input file"); + List records; + records = parseRecords(file, format); final RecordImportResult importResult; - if(contentType.equals(MediaType.APPLICATION_JSON)){ - if (phase != null) { - final RecordPhase targetPhase = RecordPhase.fromIriOrName(phase); - importResult = recordService.importRecords(records, targetPhase); - } else { - importResult = recordService.importRecords(records); + if (phase != null) { + final RecordPhase targetPhase = RecordPhase.fromIriOrName(phase); + importResult = recordService.importRecords(records, targetPhase); + } else { + importResult = recordService.importRecords(records); + } + LOG.trace("Records imported with result: {}.", importResult); + return importResult; + } + + private List parseRecords(MultipartFile file, String format) { + format = format.toLowerCase(); + if (format.equals("json")) { + try { + return new ObjectMapper().readValue(file.getBytes(), new TypeReference>(){}); + } catch (IOException e) { + throw new RuntimeException("Failed to parse JSON content", e); } - LOG.trace("Records imported with result: {}.", importResult); - }else if(contentType.equals(MediaType.MEDIA_TYPE_EXCEL)){ + } else if (format.equals("xls") || format.equals("xlsx")) { String excelImportServiceUrl = configReader.getConfig(ConfigParam.EXCEL_IMPORT_SERVICE_URL); - importResult = restTemplate.postForEntity(URI.create(publishServiceUrl), records, RecordImportResult.class).getBody(); - LOG.trace("Records imported with result: {}.", importResult); + if (excelImportServiceUrl == null) { + throw new IllegalArgumentException("Cannot import XLS, excelImportServiceUrl is not configured"); + } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", file.getResource()); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity> responseEntity = restTemplate.exchange( + URI.create(excelImportServiceUrl), + HttpMethod.POST, + requestEntity, + new ParameterizedTypeReference>() {} + ); + return responseEntity.getBody(); + } else { + throw new RuntimeException("Unsupported file format: " + format); } - return importResult; } @PutMapping(value = "/{key}", consumes = MediaType.APPLICATION_JSON_VALUE) From 4283ce6b171c51963c0f1059b237c0c1371f9138 Mon Sep 17 00:00:00 2001 From: Daniil Palagin Date: Sat, 20 Jul 2024 11:40:05 +0200 Subject: [PATCH 4/8] [kbss-cvut/record-manager-ui#184] Changed handler for importing json and excel. Divided into two handlers --- .../study/rest/PatientRecordController.java | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java index a33488b7..d2f7d58f 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java @@ -158,15 +158,55 @@ public ResponseEntity createRecord(@RequestBody PatientRecord record) { return new ResponseEntity<>(headers, HttpStatus.CREATED); } - @PostMapping(value = "/import/{format}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public RecordImportResult importRecords(@RequestPart("file") MultipartFile file, - @PathVariable("format") String format, + @PostMapping(value = "/import/json", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public RecordImportResult importRecordsJson(@RequestPart("file") MultipartFile file, @RequestParam(name = "phase", required = false) String phase) { + List records; + if(file.isEmpty()) throw new IllegalArgumentException("Cannot import records, missing input file"); + try { + records = new ObjectMapper().readValue(file.getBytes(), new TypeReference>(){}); + } catch (IOException e) { + throw new RuntimeException("Failed to parse JSON content", e); + } + return importRecords(records, phase); + } + + @PostMapping(value = "/import/excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public RecordImportResult importRecordsExcel(@RequestPart("file") MultipartFile file, + @RequestParam(name = "phase", required = false) String phase) { + List records; - records = parseRecords(file, format); + + if(file.isEmpty()) + throw new IllegalArgumentException("Cannot import records, missing input file"); + + String excelImportServiceUrl = configReader.getConfig(ConfigParam.EXCEL_IMPORT_SERVICE_URL); + + if (excelImportServiceUrl == null) + throw new IllegalArgumentException("Cannot import XLS, excelImportServiceUrl is not configured"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", file.getResource()); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity> responseEntity = restTemplate.exchange( + URI.create(excelImportServiceUrl), + HttpMethod.POST, + requestEntity, + new ParameterizedTypeReference>() {} + ); + records = responseEntity.getBody(); + return importRecords(records, phase); + } + + public RecordImportResult importRecords(List records, String phase) { final RecordImportResult importResult; if (phase != null) { final RecordPhase targetPhase = RecordPhase.fromIriOrName(phase); @@ -178,38 +218,6 @@ public RecordImportResult importRecords(@RequestPart("file") MultipartFile file, return importResult; } - private List parseRecords(MultipartFile file, String format) { - format = format.toLowerCase(); - if (format.equals("json")) { - try { - return new ObjectMapper().readValue(file.getBytes(), new TypeReference>(){}); - } catch (IOException e) { - throw new RuntimeException("Failed to parse JSON content", e); - } - } else if (format.equals("xls") || format.equals("xlsx")) { - String excelImportServiceUrl = configReader.getConfig(ConfigParam.EXCEL_IMPORT_SERVICE_URL); - if (excelImportServiceUrl == null) { - throw new IllegalArgumentException("Cannot import XLS, excelImportServiceUrl is not configured"); - } - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA); - - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("file", file.getResource()); - - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - - ResponseEntity> responseEntity = restTemplate.exchange( - URI.create(excelImportServiceUrl), - HttpMethod.POST, - requestEntity, - new ParameterizedTypeReference>() {} - ); - return responseEntity.getBody(); - } else { - throw new RuntimeException("Unsupported file format: " + format); - } - } @PutMapping(value = "/{key}", consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) From 8ccea377891cdbd7a3f7e6a01ec5946944dc3612 Mon Sep 17 00:00:00 2001 From: Daniil Palagin Date: Sat, 20 Jul 2024 14:48:46 +0200 Subject: [PATCH 5/8] [kbss-cvut/record-manager-ui#184] Updated tests for importRecords --- .../rest/PatientRecordControllerTest.java | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java index 06ced129..7fca2a90 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java @@ -1,6 +1,7 @@ package cz.cvut.kbss.study.rest; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import cz.cvut.kbss.study.dto.PatientRecordDto; import cz.cvut.kbss.study.dto.RecordImportResult; import cz.cvut.kbss.study.environment.generator.Generator; @@ -32,6 +33,7 @@ import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MvcResult; import java.time.LocalDate; @@ -51,10 +53,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) @@ -302,28 +301,63 @@ void exportRecordsExportsRecordsForProvidedInstitutionForSpecifiedPeriod() throw Pageable.unpaged()); } +// @Test +// void importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult() throws Exception { +// final List records = +// List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); +// final RecordImportResult importResult = new RecordImportResult(records.size()); +// importResult.setImportedCount(records.size()); +// when(patientRecordServiceMock.importRecords(anyList())).thenReturn(importResult); +// +// final MvcResult mvcResult = mockMvc.perform( +// post("/records/import/json").content(toJson(records)).contentType(MediaType.MULTIPART_FORM_DATA_VALUE)).andReturn(); +// final RecordImportResult result = readValue(mvcResult, RecordImportResult.class); +// assertEquals(importResult.getTotalCount(), result.getTotalCount()); +// assertEquals(importResult.getImportedCount(), result.getImportedCount()); +// assertThat(importResult.getErrors(), anyOf(nullValue(), empty())); +// @SuppressWarnings("unchecked") +// final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); +// verify(patientRecordServiceMock).importRecords(captor.capture()); +// assertEquals(records.size(), captor.getValue().size()); +// } + @Test - void importRecordsImportsSpecifiedRecordsAndReturnsImportResult() throws Exception { + void importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult() throws Exception { final List records = List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); final RecordImportResult importResult = new RecordImportResult(records.size()); importResult.setImportedCount(records.size()); + when(patientRecordServiceMock.importRecords(anyList())).thenReturn(importResult); + MockMultipartFile file = new MockMultipartFile("file", "records.json", + MediaType.MULTIPART_FORM_DATA_VALUE, toJson(records).getBytes()); + + String jsonContent = new String(file.getBytes()); + System.out.println("Received JSON content: " + jsonContent); + final MvcResult mvcResult = mockMvc.perform( - post("/records/import").content(toJson(records)).contentType(MediaType.APPLICATION_JSON)).andReturn(); + multipart("/records/import/json") + .file(file) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + ).andReturn(); + final RecordImportResult result = readValue(mvcResult, RecordImportResult.class); + + // Assertions assertEquals(importResult.getTotalCount(), result.getTotalCount()); assertEquals(importResult.getImportedCount(), result.getImportedCount()); assertThat(importResult.getErrors(), anyOf(nullValue(), empty())); + + @SuppressWarnings("unchecked") final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); - verify(patientRecordServiceMock).importRecords(captor.capture()); assertEquals(records.size(), captor.getValue().size()); } + @Test - void importRecordsImportsSpecifiedRecordsWithSpecifiedPhaseAndReturnsImportResult() throws Exception { + void importRecordsJsonImportsSpecifiedRecordsWithSpecifiedPhaseAndReturnsImportResult() throws Exception { final List records = List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); final RecordImportResult importResult = new RecordImportResult(records.size()); @@ -331,18 +365,18 @@ void importRecordsImportsSpecifiedRecordsWithSpecifiedPhaseAndReturnsImportResul final RecordPhase targetPhase = RecordPhase.values()[Generator.randomInt(0, RecordPhase.values().length)]; when(patientRecordServiceMock.importRecords(anyList(), any(RecordPhase.class))).thenReturn(importResult); - mockMvc.perform(post("/records/import").content(toJson(records)).contentType(MediaType.APPLICATION_JSON) + mockMvc.perform(post("/records/import/json").content(toJson(records)).contentType(MediaType.MULTIPART_FORM_DATA_VALUE) .param("phase", targetPhase.getIri())).andExpect(status().isOk()); verify(patientRecordServiceMock).importRecords(anyList(), eq(targetPhase)); } @Test - void importRecordsReturnsConflictWhenServiceThrowsRecordAuthorNotFound() throws Exception { + void importRecordsJsonReturnsConflictWhenServiceThrowsRecordAuthorNotFound() throws Exception { final List records = List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); when(patientRecordServiceMock.importRecords(anyList())).thenThrow(RecordAuthorNotFoundException.class); - mockMvc.perform(post("/records/import").content(toJson(records)).contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(post("/records/import/json").content(toJson(records)).contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) .andExpect(status().isConflict()); } From 640578792acd6711fa4f728bdde43d4959ea737b Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Sat, 20 Jul 2024 16:23:54 +0200 Subject: [PATCH 6/8] [kbss-cvut/record-manager-ui#184] Fix test importRecordsJsonImportsSpecifiedRecordsWithSpecifiedPhaseAndReturnsImportResult --- .../kbss/study/rest/PatientRecordController.java | 8 ++++++-- .../kbss/study/service/PatientRecordService.java | 1 + .../study/rest/PatientRecordControllerTest.java | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java index d2f7d58f..0a51aae2 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java @@ -51,13 +51,17 @@ public class PatientRecordController extends BaseController { private final ExcelRecordConverter excelRecordConverter; private final RestTemplate restTemplate; private final ConfigReader configReader; + private ObjectMapper objectMapper; - public PatientRecordController(PatientRecordService recordService, ApplicationEventPublisher eventPublisher, ExcelRecordConverter excelRecordConverter, RestTemplate restTemplate, ConfigReader configReader) { + public PatientRecordController(PatientRecordService recordService, ApplicationEventPublisher eventPublisher, + ExcelRecordConverter excelRecordConverter, RestTemplate restTemplate, + ConfigReader configReader, ObjectMapper objectMapper) { this.recordService = recordService; this.eventPublisher = eventPublisher; this.excelRecordConverter = excelRecordConverter; this.restTemplate = restTemplate; this.configReader = configReader; + this.objectMapper = objectMapper; } @PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isMemberOfInstitution(#institutionKey)") @@ -167,7 +171,7 @@ public RecordImportResult importRecordsJson(@RequestPart("file") MultipartFile f if(file.isEmpty()) throw new IllegalArgumentException("Cannot import records, missing input file"); try { - records = new ObjectMapper().readValue(file.getBytes(), new TypeReference>(){}); + records = objectMapper.readValue(file.getBytes(), new TypeReference>(){}); } catch (IOException e) { throw new RuntimeException("Failed to parse JSON content", e); } diff --git a/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java b/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java index 83eb8965..08c7a98f 100644 --- a/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java +++ b/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java @@ -72,6 +72,7 @@ public interface PatientRecordService extends BaseService { * current user is set as the record's author. * * @param records Records to import + * @param targetPhase Phase to be set to all imported records. * @return Instance representing the import result * @throws cz.cvut.kbss.study.exception.RecordAuthorNotFoundException Thrown when importing a record whose author * does not exist in this application instance's diff --git a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java index 7fca2a90..61a6537c 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java @@ -24,6 +24,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -65,6 +66,9 @@ public class PatientRecordControllerTest extends BaseControllerTestRunner { @Mock private ApplicationEventPublisher eventPublisherMock; + @Spy + private ObjectMapper objectMapper = Environment.getObjectMapper(); + @InjectMocks private PatientRecordController controller; @@ -365,8 +369,16 @@ void importRecordsJsonImportsSpecifiedRecordsWithSpecifiedPhaseAndReturnsImportR final RecordPhase targetPhase = RecordPhase.values()[Generator.randomInt(0, RecordPhase.values().length)]; when(patientRecordServiceMock.importRecords(anyList(), any(RecordPhase.class))).thenReturn(importResult); - mockMvc.perform(post("/records/import/json").content(toJson(records)).contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .param("phase", targetPhase.getIri())).andExpect(status().isOk()); + MockMultipartFile file = new MockMultipartFile("file", "records.json", + MediaType.MULTIPART_FORM_DATA_VALUE, toJson(records).getBytes()); + + mockMvc.perform( + multipart("/records/import/json") + .file(file) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .param("phase", targetPhase.getIri()) + ).andExpect(status().isOk()); + verify(patientRecordServiceMock).importRecords(anyList(), eq(targetPhase)); } From f8ddc1064680f0d6f5dc7f9ebb0f7ba9b2aafa7e Mon Sep 17 00:00:00 2001 From: Daniil Palagin Date: Sat, 20 Jul 2024 18:22:56 +0200 Subject: [PATCH 7/8] [kbss-cvut/record-manager-ui#184] Fixed importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult importRecordsJsonReturnsConflictWhenServiceThrowsRecordAuthorNotFound --- .../rest/PatientRecordControllerTest.java | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java index 61a6537c..fe532cf9 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java @@ -305,25 +305,6 @@ void exportRecordsExportsRecordsForProvidedInstitutionForSpecifiedPeriod() throw Pageable.unpaged()); } -// @Test -// void importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult() throws Exception { -// final List records = -// List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); -// final RecordImportResult importResult = new RecordImportResult(records.size()); -// importResult.setImportedCount(records.size()); -// when(patientRecordServiceMock.importRecords(anyList())).thenReturn(importResult); -// -// final MvcResult mvcResult = mockMvc.perform( -// post("/records/import/json").content(toJson(records)).contentType(MediaType.MULTIPART_FORM_DATA_VALUE)).andReturn(); -// final RecordImportResult result = readValue(mvcResult, RecordImportResult.class); -// assertEquals(importResult.getTotalCount(), result.getTotalCount()); -// assertEquals(importResult.getImportedCount(), result.getImportedCount()); -// assertThat(importResult.getErrors(), anyOf(nullValue(), empty())); -// @SuppressWarnings("unchecked") -// final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); -// verify(patientRecordServiceMock).importRecords(captor.capture()); -// assertEquals(records.size(), captor.getValue().size()); -// } @Test void importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult() throws Exception { @@ -337,9 +318,6 @@ void importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult() throws Exc MockMultipartFile file = new MockMultipartFile("file", "records.json", MediaType.MULTIPART_FORM_DATA_VALUE, toJson(records).getBytes()); - String jsonContent = new String(file.getBytes()); - System.out.println("Received JSON content: " + jsonContent); - final MvcResult mvcResult = mockMvc.perform( multipart("/records/import/json") .file(file) @@ -347,15 +325,13 @@ void importRecordsJsonImportsSpecifiedRecordsAndReturnsImportResult() throws Exc ).andReturn(); final RecordImportResult result = readValue(mvcResult, RecordImportResult.class); - - // Assertions assertEquals(importResult.getTotalCount(), result.getTotalCount()); assertEquals(importResult.getImportedCount(), result.getImportedCount()); assertThat(importResult.getErrors(), anyOf(nullValue(), empty())); - @SuppressWarnings("unchecked") final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(patientRecordServiceMock).importRecords(captor.capture()); assertEquals(records.size(), captor.getValue().size()); } @@ -388,8 +364,14 @@ void importRecordsJsonReturnsConflictWhenServiceThrowsRecordAuthorNotFound() thr List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); when(patientRecordServiceMock.importRecords(anyList())).thenThrow(RecordAuthorNotFoundException.class); - mockMvc.perform(post("/records/import/json").content(toJson(records)).contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) - .andExpect(status().isConflict()); + MockMultipartFile file = new MockMultipartFile("file", "records.json", + MediaType.MULTIPART_FORM_DATA_VALUE, toJson(records).getBytes()); + + mockMvc.perform( + multipart("/records/import/json") + .file(file) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + ).andExpect(status().isConflict()); } @Test From 6fff1db07dd7108a19ec46fd950a67d96768e88a Mon Sep 17 00:00:00 2001 From: Miroslav Blasko Date: Sun, 21 Jul 2024 07:45:11 +0200 Subject: [PATCH 8/8] [kbss-cvut/record-manager-ui#184] Implement detached import through TSV endpoint I.e. import that is fully implemented by external service. In this case database is modified by separate service. --- .../cvut/kbss/study/config/ServiceConfig.java | 9 ++- .../study/rest/PatientRecordController.java | 55 +++++++++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/config/ServiceConfig.java b/src/main/java/cz/cvut/kbss/study/config/ServiceConfig.java index d5dec886..c8d96322 100644 --- a/src/main/java/cz/cvut/kbss/study/config/ServiceConfig.java +++ b/src/main/java/cz/cvut/kbss/study/config/ServiceConfig.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; @@ -40,8 +41,12 @@ public RestTemplate restTemplate(ObjectMapper objectMapper) { final MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); jacksonConverter.setObjectMapper(objectMapper); final StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8); - restTemplate.setMessageConverters( - Arrays.asList(stringConverter, jacksonConverter, new ResourceHttpMessageConverter())); + restTemplate.setMessageConverters(Arrays.asList( + stringConverter, + jacksonConverter, + new ResourceHttpMessageConverter(), + new FormHttpMessageConverter() + )); return restTemplate; } } diff --git a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java index 0a51aae2..64e148a8 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java @@ -179,8 +179,9 @@ public RecordImportResult importRecordsJson(@RequestPart("file") MultipartFile f } @PostMapping(value = "/import/excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public RecordImportResult importRecordsExcel(@RequestPart("file") MultipartFile file, - @RequestParam(name = "phase", required = false) String phase) { + public RecordImportResult importRecordsExcel( + @RequestPart("file") MultipartFile file, + @RequestParam(name = "phase", required = false) String phase) { List records; @@ -196,12 +197,16 @@ public RecordImportResult importRecordsExcel(@RequestPart("file") MultipartFile headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA); MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("file", file.getResource()); + body.add("files", file.getResource()); + + String request = UriComponentsBuilder.fromHttpUrl(excelImportServiceUrl) + .queryParam("datasetResource", "@%s".formatted(file.getOriginalFilename())) + .toUriString(); HttpEntity> requestEntity = new HttpEntity<>(body, headers); ResponseEntity> responseEntity = restTemplate.exchange( - URI.create(excelImportServiceUrl), + URI.create(request), HttpMethod.POST, requestEntity, new ParameterizedTypeReference>() {} @@ -210,6 +215,48 @@ public RecordImportResult importRecordsExcel(@RequestPart("file") MultipartFile return importRecords(records, phase); } + @PostMapping(value = "/import/tsv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public RecordImportResult importRecordsTsv( + @RequestPart("file") MultipartFile file, + @RequestParam(name = "phase", required = false) String phase) { + + List records; + + if(file.isEmpty()) + throw new IllegalArgumentException("Cannot import records, missing input file"); + + String excelImportServiceUrl = configReader.getConfig(ConfigParam.EXCEL_IMPORT_SERVICE_URL); + + if (excelImportServiceUrl == null) + throw new IllegalArgumentException("Cannot import TSV, excelImportServiceUrl is not configured"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("files", file.getResource()); + + String request = UriComponentsBuilder.fromHttpUrl(excelImportServiceUrl) + .queryParam("datasetResource", "@%s".formatted(file.getOriginalFilename())) + .toUriString(); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + ResponseEntity responseEntity = restTemplate.postForEntity( + URI.create(request), + requestEntity, + byte[].class + ); + + LOG.info("Import finished with status {}", responseEntity.getStatusCode()); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + byte[] responseBody = responseEntity.getBody(); + LOG.debug("Response body length is {}", responseBody.length); + } + + return new RecordImportResult(); + } + public RecordImportResult importRecords(List records, String phase) { final RecordImportResult importResult; if (phase != null) {