From 65315ab11e8bb26ab04f6c8dd5a1214d9f1aa800 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Wed, 4 Oct 2023 16:52:47 +0200 Subject: [PATCH 01/32] MS Studies + ShUp: extension to auto-test study creation --- .../shanoir/ng/study/controler/StudyApi.java | 1 - .../study/controler/StudyApiController.java | 15 +++++ .../ng/study/service/StudyServiceImpl.java | 15 ++--- .../shanoir/uploader/model/rest/Study.java | 10 ++++ .../rest/ShanoirUploaderServiceClient.java | 58 ++++++++++++++++++- .../src/main/resources/endpoint.properties | 3 +- .../test/importer/ZipFileImportTest.java | 23 ++++++-- 7 files changed, 110 insertions(+), 15 deletions(-) diff --git a/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApi.java b/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApi.java index c1e787d8e0..aaeb1ba1a3 100644 --- a/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApi.java +++ b/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApi.java @@ -30,7 +30,6 @@ import org.shanoir.ng.study.dua.DataUserAgreement; import org.shanoir.ng.study.model.Study; import org.shanoir.ng.study.model.StudyUser; -import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; diff --git a/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApiController.java b/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApiController.java index 07e69be592..4b0d8ed245 100644 --- a/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApiController.java +++ b/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/controler/StudyApiController.java @@ -22,6 +22,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -197,6 +198,7 @@ public ResponseEntity saveNewStudy(@RequestBody final Study study, fin Study createdStudy; try { + addCurrentUserAsStudyUserIfEmptyStudyUsers(study); createdStudy = studyService.create(study); eventService.publishEvent(new ShanoirEvent(ShanoirEventType.CREATE_STUDY_EVENT, createdStudy.getId().toString(), KeycloakUtil.getTokenUserId(), "", ShanoirEvent.SUCCESS)); @@ -207,6 +209,19 @@ public ResponseEntity saveNewStudy(@RequestBody final Study study, fin return new ResponseEntity<>(studyMapper.studyToStudyDTO(createdStudy), HttpStatus.OK); } + private void addCurrentUserAsStudyUserIfEmptyStudyUsers(final Study study) { + if (study.getStudyUserList() == null) { + List studyUserList = new ArrayList(); + StudyUser studyUser = new StudyUser(); + studyUser.setStudy(study); + studyUser.setUserId(KeycloakUtil.getTokenUserId()); + studyUser.setUserName(KeycloakUtil.getTokenUserName()); + studyUser.setStudyUserRights(Arrays.asList(StudyUserRight.CAN_SEE_ALL, StudyUserRight.CAN_DOWNLOAD, StudyUserRight.CAN_IMPORT, StudyUserRight.CAN_ADMINISTRATE)); + studyUserList.add(studyUser); + study.setStudyUserList(studyUserList); + } + } + @Override public ResponseEntity getDetailedStorageVolume(@PathVariable("studyId") final Long studyId) throws RestServiceException { StudyStorageVolumeDTO dto = studyService.getDetailedStorageVolume(studyId); diff --git a/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/service/StudyServiceImpl.java b/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/service/StudyServiceImpl.java index 585ab1bd20..8b2f8c311a 100644 --- a/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/service/StudyServiceImpl.java +++ b/shanoir-ng-studies/src/main/java/org/shanoir/ng/study/service/StudyServiceImpl.java @@ -113,7 +113,6 @@ public class StudyServiceImpl implements StudyService { @Autowired private SubjectStudyRepository subjectStudyRepository; - @Override public void deleteById(final Long id) throws EntityNotFoundException { final Study study = studyRepository.findById(id).orElse(null); @@ -149,8 +148,10 @@ public Study create(final Study study) throws MicroServiceCommunicationException } } - for (SubjectStudy subjectStudy : study.getSubjectStudyList()) { - subjectStudy.setStudy(study); + if (study.getSubjectStudyList() != null) { + for (SubjectStudy subjectStudy : study.getSubjectStudyList()) { + subjectStudy.setStudy(study); + } } if (study.getTags() != null) { @@ -180,15 +181,16 @@ public Study create(final Study study) throws MicroServiceCommunicationException } } - List subjectStudyListSave = new ArrayList(study.getSubjectStudyList()); + List subjectStudyListSave = null; + if (study.getSubjectStudyList() != null) { + subjectStudyListSave = new ArrayList(study.getSubjectStudyList()); + } Map> subjectStudyTagSave = new HashMap<>(); study.setSubjectStudyList(null); Study studyDb = studyRepository.save(study); - //studyDb.setSubjectStudyList(new ArrayList()); if (subjectStudyListSave != null) { updateTags(subjectStudyListSave, studyDb.getTags()); - //ListDependencyUpdate.updateWith(studyDb.getSubjectStudyList(), subjectStudyListSave); studyDb.setSubjectStudyList(new ArrayList<>()); for (SubjectStudy subjectStudy : subjectStudyListSave) { SubjectStudy newSubjectStudy = new SubjectStudy(); @@ -198,7 +200,6 @@ public Study create(final Study study) throws MicroServiceCommunicationException newSubjectStudy.setSubjectType(subjectStudy.getSubjectType()); newSubjectStudy.setStudy(studyDb); subjectStudyTagSave.put(subjectStudy.getSubject().getId(), subjectStudy.getSubjectStudyTags()); - //newSubjectStudy.setSubjectStudyTags(subjectStudy.getSubjectStudyTags()); studyDb.getSubjectStudyList().add(newSubjectStudy); } studyDb = studyRepository.save(studyDb); diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java index 4ccc002abd..7312a57b6b 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java @@ -13,6 +13,8 @@ public class Study implements Comparable { private String name; + private Integer studyStatus; + private List studyCards; @JsonProperty("studyCenterList") @@ -36,6 +38,14 @@ public void setName(String name) { this.name = name; } + public Integer getStudyStatus() { + return studyStatus; + } + + public void setStudyStatus(Integer studyStatus) { + this.studyStatus = studyStatus; + } + public List getStudyCards() { return studyCards; } diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/service/rest/ShanoirUploaderServiceClient.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/service/rest/ShanoirUploaderServiceClient.java index 152f524794..c4f8ef2987 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/service/rest/ShanoirUploaderServiceClient.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/service/rest/ShanoirUploaderServiceClient.java @@ -51,8 +51,12 @@ public class ShanoirUploaderServiceClient { private static Logger logger = Logger.getLogger(ShanoirUploaderServiceClient.class); private static final String SHANOIR_SERVER_URL = "shanoir.server.url"; + + private static final String SERVICE_STUDIES_CREATE = "service.studies.create"; private static final String SERVICE_STUDIES_NAMES_CENTERS = "service.studies.names.centers"; + + private static final String SERVICE_STUDYCARDS_CREATE = "service.studycards.create"; private static final String SERVICE_STUDYCARDS_FIND_BY_STUDY_IDS = "service.studycards.find.by.study.ids"; @@ -86,8 +90,12 @@ public class ShanoirUploaderServiceClient { private String serverURL; + private String serviceURLStudiesCreate; + private String serviceURLStudiesNamesAndCenters; + private String serviceURLStudyCardsCreate; + private String serviceURLStudyCardsByStudyIds; private String serviceURLStudyCardsApplyOnStudy; @@ -96,14 +104,14 @@ public class ShanoirUploaderServiceClient { private String serviceURLAcquisitionEquipments; + private String serviceURLSubjectsCreate; + private String serviceURLSubjectsFindByIdentifier; private String serviceURLDatasets; private String serviceURLDatasetsDicomWebStudies; - private String serviceURLSubjectsCreate; - private String serviceURLExaminationsCreate; private String serviceURLImporterCreateTempDir; @@ -133,8 +141,12 @@ public ShanoirUploaderServiceClient() { this.serverURL = ShUpConfig.profileProperties.getProperty(SHANOIR_SERVER_URL); + this.serviceURLStudiesCreate = this.serverURL + + ShUpConfig.endpointProperties.getProperty(SERVICE_STUDIES_CREATE); this.serviceURLStudiesNamesAndCenters = this.serverURL + ShUpConfig.endpointProperties.getProperty(SERVICE_STUDIES_NAMES_CENTERS); + this.serviceURLStudyCardsCreate = this.serverURL + + ShUpConfig.endpointProperties.getProperty(SERVICE_STUDYCARDS_CREATE); this.serviceURLStudyCardsByStudyIds = this.serverURL + ShUpConfig.endpointProperties.getProperty(SERVICE_STUDYCARDS_FIND_BY_STUDY_IDS); this.serviceURLStudyCardsApplyOnStudy = this.serverURL @@ -521,6 +533,48 @@ public CloseableHttpResponse downloadDatasetsByStudyId(Long studyId, String form return null; } + public Study createStudy(final Study study) { + try { + String json = Util.objectWriter.writeValueAsString(study); + try (CloseableHttpResponse response = httpService.post(this.serviceURLStudiesCreate, json, false)) { + int code = response.getCode(); + if (code == HttpStatus.SC_OK) { + Study studyCreated = Util.getMappedObject(response, Study.class); + return studyCreated; + } else { + logger.error("Error in createStudy: with study " + study.getName() + + " (status code: " + code + ", message: " + apiResponseMessages.getOrDefault(code, "unknown status code") + ")"); + } + } + } catch (JsonProcessingException e) { + logger.error(e.getMessage(), e); + } catch (IOException ioE) { + logger.error(ioE.getMessage(), ioE); + } + return null; + } + + public Center createStudyCard(final StudyCard studyCard) { + try { + String json = Util.objectWriter.writeValueAsString(studyCard); + try (CloseableHttpResponse response = httpService.post(this.serviceURLStudyCardsCreate, json, false)) { + int code = response.getCode(); + if (code == HttpStatus.SC_OK) { + Center centerCreated = Util.getMappedObject(response, Center.class); + return centerCreated; + } else { + logger.error("Error in createStudy: with study " + studyCard.getName() + + " (status code: " + code + ", message: " + apiResponseMessages.getOrDefault(code, "unknown status code") + ")"); + } + } + } catch (JsonProcessingException e) { + logger.error(e.getMessage(), e); + } catch (IOException ioE) { + logger.error(ioE.getMessage(), ioE); + } + return null; + } + public Center createCenter(final Center center) { try { String json = Util.objectWriter.writeValueAsString(center); diff --git a/shanoir-uploader/src/main/resources/endpoint.properties b/shanoir-uploader/src/main/resources/endpoint.properties index ef1959ded2..ebcd3a1643 100644 --- a/shanoir-uploader/src/main/resources/endpoint.properties +++ b/shanoir-uploader/src/main/resources/endpoint.properties @@ -1,5 +1,6 @@ -service.studies=/shanoir-ng/studies/studies/ +service.studies.create=/shanoir-ng/studies/studies service.studies.names.centers=/shanoir-ng/studies/studies/namesAndCenters +service.studycards.create=/shanoir-ng/datasets/studycards service.studycards.find.by.study.ids=/shanoir-ng/datasets/studycards/search service.studycards.apply.on.study=/shanoir-ng/datasets/studycards/apply_on_study/ service.centers.create=/shanoir-ng/studies/centers diff --git a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java index 846922acbc..9a0decf461 100644 --- a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java +++ b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java @@ -13,6 +13,7 @@ import org.shanoir.ng.importer.model.Patient; import org.shanoir.ng.importer.model.Serie; import org.shanoir.ng.importer.model.Study; +import org.shanoir.uploader.model.rest.Center; import org.shanoir.uploader.model.rest.Examination; import org.shanoir.uploader.model.rest.HemisphericDominance; import org.shanoir.uploader.model.rest.IdName; @@ -30,13 +31,13 @@ public class ZipFileImportTest extends AbstractTest { private static Logger logger = Logger.getLogger(ZipFileImportTest.class); + private static final String ACR_PHANTOM_T1_ZIP = "acr_phantom_t1.zip"; + @Test public void importDicomZipTest() throws Exception { - org.shanoir.uploader.model.rest.Study study = new org.shanoir.uploader.model.rest.Study(); - study.setId(Long.valueOf(3)); - study.setName("DemoStudy"); + org.shanoir.uploader.model.rest.Study study = createStudyAndStudyCard(); for (int i = 0; i < 1; i++) { - ImportJob importJob = step1UploadDicom("acr_phantom_t1.zip"); + ImportJob importJob = step1UploadDicom(ACR_PHANTOM_T1_ZIP); if (!importJob.getPatients().isEmpty()) { selectAllSeriesForImport(importJob); Subject subject = step2CreateSubject(importJob, study); @@ -45,6 +46,20 @@ public void importDicomZipTest() throws Exception { } } } + + private org.shanoir.uploader.model.rest.Study createStudyAndStudyCard() { + org.shanoir.uploader.model.rest.Study study = new org.shanoir.uploader.model.rest.Study(); + final String randomStudyName = "Study-Name-" + UUID.randomUUID().toString(); + study.setName(randomStudyName); + study.setStudyStatus(1); // 1, in progress + List
centers = new ArrayList
(); + final Center center = new Center(); + final String randomCenterName = "Center-Name" + UUID.randomUUID().toString(); + center.setName(randomCenterName); + study.setCenters(centers); + shUpClient.createStudy(study); + return study; + } private void createSubjectStudy(org.shanoir.uploader.model.rest.Study study, Subject subject) { SubjectStudy subjectStudy = new SubjectStudy(); From c481da4e402e5128713f1a2d8a6073e7eaca480e Mon Sep 17 00:00:00 2001 From: michaelkain Date: Thu, 5 Oct 2023 13:54:57 +0200 Subject: [PATCH 02/32] sh-up: center creation WORKS --- .../org/shanoir/uploader/test/importer/ZipFileImportTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java index 9a0decf461..c1a72af7e5 100644 --- a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java +++ b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java @@ -54,8 +54,9 @@ private org.shanoir.uploader.model.rest.Study createStudyAndStudyCard() { study.setStudyStatus(1); // 1, in progress List
centers = new ArrayList
(); final Center center = new Center(); - final String randomCenterName = "Center-Name" + UUID.randomUUID().toString(); + final String randomCenterName = "Center-Name-" + UUID.randomUUID().toString(); center.setName(randomCenterName); + centers.add(center); study.setCenters(centers); shUpClient.createStudy(study); return study; From ebad269c3525bb53719f54ee2a64dca894864d72 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Thu, 5 Oct 2023 15:01:45 +0200 Subject: [PATCH 03/32] ShUp: final changes to study-create WORKS --- .../shanoir/uploader/model/rest/Study.java | 12 ++++----- .../uploader/model/rest/StudyCenter.java | 25 +++++++++++++++++++ .../test/importer/ZipFileImportTest.java | 12 +++++---- 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/StudyCenter.java diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java index 7312a57b6b..74aa422c9c 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java @@ -17,8 +17,7 @@ public class Study implements Comparable { private List studyCards; - @JsonProperty("studyCenterList") - private List
centers; + private List studyCenterList; private Boolean compatible; @@ -54,13 +53,12 @@ public void setStudyCards(List studyCards) { this.studyCards = studyCards; } - public List
getCenters() { - return centers; + public List getStudyCenterList() { + return studyCenterList; } - public void setCenters(List
centers) { - this.centers = centers; - Collections.sort(this.centers); + public void setStudyCenterList(List studyCenterList) { + this.studyCenterList = studyCenterList; } public Boolean getCompatible() { diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/StudyCenter.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/StudyCenter.java new file mode 100644 index 0000000000..ab2f6348b7 --- /dev/null +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/StudyCenter.java @@ -0,0 +1,25 @@ +package org.shanoir.uploader.model.rest; + +public class StudyCenter { + + private Center center; + + private Study study; + + public Center getCenter() { + return center; + } + + public void setCenter(Center center) { + this.center = center; + } + + public Study getStudy() { + return study; + } + + public void setStudy(Study study) { + this.study = study; + } + +} diff --git a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java index c1a72af7e5..961f9f206d 100644 --- a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java +++ b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java @@ -19,6 +19,7 @@ import org.shanoir.uploader.model.rest.IdName; import org.shanoir.uploader.model.rest.ImagedObjectCategory; import org.shanoir.uploader.model.rest.Sex; +import org.shanoir.uploader.model.rest.StudyCenter; import org.shanoir.uploader.model.rest.Subject; import org.shanoir.uploader.model.rest.SubjectStudy; import org.shanoir.uploader.model.rest.SubjectType; @@ -52,12 +53,13 @@ private org.shanoir.uploader.model.rest.Study createStudyAndStudyCard() { final String randomStudyName = "Study-Name-" + UUID.randomUUID().toString(); study.setName(randomStudyName); study.setStudyStatus(1); // 1, in progress - List
centers = new ArrayList
(); + List studyCenterList = new ArrayList(); + final StudyCenter studyCenter = new StudyCenter(); final Center center = new Center(); - final String randomCenterName = "Center-Name-" + UUID.randomUUID().toString(); - center.setName(randomCenterName); - centers.add(center); - study.setCenters(centers); + center.setId(Long.valueOf(1)); + studyCenter.setCenter(center); + studyCenterList.add(studyCenter); + study.setStudyCenterList(studyCenterList); shUpClient.createStudy(study); return study; } From ee8dce78ad9c560cfdba2fa2b6d18fbb488e8abe Mon Sep 17 00:00:00 2001 From: michaelkain Date: Thu, 5 Oct 2023 16:57:50 +0200 Subject: [PATCH 04/32] ShUp: create study: fix for StudyStatus --- .../main/java/org/shanoir/uploader/model/rest/Study.java | 6 +++--- .../shanoir/uploader/test/importer/ZipFileImportTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java index 74aa422c9c..f877b8d3c4 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/model/rest/Study.java @@ -13,7 +13,7 @@ public class Study implements Comparable { private String name; - private Integer studyStatus; + private String studyStatus; private List studyCards; @@ -37,11 +37,11 @@ public void setName(String name) { this.name = name; } - public Integer getStudyStatus() { + public String getStudyStatus() { return studyStatus; } - public void setStudyStatus(Integer studyStatus) { + public void setStudyStatus(String studyStatus) { this.studyStatus = studyStatus; } diff --git a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java index 961f9f206d..57e5f38251 100644 --- a/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java +++ b/shanoir-uploader/src/test/java/org/shanoir/uploader/test/importer/ZipFileImportTest.java @@ -52,7 +52,7 @@ private org.shanoir.uploader.model.rest.Study createStudyAndStudyCard() { org.shanoir.uploader.model.rest.Study study = new org.shanoir.uploader.model.rest.Study(); final String randomStudyName = "Study-Name-" + UUID.randomUUID().toString(); study.setName(randomStudyName); - study.setStudyStatus(1); // 1, in progress + study.setStudyStatus("IN_PROGRESS"); List studyCenterList = new ArrayList(); final StudyCenter studyCenter = new StudyCenter(); final Center center = new Center(); From d2e8aa9734fa379548f99888f4c772d6775a6113 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Thu, 12 Oct 2023 14:53:15 +0200 Subject: [PATCH 05/32] ShUp: feature: adds default profile config into ShUp, clean up old values in prop files --- ...gesCreatorAndDicomFileAnalyzerService.java | 3 +-- .../java/org/shanoir/uploader/ShUpConfig.java | 2 ++ .../action/init/InitialStartupState.java | 22 +++++++++++++----- .../action/init/ProxyConfigurationState.java | 2 +- .../init/SelectProfileConfigurationState.java | 23 +++++++++++++++++++ .../SelectProfilePanelActionListener.java | 11 +++++---- .../uploader/dicom/DicomServerClient.java | 1 + .../dicom/retrieve/DcmRcvManager.java | 2 -- .../profile.properties | 11 +-------- .../profile.Neurinfo/profile.properties | 11 +-------- .../profile.OFSEP-Qualif/profile.properties | 11 +-------- .../profile.OFSEP/profile.properties | 11 +-------- .../resources/profile.dev/profile.properties | 11 +-------- 13 files changed, 55 insertions(+), 66 deletions(-) create mode 100644 shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java index 14cda143d7..3d25e17822 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java @@ -228,7 +228,6 @@ private void processOneDicomFileForAllInstances(File dicomFile, List imag if (DicomSerieAndInstanceAnalyzer.checkInstanceIsIgnored(attributes)) { // do nothing here as instances list will be emptied after split between images and non-images } else { - // divide here between non-images and images, non-images at first Image image = new Image(); /** * Attention: the path of each image is always relative: either to the temporary folder created @@ -242,7 +241,7 @@ private void processOneDicomFileForAllInstances(File dicomFile, List imag } catch (IOException iOE) { throw iOE; } catch (Exception e) { - LOG.error("Error while processing DICOM file: " + dicomFile.getAbsolutePath()); + LOG.error("Error while processing DICOM file, one for entire serie: " + dicomFile.getAbsolutePath()); throw e; } } diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/ShUpConfig.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/ShUpConfig.java index e3860623da..e5d1379fbd 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/ShUpConfig.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/ShUpConfig.java @@ -70,6 +70,8 @@ public class ShUpConfig { public static final String RANDOM_SEED = "random.seed"; + public static final String PROFILE = "profile"; + /** * Static variables */ diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/InitialStartupState.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/InitialStartupState.java index 772e3644e3..c0cab11ef6 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/InitialStartupState.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/InitialStartupState.java @@ -49,7 +49,9 @@ public class InitialStartupState implements State { private static final String SU_V6_0_3 = ".su_v6.0.3"; private static final String SU_V6_0_4 = ".su_v6.0.4"; - + + private static final String SU_V7_0_1 = ".su_v7.0.1"; + public void load(StartupStateContext context) throws Exception { initShanoirUploaderFolder(); initLogging(); @@ -60,8 +62,8 @@ public void load(StartupStateContext context) throws Exception { logger.info(System.getProperty("java.vendor.url")); logger.info(System.getProperty("java.version")); InetAddress inetAddress = InetAddress.getLocalHost(); - logger.info("IP Address:- " + inetAddress.getHostAddress()); - logger.info("Host Name:- " + inetAddress.getHostName()); + logger.info("IP Address: " + inetAddress.getHostAddress()); + logger.info("Host Name: " + inetAddress.getHostName()); // Disable http request to check for quartz upload System.setProperty("org.quartz.scheduler.skipUpdateCheck", "true"); System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""); @@ -70,6 +72,7 @@ public void load(StartupStateContext context) throws Exception { initLanguage(); copyPseudonymus(); initProfiles(); + initProfile(); initStartupDialog(context); context.setState(new ProxyConfigurationState()); context.nextState(); @@ -78,7 +81,8 @@ public void load(StartupStateContext context) throws Exception { private void doMigration() throws IOException { // as properties, that exist already are not replaced/changed, start with the last version before, // as considered as more important - // overwrite with properties from ShanoirUploader v6.0.4 or v6.0.3, if existing + // overwrite with properties from ShanoirUploader v7.0.1, v6.0.4 or v6.0.3, if existing + migrateFromVersion(SU_V7_0_1); migrateFromVersion(SU_V6_0_4); migrateFromVersion(SU_V6_0_3); // migrate properties from ShanoirUploader v5.2 @@ -91,11 +95,11 @@ private void migrateFromVersion(String version) throws IOException { final File shanoirUploaderFolderForVersion = new File(shanoirUploaderFolderPathForVersion); boolean shanoirUploaderFolderExistsForVersion = shanoirUploaderFolderForVersion.exists(); if (shanoirUploaderFolderExistsForVersion) { - logger.info("Start migrating properties from version " + version + " (.su == v5.2) of ShUp."); + logger.info("Start migrating properties from version " + version + " of ShUp."); copyPropertiesFile(shanoirUploaderFolderForVersion, ShUpConfig.shanoirUploaderFolder, ShUpConfig.LANGUAGE_PROPERTIES); copyPropertiesFile(shanoirUploaderFolderForVersion, ShUpConfig.shanoirUploaderFolder, ShUpConfig.PROXY_PROPERTIES); copyPropertiesFile(shanoirUploaderFolderForVersion, ShUpConfig.shanoirUploaderFolder, ShUpConfig.DICOM_SERVER_PROPERTIES); - logger.info("Finished migrating properties from version " + version + " (.su == v5.2) of ShUp: language, proxy, dicom_server."); + logger.info("Finished migrating properties from version " + version + " of ShUp: language, proxy, dicom_server."); } } @@ -297,4 +301,10 @@ private void initLanguage() { } } + private void initProfile() throws FileNotFoundException, IOException { + String profile = ShUpConfig.basicProperties.getProperty(ShUpConfig.PROFILE); + if (profile != null && !profile.isEmpty()) { + ShUpConfig.profileSelected = profile; + } + } } diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/ProxyConfigurationState.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/ProxyConfigurationState.java index 6a9266d888..1bd5a2e6bc 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/ProxyConfigurationState.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/ProxyConfigurationState.java @@ -37,7 +37,7 @@ public void load(StartupStateContext context) { switch (httpResponseCode){ case 200 : context.getShUpStartupDialog().updateStartupText("\n" + ShUpConfig.resourceBundle.getString("shanoir.uploader.startup.test.proxy.success")); - context.setState(new SelectProfileManualConfigurationState()); + context.setState(new SelectProfileConfigurationState()); context.nextState(); break; default: diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java new file mode 100644 index 0000000000..72cac6cc12 --- /dev/null +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java @@ -0,0 +1,23 @@ +package org.shanoir.uploader.action.init; + + +import org.apache.log4j.Logger; +import org.shanoir.uploader.ShUpConfig; + +public class SelectProfileConfigurationState implements State { + + private static Logger logger = Logger.getLogger(SelectProfileConfigurationState.class); + + public void load(StartupStateContext context) { + if(ShUpConfig.profileSelected == null) { + context.setState(new SelectProfileManualConfigurationState()); + context.nextState(); + } else { + logger.info("Profile found in basic.properties. Used as default: " + ShUpConfig.profileSelected); + context.getShUpStartupDialog().updateStartupText("\nProfile: " + ShUpConfig.profileSelected); + context.setState(new AuthenticationConfigurationState()); + context.nextState(); + } + } + +} diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfilePanelActionListener.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfilePanelActionListener.java index 12deea5f8f..6d410c95c4 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfilePanelActionListener.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfilePanelActionListener.java @@ -30,14 +30,16 @@ public SelectProfilePanelActionListener(SelectProfileConfigurationPanel selectPr public void actionPerformed(ActionEvent e) { String selectedProfile = (String) selectProfilePanel.selectProfileCB.getSelectedItem(); ShUpConfig.profileSelected = selectedProfile; + configureSelectedProfile(selectedProfile); + sSC.nextState(); + } + + public void configureSelectedProfile(String selectedProfile) { String filePath = File.separator + ShUpConfig.PROFILE_DIR + selectedProfile; ShUpConfig.profileDirectory = new File(ShUpConfig.shanoirUploaderFolder, filePath); logger.info("Profile directory set to: " + ShUpConfig.profileDirectory.getAbsolutePath()); File profilePropertiesFile = new File(ShUpConfig.profileDirectory, ShUpConfig.PROFILE_PROPERTIES); loadPropertiesFromFile(profilePropertiesFile, ShUpConfig.profileProperties); - - ShUpConfig.encryption.decryptIfEncryptedString(profilePropertiesFile, - ShUpConfig.profileProperties, "shanoir.server.user.password"); logger.info("Profile " + selectedProfile + " successfully initialized."); File keycloakJson = new File(ShUpConfig.profileDirectory, ShUpConfig.KEYCLOAK_JSON); @@ -47,7 +49,7 @@ public void actionPerformed(ActionEvent e) { } else { logger.error("Error: missing keycloak.json! Connection with sh-ng will not work."); return; - } + } // check if pseudonymus has been copied in case of true if (Boolean.parseBoolean(ShUpConfig.profileProperties.getProperty(ShUpConfig.MODE_PSEUDONYMUS))) { @@ -72,7 +74,6 @@ public void actionPerformed(ActionEvent e) { return; } } - sSC.nextState(); } private void loadPropertiesFromFile(final File propertiesFile, final Properties properties) { diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java index a38f4bb1a7..12b25ef855 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java @@ -42,6 +42,7 @@ public class DicomServerClient implements IDicomServerClient { private File workFolder; public DicomServerClient(final Properties dicomServerProperties, final File workFolder) { + logger.info("New DicomServerClient created with properties: " + dicomServerProperties.toString()); config.initWithPropertiesFile(dicomServerProperties); this.workFolder = workFolder; // Initialize connection configuration parameters here: to be used for all queries diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java index 4c442fc7c9..1dadc8ff2a 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java @@ -12,8 +12,6 @@ /** * The DcmRcvHelper handles the download of DICOM files. - * It's a local service / server which is started, when - * the ShanoirUploader is started. * * @author mkain * diff --git a/shanoir-uploader/src/main/resources/profile.Neurinfo-Qualif/profile.properties b/shanoir-uploader/src/main/resources/profile.Neurinfo-Qualif/profile.properties index 5d3c73d84a..f22c46a13a 100644 --- a/shanoir-uploader/src/main/resources/profile.Neurinfo-Qualif/profile.properties +++ b/shanoir-uploader/src/main/resources/profile.Neurinfo-Qualif/profile.properties @@ -14,16 +14,7 @@ mode.subject.study.identifier=true anonymization.profile=Profile Neurinfo -########################################################## -# Server properties for accessing to Shanoir-old server -########################################################## -shanoir.server.user.name= -shanoir.server.user.password= -shanoir.server.uploader.service.url= -shanoir.server.uploader.service.qname.local.part= -shanoir.server.uploader.service.qname.namespace.uri= - ########################################################## # Server properties for accessing to Shanoir-NG server ########################################################## -shanoir.server.url=https\://shanoir-qualif.irisa.fr +shanoir.server.url=https\://shanoir-qualif.irisa.fr \ No newline at end of file diff --git a/shanoir-uploader/src/main/resources/profile.Neurinfo/profile.properties b/shanoir-uploader/src/main/resources/profile.Neurinfo/profile.properties index bd3aae3d7c..e39fd64dc6 100644 --- a/shanoir-uploader/src/main/resources/profile.Neurinfo/profile.properties +++ b/shanoir-uploader/src/main/resources/profile.Neurinfo/profile.properties @@ -14,16 +14,7 @@ mode.subject.study.identifier=true anonymization.profile=Profile Neurinfo -########################################################## -# Server properties for accessing to Shanoir-old server -########################################################## -shanoir.server.user.name= -shanoir.server.user.password= -shanoir.server.uploader.service.url= -shanoir.server.uploader.service.qname.local.part= -shanoir.server.uploader.service.qname.namespace.uri= - ########################################################## # Server properties for accessing to Shanoir-NG server ########################################################## -shanoir.server.url=https\://shanoir.irisa.fr +shanoir.server.url=https\://shanoir.irisa.fr \ No newline at end of file diff --git a/shanoir-uploader/src/main/resources/profile.OFSEP-Qualif/profile.properties b/shanoir-uploader/src/main/resources/profile.OFSEP-Qualif/profile.properties index a4f3217193..995774c032 100644 --- a/shanoir-uploader/src/main/resources/profile.OFSEP-Qualif/profile.properties +++ b/shanoir-uploader/src/main/resources/profile.OFSEP-Qualif/profile.properties @@ -14,16 +14,7 @@ mode.subject.study.identifier=false anonymization.profile=Profile OFSEP -########################################################## -# Server properties for accessing to Shanoir-old server -########################################################## -shanoir.server.user.name= -shanoir.server.user.password= -shanoir.server.uploader.service.url= -shanoir.server.uploader.service.qname.local.part= -shanoir.server.uploader.service.qname.namespace.uri= - ########################################################## # Server properties for accessing to Shanoir-NG server ########################################################## -shanoir.server.url=https\://shanoir-ofsep-qualif.irisa.fr +shanoir.server.url=https\://shanoir-ofsep-qualif.irisa.fr \ No newline at end of file diff --git a/shanoir-uploader/src/main/resources/profile.OFSEP/profile.properties b/shanoir-uploader/src/main/resources/profile.OFSEP/profile.properties index 352f6355f3..e2d4366fff 100644 --- a/shanoir-uploader/src/main/resources/profile.OFSEP/profile.properties +++ b/shanoir-uploader/src/main/resources/profile.OFSEP/profile.properties @@ -14,16 +14,7 @@ mode.subject.study.identifier=false anonymization.profile=Profile OFSEP -########################################################## -# Server properties for accessing to Shanoir-old server -########################################################## -shanoir.server.user.name= -shanoir.server.user.password= -shanoir.server.uploader.service.url= -shanoir.server.uploader.service.qname.local.part= -shanoir.server.uploader.service.qname.namespace.uri= - ########################################################## # Server properties for accessing to Shanoir-NG server ########################################################## -shanoir.server.url=https\://shanoir-ofsep.irisa.fr +shanoir.server.url=https\://shanoir-ofsep.irisa.fr \ No newline at end of file diff --git a/shanoir-uploader/src/main/resources/profile.dev/profile.properties b/shanoir-uploader/src/main/resources/profile.dev/profile.properties index dcfcf1bd7a..5a94ca403f 100644 --- a/shanoir-uploader/src/main/resources/profile.dev/profile.properties +++ b/shanoir-uploader/src/main/resources/profile.dev/profile.properties @@ -14,16 +14,7 @@ mode.subject.study.identifier=true anonymization.profile=Profile Neurinfo -########################################################## -# Server properties for accessing to Shanoir-old server -########################################################## -shanoir.server.user.name= -shanoir.server.user.password= -shanoir.server.uploader.service.url= -shanoir.server.uploader.service.qname.local.part= -shanoir.server.uploader.service.qname.namespace.uri= - ########################################################## # Server properties for accessing to Shanoir-NG server ########################################################## -shanoir.server.url=https\://shanoir-ng-nginx +shanoir.server.url=https\://shanoir-ng-nginx \ No newline at end of file From a70b6e40516a381262a2b8f69c18a05517bd9dc0 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 13 Oct 2023 10:48:08 +0200 Subject: [PATCH 06/32] Update SelectProfileConfigurationState.java Important bug fix for auto configuration of profile in basic.properties --- .../uploader/action/init/SelectProfileConfigurationState.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java index 72cac6cc12..4bc75a3a7c 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/init/SelectProfileConfigurationState.java @@ -14,6 +14,8 @@ public void load(StartupStateContext context) { context.nextState(); } else { logger.info("Profile found in basic.properties. Used as default: " + ShUpConfig.profileSelected); + SelectProfilePanelActionListener actionListener = new SelectProfilePanelActionListener(null, null); + actionListener.configureSelectedProfile(ShUpConfig.profileSelected); context.getShUpStartupDialog().updateStartupText("\nProfile: " + ShUpConfig.profileSelected); context.setState(new AuthenticationConfigurationState()); context.nextState(); From a4976f1317b3b82c97042081d9a7643945d4b427 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 13 Oct 2023 11:27:33 +0200 Subject: [PATCH 07/32] Update QueryPACSService.java better and clearer logging for query pacs service, log usage of two types --- .../importer/dicom/query/QueryPACSService.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index e04a4a8ea5..d6cb5c7547 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -32,7 +32,6 @@ import org.dcm4che3.net.Association; import org.dcm4che3.net.Connection; import org.dcm4che3.net.Device; -import org.dcm4che3.net.DimseRSP; import org.dcm4che3.net.IncompatibleConnectionException; import org.dcm4che3.net.QueryOption; import org.dcm4che3.net.pdu.AAssociateRQ; @@ -101,15 +100,26 @@ public QueryPACSService() {} // for ShUp usage @PostConstruct private void initDicomNodes() { // Initialize connection configuration parameters here: to be used for all queries - this.calling = new DicomNode(callingName, callingHost, callingPort); // ShUp - this.called = new DicomNode(calledName, calledHost, calledPort); // PACS + this.calling = new DicomNode(callingName, callingHost, callingPort); + this.called = new DicomNode(calledName, calledHost, calledPort); + LOG.info("Query: DicomNodes initialized via CDI: calling ({}, {}, {}) and called ({}, {}, {})", + callingName, callingHost, callingPort, calledName, calledHost, calledPort); } + /** + * Do configuration of QueryPACSService from outside. Used by ShanoirUploader. + * + * @param calling + * @param called + * @param calledNameSCP + */ public void setDicomNodes(DicomNode calling, DicomNode called, String calledNameSCP) { this.calling = calling; this.called = called; this.calledNameSCP = calledNameSCP; this.maxPatientsFromPACS = 10; + LOG.info("Query: DicomNodes initialized via method call (ShUp): calling ({}, {}, {}) and called ({}, {}, {})", + calling.getAet(), calling.getHostname(), calling.getPort(), called.getAet(), called.getHostname(), called.getPort()); } public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException { From 2a25bc4b752862dcbf6eff7a501bf9eb1012c1e6 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 13 Oct 2023 15:11:45 +0200 Subject: [PATCH 08/32] Final fix: c-store from pacs back to work in ShUp --- .../action/DownloadOrCopyRunnable.java | 4 +-- .../action/FindDicomActionListener.java | 2 +- .../uploader/dicom/DicomServerClient.java | 28 ++++++++++++------- .../dicom/retrieve/DcmRcvManager.java | 7 ++++- .../org/shanoir/uploader/gui/MainWindow.java | 1 + 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/DownloadOrCopyRunnable.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/DownloadOrCopyRunnable.java index a10c38d08f..a632c4a784 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/DownloadOrCopyRunnable.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/DownloadOrCopyRunnable.java @@ -66,12 +66,12 @@ public void run() { /** * 2. Fill MRI information into serie from first DICOM file of each serie - * This has already been done for CD/DVD import, but not yet here for PACS + * This has already been done for CD/DVD import, but not yet here for PACS. */ if (this.isFromPACS) { for (Iterator iterator = selectedSeries.iterator(); iterator.hasNext();) { SerieTreeNode serieTreeNode = (SerieTreeNode) iterator.next(); - dicomFileAnalyzer.getAdditionalMetaDataFromFirstInstanceOfSerie(filePathDicomDir, serieTreeNode.getSerie(), null, isFromPACS); + dicomFileAnalyzer.getAdditionalMetaDataFromFirstInstanceOfSerie(uploadFolder.getAbsolutePath(), serieTreeNode.getSerie(), null, isFromPACS); } } } catch (FileNotFoundException e) { diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/FindDicomActionListener.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/FindDicomActionListener.java index 918ff875a5..037699401c 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/FindDicomActionListener.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/FindDicomActionListener.java @@ -246,7 +246,7 @@ private void fillMediaWithPatients(Media media, final List patients) { for (Iterator iterator = patients.iterator(); iterator.hasNext();) { Patient patient = (Patient) iterator.next(); final PatientTreeNode patientTreeNode = media.initChildTreeNode(patient); - logger.info("Patient info read from DICOMDIR: " + patient.toString()); + logger.info("Patient info read: " + patient.toString()); // add patients media.addTreeNode(patient.getPatientID(), patientTreeNode); List studies = patient.getStudies(); diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java index 12b25ef855..c4a9556b1f 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/DicomServerClient.java @@ -127,17 +127,25 @@ public boolean accept(File dir, String name) { } } }; - File[] newFileNames = uploadFolder.listFiles(oldFileNamesAndDICOMFilter); - logger.debug("newFileNames: " + newFileNames.length); - for (int i = 0; i < newFileNames.length; i++) { - fileNamesForSerie.add(newFileNames[i].getName()); + File serieFolder = new File (uploadFolder.getAbsolutePath() + File.separator + seriesInstanceUID); + if (serieFolder.exists()) { + File[] newFileNames = serieFolder.listFiles(oldFileNamesAndDICOMFilter); + logger.debug("newFileNames: " + newFileNames.length); + for (int i = 0; i < newFileNames.length; i++) { + fileNamesForSerie.add(newFileNames[i].getName()); + } + serieTreeNode.setFileNames(fileNamesForSerie); + retrievedDicomFiles.addAll(fileNamesForSerie); + oldFileNames.addAll(fileNamesForSerie); + logger.info(uploadFolder.getName() + ":\n\n Download of " + fileNamesForSerie.size() + + " DICOM files for serie " + seriesInstanceUID + ": " + serieTreeNode.getDisplayString() + + " was successful.\n\n"); + } else { + logger.error(uploadFolder.getName() + ":\n\n Download of " + fileNamesForSerie.size() + + " DICOM files for serie " + seriesInstanceUID + ": " + serieTreeNode.getDisplayString() + + " has failed.\n\n"); + return null; } - serieTreeNode.setFileNames(fileNamesForSerie); - retrievedDicomFiles.addAll(fileNamesForSerie); - oldFileNames.addAll(fileNamesForSerie); - logger.info(uploadFolder.getName() + ":\n\n Download of " + fileNamesForSerie.size() - + " DICOM files for serie " + seriesInstanceUID + ": " + serieTreeNode.getDisplayString() - + " was successful.\n\n"); } else { logger.error(uploadFolder.getName() + ":\n\n Download of " + fileNamesForSerie.size() + " DICOM files for serie " + seriesInstanceUID + ": " + serieTreeNode.getDisplayString() diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java index 1dadc8ff2a..872ec89abc 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/retrieve/DcmRcvManager.java @@ -24,7 +24,7 @@ public class DcmRcvManager { * In the brackets '{ggggeeee}' the dicom attribute value is used to be replaced. * We store in a folder with the SeriesInstanceUID and the file name of the SOPInstanceUID. */ - private static final String STORAGE_PATTERN = "{00080018}"; + private static final String STORAGE_PATTERN = "{0020000E}" + File.separator + "{00080018}"; public static final String DICOM_FILE_SUFFIX = ".dcm"; @@ -51,6 +51,11 @@ public void configure(final ConfigBean configBean) { this.lParams = new ListenerParams(params, true, STORAGE_PATTERN + DICOM_FILE_SUFFIX, null, null); } + /** + * Called from a synchronized method only, so should not be a problem for multiple usages. + * + * @param folderPath + */ public void startSCPServer(final String folderPath) { try { if(this.listener != null) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/gui/MainWindow.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/gui/MainWindow.java index 7f4ac0c97c..2b8f3901d4 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/gui/MainWindow.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/gui/MainWindow.java @@ -549,6 +549,7 @@ public void actionPerformed(ActionEvent e) { gbc_queryButton.gridy = 6; queryPanel.add(queryButton, gbc_queryButton); queryButton.setEnabled(false); + frame.getRootPane().setDefaultButton(queryButton); queryButton.addActionListener(fAL); JSeparator separator = new JSeparator(); From c47126a3591f79f70f4fe517531887c7fb8ea0fa Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 13 Oct 2023 15:34:03 +0200 Subject: [PATCH 09/32] ShUp: since dicoms are now in serie-folder from pacs, see code on server: fix for anonymizer: act recursive --- .../uploader/action/ImportFinishRunnable.java | 3 +-- .../uploader/dicom/anonymize/Anonymizer.java | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/ImportFinishRunnable.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/ImportFinishRunnable.java index 307310fe49..f3ceaab523 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/action/ImportFinishRunnable.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/action/ImportFinishRunnable.java @@ -60,13 +60,12 @@ public void run() { boolean anonymizationSuccess = false; try { String anonymizationProfile = ShUpConfig.profileProperties.getProperty(ANONYMIZATION_PROFILE); - anonymizationSuccess = anonymizer.anonymize(uploadFolder, anonymizationProfile, subjectName); + anonymizationSuccess = anonymizer.pseudonymize(uploadFolder, anonymizationProfile, subjectName); } catch (IOException e) { logger.error(uploadFolder.getName() + ": " + e.getMessage(), e); } if (anonymizationSuccess) { - logger.info(uploadFolder.getName() + ": DICOM files successfully anonymized."); /** * Write import-job.json to disk */ diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/anonymize/Anonymizer.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/anonymize/Anonymizer.java index 838c5326eb..59c7e4ab8c 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/anonymize/Anonymizer.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/anonymize/Anonymizer.java @@ -7,35 +7,39 @@ import org.apache.log4j.Logger; import org.shanoir.anonymization.anonymization.AnonymizationService; import org.shanoir.anonymization.anonymization.AnonymizationServiceImpl; +import org.shanoir.uploader.dicom.retrieve.DcmRcvManager; public class Anonymizer { private static Logger logger = Logger.getLogger(Anonymizer.class); - public boolean anonymize(final File uploadFolder, + public boolean pseudonymize(final File uploadFolder, final String profile, final String subjectName) throws IOException { - ArrayList dicomFiles = getListOfDicomFiles(uploadFolder); + ArrayList dicomFiles = new ArrayList(); + getListOfDicomFiles(uploadFolder, dicomFiles); try { AnonymizationService anonymizationService = new AnonymizationServiceImpl(); anonymizationService.anonymizeForShanoir(dicomFiles, profile, subjectName, subjectName); + logger.info("--> " + dicomFiles.size() + " DICOM files successfully pseudonymized."); } catch (Exception e) { - logger.error("anonymization service: ", e); + logger.error("pseudonymization service: ", e); return false; } return true; } - private ArrayList getListOfDicomFiles(final File uploadFolder) - throws IOException { - ArrayList dicomFileList = new ArrayList(); - File[] listOfFiles = uploadFolder.listFiles(); + private void getListOfDicomFiles(final File folder, ArrayList dicomFiles) throws IOException { + File[] listOfFiles = folder.listFiles(); for (File file : listOfFiles) { - if (file.isFile() && !file.getName().endsWith(".xml")) { - dicomFileList.add(file); + if (file.isFile() && file.getName().endsWith(DcmRcvManager.DICOM_FILE_SUFFIX)) { + dicomFiles.add(file); + } else { + if (file.isDirectory()) { + getListOfDicomFiles(file, dicomFiles); + } } } - return dicomFileList; } } From b7a3d4b19a1debc9c5f84ce703acd2e6362bfef5 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 13 Oct 2023 17:03:23 +0200 Subject: [PATCH 10/32] Final fix: import from pacs with ShUp back to work again! --- .../main/java/org/shanoir/uploader/upload/UploadServiceJob.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/upload/UploadServiceJob.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/upload/UploadServiceJob.java index 73a37167c4..86d00bf28e 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/upload/UploadServiceJob.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/upload/UploadServiceJob.java @@ -94,7 +94,7 @@ private void processFolderForServer(final File folder, final UploadJobManager up final File uploadJobFile, CurrentNominativeDataController currentNominativeDataController) { NominativeDataUploadJobManager nominativeDataUploadJobManager = null; final List filesToTransfer = new ArrayList(); - final Collection files = Util.listFiles(folder, null, false); + final Collection files = Util.listFiles(folder, null, true); for (Iterator filesIt = files.iterator(); filesIt.hasNext();) { final File file = (File) filesIt.next(); // do not transfer nominativeDataUploadJob as only for display in ShUp From 05194c5001c31eb21bb1c8c9fb304f6eea9a98ae Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 13 Oct 2023 17:16:46 +0200 Subject: [PATCH 11/32] Final fix: import-from-local-file-system back to work again! Yes! --- .../src/main/java/org/shanoir/uploader/utils/ImportUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/utils/ImportUtils.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/utils/ImportUtils.java index 95fe8be125..d5bc44d4d0 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/utils/ImportUtils.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/utils/ImportUtils.java @@ -21,6 +21,7 @@ import org.shanoir.uploader.action.DicomDataTransferObject; import org.shanoir.uploader.dicom.IDicomServerClient; import org.shanoir.uploader.dicom.query.SerieTreeNode; +import org.shanoir.uploader.dicom.retrieve.DcmRcvManager; import org.shanoir.uploader.model.rest.IdName; import org.shanoir.uploader.model.rest.Study; import org.shanoir.uploader.model.rest.StudyCard; @@ -290,7 +291,7 @@ public static List copyFilesToUploadFolder(ImagesCreatorAndDicomFileAnal List newFileNamesOfSerie = new ArrayList(); for (Instance instance : serie.getInstances()) { File sourceFile = dicomFileAnalyzer.getFileFromInstance(instance, serie, filePathDicomDir, false); - String dicomFileName = sourceFile.getAbsolutePath().replace(File.separator, "_"); + String dicomFileName = sourceFile.getAbsolutePath().replace(File.separator, "_") + DcmRcvManager.DICOM_FILE_SUFFIX; File destFile = new File(uploadFolder.getAbsolutePath() + File.separator + dicomFileName); FileUtil.copyFile(sourceFile, destFile); newFileNamesOfSerie.add(dicomFileName); From 0ade3d828d041e1608f290329acc6b5f472923b1 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Mon, 16 Oct 2023 15:07:39 +0200 Subject: [PATCH 12/32] V1 - QueryPACSService: use same connection all time --- .../dicom/query/QueryPACSService.java | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index d6cb5c7547..0bfb9cf801 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.security.GeneralSecurityException; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -26,17 +27,21 @@ import org.apache.commons.lang3.StringUtils; import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.ElementDictionary; import org.dcm4che3.data.Tag; import org.dcm4che3.data.UID; +import org.dcm4che3.data.VR; import org.dcm4che3.net.ApplicationEntity; import org.dcm4che3.net.Association; import org.dcm4che3.net.Connection; import org.dcm4che3.net.Device; import org.dcm4che3.net.IncompatibleConnectionException; import org.dcm4che3.net.QueryOption; +import org.dcm4che3.net.Status; import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.net.service.QueryRetrieveLevel; +import org.dcm4che3.tool.findscu.FindSCU; import org.dcm4che3.tool.findscu.FindSCU.InformationModel; import org.shanoir.ng.importer.dicom.DicomSerieAndInstanceAnalyzer; import org.shanoir.ng.importer.dicom.InstanceNumberSorter; @@ -51,14 +56,18 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.weasis.core.api.util.FileUtil; +import org.weasis.core.api.util.StringUtil; import org.weasis.dicom.op.CFind; import org.weasis.dicom.op.CMove; import org.weasis.dicom.param.AdvancedParams; +import org.weasis.dicom.param.DeviceOpService; import org.weasis.dicom.param.DicomNode; import org.weasis.dicom.param.DicomParam; import org.weasis.dicom.param.DicomProgress; import org.weasis.dicom.param.DicomState; import org.weasis.dicom.param.ProgressListener; +import org.weasis.dicom.util.ServiceUtil; import jakarta.annotation.PostConstruct; @@ -92,11 +101,16 @@ public class QueryPACSService { private DicomNode called; + private FindSCU findSCU; + @Value("${shanoir.import.pacs.store.aet.called.name}") private String calledNameSCP; public QueryPACSService() {} // for ShUp usage + /** + * Used within microservice MS Import on the server, via PostConstruct. + */ @PostConstruct private void initDicomNodes() { // Initialize connection configuration parameters here: to be used for all queries @@ -104,6 +118,7 @@ private void initDicomNodes() { this.called = new DicomNode(calledName, calledHost, calledPort); LOG.info("Query: DicomNodes initialized via CDI: calling ({}, {}, {}) and called ({}, {}, {})", callingName, callingHost, callingPort, calledName, calledHost, calledPort); + initFindSCU(calling, called); } /** @@ -120,6 +135,34 @@ public void setDicomNodes(DicomNode calling, DicomNode called, String calledName this.maxPatientsFromPACS = 10; LOG.info("Query: DicomNodes initialized via method call (ShUp): calling ({}, {}, {}) and called ({}, {}, {})", calling.getAet(), calling.getHostname(), calling.getPort(), called.getAet(), called.getHostname(), called.getPort()); + initFindSCU(calling, called); + } + + private void initFindSCU(DicomNode calling, DicomNode called) { + try (FindSCU findSCU = new FindSCU()) { + // calling configuration + Connection connection = findSCU.getConnection(); + findSCU.getApplicationEntity().setAETitle(calling.getAet()); + connection.setHostname(calling.getHostname()); + connection.setPort(calling.getPort()); + // called configuration + Connection remote = findSCU.getRemoteConnection(); + findSCU.getAAssociateRQ().setCalledAET(called.getAet()); + remote.setHostname(called.getHostname()); + remote.setPort(called.getPort()); + try { + long t1 = System.currentTimeMillis(); + findSCU.open(); + long t2 = System.currentTimeMillis(); + LOG.info("FindSCU initialized in {}ms.", t2 - t1); + this.findSCU = findSCU; + } catch (Exception e) { + LOG.error("FindSCU", e); + ServiceUtil.forceGettingAttributes(findSCU.getState(), findSCU); + } + } catch (Exception e) { + LOG.error("FindSCU", e); + } } public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException { @@ -437,9 +480,36 @@ private List queryCFIND(DicomParam[] params, QueryRetrieveLevel leve options.setInformationModel(InformationModel.StudyRoot); } logQuery(params, options); - DicomState state = CFind.process(options, calling, called, 0, level, params); + DicomState state = processCFind(options, 0, level, params); return state.getDicomRSP(); } + + private DicomState processCFind(AdvancedParams params, int cancelAfter, QueryRetrieveLevel level, DicomParam... keys) { + this.findSCU.setInformationModel(getInformationModel(params), params.getTsuidOrder(), params.getQueryOptions()); + if (level != null) { + this.findSCU.addLevel(level.name()); + } + for (DicomParam p : keys) { + addAttributes(findSCU.getKeys(), p); + } + findSCU.setCancelAfter(cancelAfter); + findSCU.setPriority(params.getPriority()); + try { + DicomState dcmState = findSCU.getState(); + long t1 = System.currentTimeMillis(); + findSCU.query(); + ServiceUtil.forceGettingAttributes(dcmState, findSCU); + long t2 = System.currentTimeMillis(); + String timeMsg = + MessageFormat.format("DICOM C-Find from {0} to {1}. Query in {3}ms.", + findSCU.getAAssociateRQ().getCallingAET(), findSCU.getAAssociateRQ().getCalledAET(), t2 - t1); + return DicomState.buildMessage(dcmState, timeMsg, null); + } catch (Exception e) { + LOG.error("FindSCU", e); + ServiceUtil.forceGettingAttributes(findSCU.getState(), findSCU); + return DicomState.buildMessage(findSCU.getState(), null, e); + } + } /** * This method logs the params and options of the PACS query. @@ -452,5 +522,30 @@ private void logQuery(DicomParam[] params, AdvancedParams options) { LOG.info("Tag: {}, Value: {}", params[i].getTagName(), Arrays.toString(params[i].getValues())); } } + + private InformationModel getInformationModel(AdvancedParams options) { + Object model = options.getInformationModel(); + if (model instanceof InformationModel) { + return (InformationModel) model; + } + return InformationModel.StudyRoot; + } + + private void addAttributes(Attributes attrs, DicomParam param) { + int tag = param.getTag(); + String[] ss = param.getValues(); + VR vr = ElementDictionary.vrOf(tag, attrs.getPrivateCreator(tag)); + if (ss == null || ss.length == 0) { + // Returning key + if (vr == VR.SQ) { + attrs.newSequence(tag, 1).add(new Attributes(0)); + } else { + attrs.setNull(tag, vr); + } + } else { + // Matching key + attrs.setString(tag, vr, ss); + } + } } From 3bcb696b0996dcfc84d8d905ef552233f2dbcbce Mon Sep 17 00:00:00 2001 From: michaelkain Date: Tue, 17 Oct 2023 14:36:18 +0200 Subject: [PATCH 13/32] Update anonymization.xlsx As asked by CH: keep institutionName and -Address now for Ofsep --- .../src/main/resources/anonymization.xlsx | Bin 29727 -> 29764 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/shanoir-ng-anonymization/src/main/resources/anonymization.xlsx b/shanoir-ng-anonymization/src/main/resources/anonymization.xlsx index 9b3181e3c82b33b5369a421548a88c24f8b0f0b1..0431e6aa035757fb91ceb6819b5e705d861ff5d2 100644 GIT binary patch delta 21424 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 delta 21449 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 kNo`N0c3v} Date: Tue, 17 Oct 2023 14:55:26 +0200 Subject: [PATCH 14/32] Update ContrastAgentUsed.java OFSEP: new contrast agent used added --- .../ng/datasetacquisition/model/mr/ContrastAgentUsed.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/model/mr/ContrastAgentUsed.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/model/mr/ContrastAgentUsed.java index 99f00d5abb..9b84efff50 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/model/mr/ContrastAgentUsed.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/model/mr/ContrastAgentUsed.java @@ -41,7 +41,10 @@ public enum ContrastAgentUsed { GADOVIST(7), // Clariscan - CLARISCAN(8); + CLARISCAN(8), + + // Dotarem + DOTAREM(9); private int id; From 242203d377574628657a93716be2b0b616d99422 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Tue, 17 Oct 2023 17:22:37 +0200 Subject: [PATCH 15/32] Update QueryPACSService.java first impl. initAssociation --- .../dicom/query/QueryPACSService.java | 241 +++++++----------- 1 file changed, 97 insertions(+), 144 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index 0bfb9cf801..f185b55117 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -16,7 +16,6 @@ import java.io.IOException; import java.security.GeneralSecurityException; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -35,14 +34,14 @@ import org.dcm4che3.net.Association; import org.dcm4che3.net.Connection; import org.dcm4che3.net.Device; +import org.dcm4che3.net.DimseRSPHandler; import org.dcm4che3.net.IncompatibleConnectionException; +import org.dcm4che3.net.Priority; import org.dcm4che3.net.QueryOption; import org.dcm4che3.net.Status; import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.net.service.QueryRetrieveLevel; -import org.dcm4che3.tool.findscu.FindSCU; -import org.dcm4che3.tool.findscu.FindSCU.InformationModel; import org.shanoir.ng.importer.dicom.DicomSerieAndInstanceAnalyzer; import org.shanoir.ng.importer.dicom.InstanceNumberSorter; import org.shanoir.ng.importer.dicom.SeriesNumberSorter; @@ -56,18 +55,13 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.weasis.core.api.util.FileUtil; -import org.weasis.core.api.util.StringUtil; -import org.weasis.dicom.op.CFind; import org.weasis.dicom.op.CMove; import org.weasis.dicom.param.AdvancedParams; -import org.weasis.dicom.param.DeviceOpService; import org.weasis.dicom.param.DicomNode; import org.weasis.dicom.param.DicomParam; import org.weasis.dicom.param.DicomProgress; import org.weasis.dicom.param.DicomState; import org.weasis.dicom.param.ProgressListener; -import org.weasis.dicom.util.ServiceUtil; import jakarta.annotation.PostConstruct; @@ -101,7 +95,7 @@ public class QueryPACSService { private DicomNode called; - private FindSCU findSCU; + private Association association; @Value("${shanoir.import.pacs.store.aet.called.name}") private String calledNameSCP; @@ -118,7 +112,7 @@ private void initDicomNodes() { this.called = new DicomNode(calledName, calledHost, calledPort); LOG.info("Query: DicomNodes initialized via CDI: calling ({}, {}, {}) and called ({}, {}, {})", callingName, callingHost, callingPort, calledName, calledHost, calledPort); - initFindSCU(calling, called); + initAssociation(calling, called); } /** @@ -135,34 +129,44 @@ public void setDicomNodes(DicomNode calling, DicomNode called, String calledName this.maxPatientsFromPACS = 10; LOG.info("Query: DicomNodes initialized via method call (ShUp): calling ({}, {}, {}) and called ({}, {}, {})", calling.getAet(), calling.getHostname(), calling.getPort(), called.getAet(), called.getHostname(), called.getPort()); - initFindSCU(calling, called); + initAssociation(calling, called); } - private void initFindSCU(DicomNode calling, DicomNode called) { - try (FindSCU findSCU = new FindSCU()) { - // calling configuration - Connection connection = findSCU.getConnection(); - findSCU.getApplicationEntity().setAETitle(calling.getAet()); - connection.setHostname(calling.getHostname()); - connection.setPort(calling.getPort()); - // called configuration - Connection remote = findSCU.getRemoteConnection(); - findSCU.getAAssociateRQ().setCalledAET(called.getAet()); - remote.setHostname(called.getHostname()); - remote.setPort(called.getPort()); - try { - long t1 = System.currentTimeMillis(); - findSCU.open(); - long t2 = System.currentTimeMillis(); - LOG.info("FindSCU initialized in {}ms.", t2 - t1); - this.findSCU = findSCU; - } catch (Exception e) { - LOG.error("FindSCU", e); - ServiceUtil.forceGettingAttributes(findSCU.getState(), findSCU); - } - } catch (Exception e) { - LOG.error("FindSCU", e); - } + private void initAssociation(DicomNode calling, DicomNode called) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + try { + // calling: create a device, a connection and an application entity + Device device = new Device(this.getClass().getName()); + device.setExecutor(executor); + device.setScheduledExecutor(scheduledExecutor); + ApplicationEntity callingAE = new ApplicationEntity(calling.getAet()); + Connection callingConn = new Connection(); + device.addConnection(callingConn); + device.addApplicationEntity(callingAE); + callingAE.addConnection(callingConn); + // called: create a connection and an AAssociateRQ + Connection calledConn = new Connection(null, called.getHostname(), called.getPort()); + AAssociateRQ aarq = new AAssociateRQ(); + aarq.setCallingAET(calling.getAet()); + aarq.setCalledAET(called.getAet()); + aarq.addPresentationContext(new PresentationContext(1, + UID.Verification, UID.ImplicitVRLittleEndian)); + aarq.addPresentationContext(new PresentationContext(2, + UID.PatientRootQueryRetrieveInformationModelFind, UID.ImplicitVRLittleEndian)); + aarq.addPresentationContext(new PresentationContext(3, + UID.StudyRootQueryRetrieveInformationModelFind, UID.ImplicitVRLittleEndian)); + this.association = callingAE.connect(calledConn, aarq); + LOG.info("initAssociation finished between calling {} and called {}", calling.getAet(), called.getAet()); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } catch (InterruptedException e) { + LOG.error(e.getMessage(), e); + } catch (IncompatibleConnectionException e) { + LOG.error(e.getMessage(), e); + } catch (GeneralSecurityException e) { + LOG.error(e.getMessage(), e); + } } public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException { @@ -175,7 +179,7 @@ public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException || StringUtils.isNotBlank(dicomQuery.getPatientBirthDate())) { // For Patient Name and Patient ID, wild card search is not allowed if (!dicomQuery.getPatientName().contains("*") && !dicomQuery.getPatientID().contains("*")) { - queryPatientLevel(dicomQuery, calling, called, importJob); + queryPatientLevel(dicomQuery, importJob); } // @Todo: implement wild card search // Do Fuzzy search on base of patient name here @@ -189,7 +193,7 @@ public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException */ } else if (StringUtils.isNotBlank(dicomQuery.getStudyDescription()) || StringUtils.isNotBlank(dicomQuery.getStudyDate())) { - queryStudyLevel(dicomQuery, calling, called, importJob); + queryStudyLevel(dicomQuery, importJob); } else { throw new ShanoirImportException("DicomQuery: missing parameters."); } @@ -217,60 +221,29 @@ public void handleProgression(DicomProgress progress) { public boolean queryECHO(String calledAET, String hostName, int port, String callingAET) { LOG.info("DICOM ECHO: Starting with configuration {}, {}, {} <- {}", calledAET, hostName, port, callingAET); - ExecutorService executor = Executors.newSingleThreadExecutor(); - ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); try { - // set up calling configuration - Device device = new Device("c-echo-scu"); - ApplicationEntity callingAE = new ApplicationEntity(callingAET); - Connection callingConn = new Connection(); - device.addApplicationEntity(callingAE); - device.addConnection(callingConn); - device.setExecutor(executor); - device.setScheduledExecutor(scheduledExecutor); - callingAE.addConnection(callingConn); - - Connection calledConn = new Connection(null, hostName, port); - AAssociateRQ aarq = new AAssociateRQ(); - aarq.setCallingAET(callingAET); - aarq.setCalledAET(calledAET); - aarq.addPresentationContext(new PresentationContext(1, - UID.Verification, UID.ImplicitVRLittleEndian)); - Association as = callingAE.connect(calledConn, aarq); - as.cecho(); - as.release(); + this.association.cecho(); } catch (IOException e) { LOG.error(e.getMessage(), e); return false; } catch (InterruptedException e) { LOG.error(e.getMessage(), e); return false; - } catch (IncompatibleConnectionException e) { - LOG.error(e.getMessage(), e); - return false; - } catch (GeneralSecurityException e) { - LOG.error(e.getMessage(), e); - return false; - } finally { - executor.shutdown(); - scheduledExecutor.shutdown(); - } + } return true; } /** * This method queries on patient root level. * @param dicomQuery - * @param calling - * @param called * @param importJob */ - private void queryPatientLevel(DicomQuery dicomQuery, DicomNode calling, DicomNode called, ImportJob importJob) { + private void queryPatientLevel(DicomQuery dicomQuery, ImportJob importJob) { DicomParam patientName = initDicomParam(Tag.PatientName, dicomQuery.getPatientName()); DicomParam patientID = initDicomParam(Tag.PatientID, dicomQuery.getPatientID()); DicomParam patientBirthDate = initDicomParam(Tag.PatientBirthDate, dicomQuery.getPatientBirthDate()); DicomParam[] params = { patientName, patientID, patientBirthDate, new DicomParam(Tag.PatientBirthName), new DicomParam(Tag.PatientSex) }; - List attributesPatients = queryCFIND(params, QueryRetrieveLevel.PATIENT, calling, called); + List attributesPatients = queryCFind(params, QueryRetrieveLevel.PATIENT); if (attributesPatients != null) { // Limit the max number of patients returned int patientsNbre = attributesPatients.size(); @@ -283,7 +256,7 @@ private void queryPatientLevel(DicomQuery dicomQuery, DicomNode calling, DicomNo boolean patientExists = patients.stream().anyMatch(p -> p.getPatientID().equals(patient.getPatientID())); if (!patientExists) { patients.add(patient); - queryStudies(calling, called, dicomQuery, patient); + queryStudies(dicomQuery, patient); } } importJob.setPatients(patients); @@ -297,13 +270,13 @@ private void queryPatientLevel(DicomQuery dicomQuery, DicomNode calling, DicomNo * @param called * @param importJob */ - private void queryStudyLevel(DicomQuery dicomQuery, DicomNode calling, DicomNode called, ImportJob importJob) { + private void queryStudyLevel(DicomQuery dicomQuery, ImportJob importJob) { DicomParam studyDescription = initDicomParam(Tag.StudyDescription, dicomQuery.getStudyDescription()); DicomParam studyDate = initDicomParam(Tag.StudyDate, dicomQuery.getStudyDate()); DicomParam[] params = { studyDescription, studyDate, new DicomParam(Tag.PatientName), new DicomParam(Tag.PatientID), new DicomParam(Tag.PatientBirthDate), new DicomParam(Tag.PatientBirthName), new DicomParam(Tag.PatientSex), new DicomParam(Tag.StudyInstanceUID) }; - List attributesStudies = queryCFIND(params, QueryRetrieveLevel.STUDY, calling, called); + List attributesStudies = queryCFind(params, QueryRetrieveLevel.STUDY); if (attributesStudies != null) { List patients = new ArrayList<>(); for (int i = 0; i < attributesStudies.size(); i++) { @@ -313,7 +286,7 @@ private void queryStudyLevel(DicomQuery dicomQuery, DicomNode calling, DicomNode // handle studies Study study = new Study(attributesStudies.get(i)); patient.getStudies().add(study); - querySeries(calling, called, study); + querySeries(study); } // Limit the max number of patients returned if (maxPatientsFromPACS < patients.size()) { @@ -363,7 +336,7 @@ private DicomParam initDicomParam(int tag, String value) { * @param dicomQuery * @param patient */ - private void queryStudies(DicomNode calling, DicomNode called, DicomQuery dicomQuery, Patient patient) { + private void queryStudies(DicomQuery dicomQuery, Patient patient) { DicomParam[] params = { new DicomParam(Tag.PatientID, patient.getPatientID()), new DicomParam(Tag.PatientName, patient.getPatientName()), @@ -371,13 +344,13 @@ private void queryStudies(DicomNode calling, DicomNode called, DicomQuery dicomQ new DicomParam(Tag.StudyDate, dicomQuery.getStudyDate()), new DicomParam(Tag.StudyDescription, dicomQuery.getStudyDescription()) }; - List attributesStudies = queryCFIND(params, QueryRetrieveLevel.STUDY, calling, called); + List attributesStudies = queryCFind(params, QueryRetrieveLevel.STUDY); if (attributesStudies != null) { List studies = new ArrayList<>(); for (int i = 0; i < attributesStudies.size(); i++) { Study study = new Study(attributesStudies.get(i)); studies.add(study); - querySeries(calling, called, study); + querySeries(study); } studies.sort((p1, p2) -> p1.getStudyDate().compareTo(p2.getStudyDate())); patient.setStudies(studies); @@ -390,7 +363,7 @@ private void queryStudies(DicomNode calling, DicomNode called, DicomQuery dicomQ * @param called * @param study */ - private void querySeries(DicomNode calling, DicomNode called, Study study) { + private void querySeries(Study study) { DicomParam[] params = { new DicomParam(Tag.StudyInstanceUID, study.getStudyInstanceUID()), new DicomParam(Tag.SeriesInstanceUID), @@ -404,14 +377,14 @@ private void querySeries(DicomNode calling, DicomNode called, Study study) { new DicomParam(Tag.ManufacturerModelName), new DicomParam(Tag.DeviceSerialNumber) }; - List attributesList = queryCFIND(params, QueryRetrieveLevel.SERIES, calling, called); + List attributesList = queryCFind(params, QueryRetrieveLevel.SERIES); if (attributesList != null) { List series = new ArrayList(); for (int i = 0; i < attributesList.size(); i++) { Attributes attributes = attributesList.get(i); Serie serie = new Serie(attributes); if (!DicomSerieAndInstanceAnalyzer.checkSerieIsIgnored(attributes)) { - queryInstances(calling, called, serie, study); + queryInstances(serie, study); if (!serie.getInstances().isEmpty()) { DicomSerieAndInstanceAnalyzer.checkSerieIsEnhanced(serie, attributes); DicomSerieAndInstanceAnalyzer.checkSerieIsSpectroscopy(serie); @@ -439,14 +412,14 @@ private void querySeries(DicomNode calling, DicomNode called, Study study) { * @param called * @param serie */ - private void queryInstances(DicomNode calling, DicomNode called, Serie serie, Study study) { + private void queryInstances(Serie serie, Study study) { DicomParam[] params = { new DicomParam(Tag.StudyInstanceUID, study.getStudyInstanceUID()), new DicomParam(Tag.SeriesInstanceUID, serie.getSeriesInstanceUID()), new DicomParam(Tag.SOPInstanceUID), new DicomParam(Tag.InstanceNumber) }; - List attributes = queryCFIND(params, QueryRetrieveLevel.IMAGE, calling, called); + List attributes = queryCFind(params, QueryRetrieveLevel.IMAGE); if (attributes != null) { List instances = new ArrayList<>(); for (int i = 0; i < attributes.size(); i++) { @@ -462,76 +435,56 @@ private void queryInstances(DicomNode calling, DicomNode called, Serie serie, St /** * This method does a C-FIND query and returns the results. - * @param params + * + * The state of each c-find query is a local attribute of the method. + * So, when e.g. on the server 3 users call in parallel queryCFind, + * each one has its own DimseRSPHandler and its own state, so this + * might work, in case the association is not caching aspects. + * + * @param keys * @param level - * @param calling - * @param called * @return */ - private List queryCFIND(DicomParam[] params, QueryRetrieveLevel level, final DicomNode calling, final DicomNode called) { - AdvancedParams options = new AdvancedParams(); + private List queryCFind(DicomParam[] keys, QueryRetrieveLevel level) { + String cuid = null; if (level.equals(QueryRetrieveLevel.PATIENT)) { - options.setInformationModel(InformationModel.PatientRoot); + cuid = UID.PatientRootQueryRetrieveInformationModelFind; } else if (level.equals(QueryRetrieveLevel.STUDY)) { - options.setInformationModel(InformationModel.StudyRoot); + cuid = UID.StudyRootQueryRetrieveInformationModelFind; } else if (level.equals(QueryRetrieveLevel.SERIES)) { - options.setInformationModel(InformationModel.StudyRoot); + cuid = UID.StudyRootQueryRetrieveInformationModelFind; } else if (level.equals(QueryRetrieveLevel.IMAGE)) { - options.setInformationModel(InformationModel.StudyRoot); + cuid = UID.StudyRootQueryRetrieveInformationModelFind; } - logQuery(params, options); - DicomState state = processCFind(options, 0, level, params); - return state.getDicomRSP(); - } - - private DicomState processCFind(AdvancedParams params, int cancelAfter, QueryRetrieveLevel level, DicomParam... keys) { - this.findSCU.setInformationModel(getInformationModel(params), params.getTsuidOrder(), params.getQueryOptions()); - if (level != null) { - this.findSCU.addLevel(level.name()); - } - for (DicomParam p : keys) { - addAttributes(findSCU.getKeys(), p); - } - findSCU.setCancelAfter(cancelAfter); - findSCU.setPriority(params.getPriority()); - try { - DicomState dcmState = findSCU.getState(); - long t1 = System.currentTimeMillis(); - findSCU.query(); - ServiceUtil.forceGettingAttributes(dcmState, findSCU); - long t2 = System.currentTimeMillis(); - String timeMsg = - MessageFormat.format("DICOM C-Find from {0} to {1}. Query in {3}ms.", - findSCU.getAAssociateRQ().getCallingAET(), findSCU.getAAssociateRQ().getCalledAET(), t2 - t1); - return DicomState.buildMessage(dcmState, timeMsg, null); - } catch (Exception e) { - LOG.error("FindSCU", e); - ServiceUtil.forceGettingAttributes(findSCU.getState(), findSCU); - return DicomState.buildMessage(findSCU.getState(), null, e); - } - } - - /** - * This method logs the params and options of the PACS query. - * @param params - * @param options - */ - private void logQuery(DicomParam[] params, AdvancedParams options) { - LOG.info("Calling PACS, C-FIND with level: {} and params:", options.getInformationModel()); - for (int i = 0; i < params.length; i++) { - LOG.info("Tag: {}, Value: {}", params[i].getTagName(), Arrays.toString(params[i].getValues())); + Attributes attributes = new Attributes(); + for (DicomParam p : keys) { + addAttributes(attributes, p); } + DicomState state = new DicomState(new DicomProgress()); + DimseRSPHandler rspHandler = new DimseRSPHandler(this.association.nextMessageID()) { + @Override + public void onDimseRSP(Association as, Attributes cmd, Attributes data) { + super.onDimseRSP(as, cmd, data); + int status = cmd.getInt(Tag.Status, -1); + if (Status.isPending(status)) { + } else { + state.setStatus(status); + } + } + }; + try { + LOG.info("Calling PACS, C-FIND with level: {}", level); + for (int i = 0; i < keys.length; i++) { + LOG.info("Tag: {}, Value: {}", keys[i].getTagName(), Arrays.toString(keys[i].getValues())); + } + this.association.cfind(cuid, Priority.NORMAL, attributes, null, rspHandler); + } catch (IOException | InterruptedException e) { + LOG.error("Error in c-find query: ", e); + } + return state.getDicomRSP(); } - - private InformationModel getInformationModel(AdvancedParams options) { - Object model = options.getInformationModel(); - if (model instanceof InformationModel) { - return (InformationModel) model; - } - return InformationModel.StudyRoot; - } - private void addAttributes(Attributes attrs, DicomParam param) { + public void addAttributes(Attributes attrs, DicomParam param) { int tag = param.getTag(); String[] ss = param.getValues(); VR vr = ElementDictionary.vrOf(tag, attrs.getPrivateCreator(tag)); From 5eb3f11c35a543cd76c459ca6b02624f60a91700 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Wed, 18 Oct 2023 13:58:02 +0200 Subject: [PATCH 16/32] Update QueryPACSService.java fix: first result seen in tree --- .../ng/importer/dicom/query/QueryPACSService.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index f185b55117..6224850a90 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -42,6 +42,7 @@ import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.net.service.QueryRetrieveLevel; +import org.dcm4che3.tool.findscu.FindSCU.InformationModel; import org.shanoir.ng.importer.dicom.DicomSerieAndInstanceAnalyzer; import org.shanoir.ng.importer.dicom.InstanceNumberSorter; import org.shanoir.ng.importer.dicom.SeriesNumberSorter; @@ -55,6 +56,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.weasis.dicom.op.CFind; import org.weasis.dicom.op.CMove; import org.weasis.dicom.param.AdvancedParams; import org.weasis.dicom.param.DicomNode; @@ -456,10 +458,6 @@ private List queryCFind(DicomParam[] keys, QueryRetrieveLevel level) } else if (level.equals(QueryRetrieveLevel.IMAGE)) { cuid = UID.StudyRootQueryRetrieveInformationModelFind; } - Attributes attributes = new Attributes(); - for (DicomParam p : keys) { - addAttributes(attributes, p); - } DicomState state = new DicomState(new DicomProgress()); DimseRSPHandler rspHandler = new DimseRSPHandler(this.association.nextMessageID()) { @Override @@ -467,12 +465,18 @@ public void onDimseRSP(Association as, Attributes cmd, Attributes data) { super.onDimseRSP(as, cmd, data); int status = cmd.getInt(Tag.Status, -1); if (Status.isPending(status)) { + state.addDicomRSP(data); } else { state.setStatus(status); } } }; try { + Attributes attributes = new Attributes(); + attributes.setString(Tag.QueryRetrieveLevel, VR.CS, level.name()); + for (DicomParam p : keys) { + addAttributes(attributes, p); + } LOG.info("Calling PACS, C-FIND with level: {}", level); for (int i = 0; i < keys.length; i++) { LOG.info("Tag: {}, Value: {}", keys[i].getTagName(), Arrays.toString(keys[i].getValues())); @@ -483,7 +487,7 @@ public void onDimseRSP(Association as, Attributes cmd, Attributes data) { } return state.getDicomRSP(); } - + public void addAttributes(Attributes attrs, DicomParam param) { int tag = param.getTag(); String[] ss = param.getValues(); From 7f7b405ccca55088da3bc174ca81918e0e434714 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Wed, 18 Oct 2023 15:15:36 +0200 Subject: [PATCH 17/32] Update QueryPACSService: final fix, now very fast --- .../shanoir/ng/importer/dicom/query/QueryPACSService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index 6224850a90..e230933417 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -56,6 +56,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.weasis.core.api.util.FileUtil; import org.weasis.dicom.op.CFind; import org.weasis.dicom.op.CMove; import org.weasis.dicom.param.AdvancedParams; @@ -64,6 +65,7 @@ import org.weasis.dicom.param.DicomProgress; import org.weasis.dicom.param.DicomState; import org.weasis.dicom.param.ProgressListener; +import org.weasis.dicom.util.ServiceUtil; import jakarta.annotation.PostConstruct; @@ -481,7 +483,10 @@ public void onDimseRSP(Association as, Attributes cmd, Attributes data) { for (int i = 0; i < keys.length; i++) { LOG.info("Tag: {}, Value: {}", keys[i].getTagName(), Arrays.toString(keys[i].getValues())); } - this.association.cfind(cuid, Priority.NORMAL, attributes, null, rspHandler); + association.cfind(cuid, Priority.NORMAL, attributes, null, rspHandler); + if (association.isReadyForDataTransfer()) { + association.waitForOutstandingRSP(); + } } catch (IOException | InterruptedException e) { LOG.error("Error in c-find query: ", e); } From e275f923f406fdfa3af64b5192f53e6cf80c731f Mon Sep 17 00:00:00 2001 From: michaelkain Date: Wed, 18 Oct 2023 15:15:53 +0200 Subject: [PATCH 18/32] Update pom.xml --- shanoir-ng-back/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shanoir-ng-back/pom.xml b/shanoir-ng-back/pom.xml index 2c05c1ed24..cb2bba7be8 100644 --- a/shanoir-ng-back/pom.xml +++ b/shanoir-ng-back/pom.xml @@ -49,7 +49,7 @@ along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html 17 1.5.3.Final 22.0.1 - 5.30.0 + 5.31.0 From 87381e043111c1c3c68478c420cc7fc70b90af66 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Thu, 19 Oct 2023 16:49:52 +0200 Subject: [PATCH 19/32] ShUp: fix for mriInformation in ImportDialog, to select correct study card --- .../uploader/dicom/query/SerieTreeNode.java | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/query/SerieTreeNode.java b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/query/SerieTreeNode.java index 56022c4621..6d421585a4 100644 --- a/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/query/SerieTreeNode.java +++ b/shanoir-uploader/src/main/java/org/shanoir/uploader/dicom/query/SerieTreeNode.java @@ -44,6 +44,8 @@ public class SerieTreeNode implements DicomTreeNode { private List fileNames; + private MRI mriInformation; + // constructor for JAXB public SerieTreeNode() { this.serie = new Serie(); @@ -274,21 +276,27 @@ public DicomTreeNode initChildTreeNode(Object arg0) { @XmlElement public MRI getMriInformation() { - MRI mriInformation = new MRI(); - InstitutionDicom institutionDicom = this.serie.getInstitution(); - if(institutionDicom != null) { - mriInformation.setInstitutionName(institutionDicom.getInstitutionName()); - mriInformation.setInstitutionAddress(institutionDicom.getInstitutionAddress()); - } - EquipmentDicom equipmentDicom = this.serie.getEquipment(); - if(equipmentDicom != null) { - mriInformation.setManufacturer(equipmentDicom.getManufacturer()); - mriInformation.setManufacturersModelName(equipmentDicom.getManufacturerModelName()); - mriInformation.setDeviceSerialNumber(equipmentDicom.getDeviceSerialNumber()); - mriInformation.setStationName(equipmentDicom.getStationName()); - mriInformation.setMagneticFieldStrength(equipmentDicom.getMagneticFieldStrength()); + if (this.mriInformation == null) { + this.mriInformation = new MRI(); + InstitutionDicom institutionDicom = this.serie.getInstitution(); + if(institutionDicom != null) { + this.mriInformation.setInstitutionName(institutionDicom.getInstitutionName()); + this.mriInformation.setInstitutionAddress(institutionDicom.getInstitutionAddress()); + } + EquipmentDicom equipmentDicom = this.serie.getEquipment(); + if(equipmentDicom != null) { + this.mriInformation.setManufacturer(equipmentDicom.getManufacturer()); + this.mriInformation.setManufacturersModelName(equipmentDicom.getManufacturerModelName()); + this.mriInformation.setDeviceSerialNumber(equipmentDicom.getDeviceSerialNumber()); + this.mriInformation.setStationName(equipmentDicom.getStationName()); + this.mriInformation.setMagneticFieldStrength(equipmentDicom.getMagneticFieldStrength()); + } } - return mriInformation; + return this.mriInformation; + } + + public void setMriInformation(MRI mriInformation) { + this.mriInformation = mriInformation; } @Override From f4226cce24360306446830b6e0d110bc552c7f3e Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 11:33:55 +0200 Subject: [PATCH 20/32] Fix for ShUp: do not pseudonymize a second time, code more clearer now --- .../ng/importer/ImporterManagerService.java | 51 ++++++++++--------- ...gesCreatorAndDicomFileAnalyzerService.java | 4 +- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java index 9d5a0c3310..cdc7d3f736 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java @@ -148,29 +148,9 @@ public void manageImportJob(final ImportJob importJob) { for (Iterator patientsIt = patients.iterator(); patientsIt.hasNext();) { Patient patient = patientsIt.next(); - // perform anonymization only in case of profile explicitly set - if (importJob.getAnonymisationProfileToUse() == null || !importJob.getAnonymisationProfileToUse().isEmpty()) { - String anonymizationProfile = (String) this.rabbitTemplate.convertSendAndReceive(RabbitMQConfiguration.STUDY_ANONYMISATION_PROFILE_QUEUE, importJob.getStudyId()); - importJob.setAnonymisationProfileToUse(anonymizationProfile); - } - ArrayList dicomFiles = getDicomFilesForPatient(importJob, patient, importJobDir.getAbsolutePath()); - final Subject subject = patient.getSubject(); - - if (subject == null) { - LOG.error("Error: subject == null in importJob."); - throw new ShanoirException("Error: subject == null in importJob."); - } - - final String subjectName = subject.getName(); - - event.setMessage("Pseudonymizing DICOM files for subject [" + subjectName + "]..."); - eventService.publishEvent(event); - - try { - ANONYMIZER.anonymizeForShanoir(dicomFiles, importJob.getAnonymisationProfileToUse(), subjectName, subjectName); - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new ShanoirException("Error during pseudonymization."); + // DICOM file coming from ShUp are already pseudonymized + if (!importJob.isFromShanoirUploader()) { + pseudonymize(importJob, event, importJobDir, patient); } Long converterId = importJob.getConverterId(); datasetsCreatorAndNIfTIConverter.createDatasetsAndRunConversion(patient, importJobDir, converterId, importJob); @@ -188,6 +168,29 @@ public void manageImportJob(final ImportJob importJob) { } } + private void pseudonymize(final ImportJob importJob, ShanoirEvent event, final File importJobDir, Patient patient) + throws FileNotFoundException, ShanoirException { + if (importJob.getAnonymisationProfileToUse() == null || !importJob.getAnonymisationProfileToUse().isEmpty()) { + String anonymizationProfile = (String) this.rabbitTemplate.convertSendAndReceive(RabbitMQConfiguration.STUDY_ANONYMISATION_PROFILE_QUEUE, importJob.getStudyId()); + importJob.setAnonymisationProfileToUse(anonymizationProfile); + } + ArrayList dicomFiles = getDicomFilesForPatient(importJob, patient, importJobDir.getAbsolutePath()); + final Subject subject = patient.getSubject(); + if (subject == null) { + LOG.error("Error: subject == null in importJob."); + throw new ShanoirException("Error: subject == null in importJob."); + } + final String subjectName = subject.getName(); + event.setMessage("Pseudonymizing DICOM files for subject [" + subjectName + "]..."); + eventService.publishEvent(event); + try { + ANONYMIZER.anonymizeForShanoir(dicomFiles, importJob.getAnonymisationProfileToUse(), subjectName, subjectName); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new ShanoirException("Error during pseudonymization."); + } + } + private void sendFailureMail(ImportJob importJob, String errorMessage) { EmailDatasetImportFailed generatedMail = new EmailDatasetImportFailed(); generatedMail.setExaminationId(importJob.getExaminationId().toString()); @@ -195,9 +198,7 @@ private void sendFailureMail(ImportJob importJob, String errorMessage) { generatedMail.setSubjectName(importJob.getSubjectName()); generatedMail.setStudyName(importJob.getStudyName()); generatedMail.setUserId(importJob.getUserId()); - generatedMail.setErrorMessage(errorMessage != null ? errorMessage : "An unexpected error occured, please contact Shanoir support."); - sendMail(importJob, generatedMail); } diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java index 3d25e17822..0549c1be25 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java @@ -157,7 +157,7 @@ private void filterAndCreateImages(String folderFileAbsolutePath, Serie serie, b for (Iterator instancesIt = instances.iterator(); instancesIt.hasNext();) { Instance instance = instancesIt.next(); File instanceFile = getFileFromInstance(instance, serie, folderFileAbsolutePath, isImportFromPACS); - processOneDicomFileForAllInstances(instanceFile, images, folderFileAbsolutePath); + processDicomFilePerInstanceAndCreateImage(instanceFile, images, folderFileAbsolutePath); } serie.setNonImages(nonImages); serie.setNonImagesNumber(nonImages.size()); @@ -221,7 +221,7 @@ public File getFileFromInstance(Instance instance, Serie serie, String folderFil * @param nonImages * @param images */ - private void processOneDicomFileForAllInstances(File dicomFile, List images, String folderFileAbsolutePath) throws Exception { + private void processDicomFilePerInstanceAndCreateImage(File dicomFile, List images, String folderFileAbsolutePath) throws Exception { try (DicomInputStream dIS = new DicomInputStream(dicomFile)) { // keep try to finally close input stream Attributes attributes = dIS.readDataset(); // Some DICOM files with a particular SOPClassUID are ignored: such as Raw Data Storage etc. From f1d6c9554f49d1bb91d77ee259579f887c4433aa Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 11:34:19 +0200 Subject: [PATCH 21/32] Update ImporterManagerService.java --- .../java/org/shanoir/ng/importer/ImporterManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java index cdc7d3f736..c266a6d3e9 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java @@ -148,7 +148,7 @@ public void manageImportJob(final ImportJob importJob) { for (Iterator patientsIt = patients.iterator(); patientsIt.hasNext();) { Patient patient = patientsIt.next(); - // DICOM file coming from ShUp are already pseudonymized + // DICOM files coming from ShUp are already pseudonymized if (!importJob.isFromShanoirUploader()) { pseudonymize(importJob, event, importJobDir, patient); } From 53fadf48a9b61ee47ff55f3c6da154c654862130 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 11:48:09 +0200 Subject: [PATCH 22/32] Fixes for erroneous series during import --- .../ng/importer/ImporterManagerService.java | 10 ++-- ...tasetsCreatorAndNIfTIConverterService.java | 47 +++++++++---------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java index c266a6d3e9..200358732e 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java @@ -311,8 +311,8 @@ private void downloadAndMoveDicomFilesToImportJobDir(final File importJobDir, Li /** * Using Java HashSet here to avoid duplicate files for Pseudonymization. - * For performance reasons already init with 5000 buckets, assuming, - * that we will normally never have more than 5000 files to process. + * For performance reasons already init with 10000 buckets, assuming, + * that we will normally never have more than 10000 files to process. * Maybe to be evaluated later with more bigger imports. * * @param importJob @@ -322,14 +322,16 @@ private void downloadAndMoveDicomFilesToImportJobDir(final File importJobDir, Li * @throws FileNotFoundException */ private ArrayList getDicomFilesForPatient(final ImportJob importJob, final Patient patient, final String workFolderPath) throws FileNotFoundException { - Set pathsSet = new HashSet<>(5000); + Set pathsSet = new HashSet<>(10000); List studies = patient.getStudies(); for (Iterator studiesIt = studies.iterator(); studiesIt.hasNext();) { Study study = studiesIt.next(); List series = study.getSeries(); for (Iterator seriesIt = series.iterator(); seriesIt.hasNext();) { Serie serie = seriesIt.next(); - handleSerie(workFolderPath, pathsSet, serie); + if (!serie.isErroneous() && serie.getSelected() && !serie.isIgnored()) { + handleSerie(workFolderPath, pathsSet, serie); + } } } return new ArrayList<>(pathsSet); diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java index 2c8940a472..d5026f7fb6 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java @@ -149,36 +149,35 @@ public void createDatasetsAndRunConversion(Patient patient, File workFolder, Lon Study study = studiesIt.next(); List series = study.getSelectedSeries(); float progress = 0; - int nbSeries = series.size(); int cpt = 1; - for (Iterator seriesIt = series.iterator(); seriesIt.hasNext();) { Serie serie = seriesIt.next(); - - progress = progress + (0.5f / series.size()); - importJob.getShanoirEvent().setProgress(progress); - importJob.getShanoirEvent().setMessage("Converting to NIfTI for serie [" + (serie.getProtocolName() == null ? serie.getSeriesInstanceUID() : serie.getProtocolName()) + "] (" + cpt + "/" + nbSeries + ")..."); - shanoirEventService.publishEvent(importJob.getShanoirEvent()); - - File serieIDFolderFile = createSerieIDFolderAndMoveFiles(workFolder, seriesFolderFile, serie); - boolean serieIdentifiedForNotSeparating; - try { - serieIdentifiedForNotSeparating = checkSerieForPropertiesString(serie, seriesProperties); - // if the serie is not one of the series, that should not be separated, please separate the series, - // otherwise just do not separate the series and keep all images for one nii conversion - serie.setDatasets(new ArrayList()); - constructDicom(serieIDFolderFile, serie, serieIdentifiedForNotSeparating); - // we exclude MR Spectroscopy (MRS) from NIfTI conversion, see MRS on GitHub Wiki - if (serie.getIsSpectroscopy() != null && !serie.getIsSpectroscopy()) { - constructNifti(serieIDFolderFile, serie, converterId); + // do not convert an erroneous serie + if (!serie.isErroneous() && !serie.isIgnored()) { + progress = progress + (0.5f / series.size()); + importJob.getShanoirEvent().setProgress(progress); + importJob.getShanoirEvent().setMessage("Converting to NIfTI for serie [" + (serie.getProtocolName() == null ? serie.getSeriesInstanceUID() : serie.getProtocolName()) + "] (" + cpt + "/" + nbSeries + ")..."); + shanoirEventService.publishEvent(importJob.getShanoirEvent()); + File serieIDFolderFile = createSerieIDFolderAndMoveFiles(workFolder, seriesFolderFile, serie); + boolean serieIdentifiedForNotSeparating; + try { + serieIdentifiedForNotSeparating = checkSerieForPropertiesString(serie, seriesProperties); + // if the serie is not one of the series, that should not be separated, please separate the series, + // otherwise just do not separate the series and keep all images for one nii conversion + serie.setDatasets(new ArrayList()); + constructDicom(serieIDFolderFile, serie, serieIdentifiedForNotSeparating); + // we exclude MR Spectroscopy (MRS) from NIfTI conversion, see MRS on GitHub Wiki + if (serie.getIsSpectroscopy() != null && !serie.getIsSpectroscopy()) { + constructNifti(serieIDFolderFile, serie, converterId); + } + } catch (NoSuchFieldException | SecurityException e) { + LOG.error(e.getMessage()); } - } catch (NoSuchFieldException | SecurityException e) { - LOG.error(e.getMessage()); + // as images/non-images are migrated to datasets, clear the list now + serie.getImages().clear(); + serie.getNonImages().clear(); } - // as images/non-images are migrated to datasets, clear the list now - serie.getImages().clear(); - serie.getNonImages().clear(); cpt++; } } From bbbb12404256d04179509eea44c3acc035a688a4 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 11:48:44 +0200 Subject: [PATCH 23/32] Update DatasetsCreatorAndNIfTIConverterService.java --- .../dcm2nii/DatasetsCreatorAndNIfTIConverterService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java index d5026f7fb6..ad436371a6 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java @@ -36,13 +36,10 @@ import java.util.Set; import java.util.stream.Collectors; -import jakarta.transaction.Transactional; - import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.io.filefilter.RegexFileFilter; -import org.apache.poi.ss.formula.eval.NotImplementedException; import org.shanoir.ng.importer.model.Dataset; import org.shanoir.ng.importer.model.DatasetFile; import org.shanoir.ng.importer.model.DiffusionGradient; @@ -69,6 +66,8 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; +import jakarta.transaction.Transactional; + /** * The NIfTIConverter does the actual conversion of dcm to nii files. * To use the converter the dcm files have to be put in separate folders. From 2dbb0e7612ce97e49b61887466ccd56343cab1d7 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 14:37:24 +0200 Subject: [PATCH 24/32] Remove rabbitmq call for pseudo profile from ImporterManagerService (to prepare before on job) --- .../ng/importer/ImporterManagerService.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java index 200358732e..0783c081fd 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java @@ -171,23 +171,21 @@ public void manageImportJob(final ImportJob importJob) { private void pseudonymize(final ImportJob importJob, ShanoirEvent event, final File importJobDir, Patient patient) throws FileNotFoundException, ShanoirException { if (importJob.getAnonymisationProfileToUse() == null || !importJob.getAnonymisationProfileToUse().isEmpty()) { - String anonymizationProfile = (String) this.rabbitTemplate.convertSendAndReceive(RabbitMQConfiguration.STUDY_ANONYMISATION_PROFILE_QUEUE, importJob.getStudyId()); - importJob.setAnonymisationProfileToUse(anonymizationProfile); - } - ArrayList dicomFiles = getDicomFilesForPatient(importJob, patient, importJobDir.getAbsolutePath()); - final Subject subject = patient.getSubject(); - if (subject == null) { - LOG.error("Error: subject == null in importJob."); - throw new ShanoirException("Error: subject == null in importJob."); - } - final String subjectName = subject.getName(); - event.setMessage("Pseudonymizing DICOM files for subject [" + subjectName + "]..."); - eventService.publishEvent(event); - try { - ANONYMIZER.anonymizeForShanoir(dicomFiles, importJob.getAnonymisationProfileToUse(), subjectName, subjectName); - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new ShanoirException("Error during pseudonymization."); + ArrayList dicomFiles = getDicomFilesForPatient(importJob, patient, importJobDir.getAbsolutePath()); + final Subject subject = patient.getSubject(); + if (subject == null) { + LOG.error("Error: subject == null in importJob."); + throw new ShanoirException("Error: subject == null in importJob."); + } + final String subjectName = subject.getName(); + event.setMessage("Pseudonymizing DICOM files for subject [" + subjectName + "]..."); + eventService.publishEvent(event); + try { + ANONYMIZER.anonymizeForShanoir(dicomFiles, importJob.getAnonymisationProfileToUse(), subjectName, subjectName); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new ShanoirException("Error during pseudonymization."); + } } } From 86d5849a6137896431e2597bcb54c7fefa2fe144 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 15:00:29 +0200 Subject: [PATCH 25/32] More doc + code clean up --- .../ng/importer/ImporterApiController.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java index 01585a3d2f..ca0c23c297 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java @@ -75,8 +75,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; @@ -87,7 +85,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; import com.fasterxml.jackson.databind.ObjectMapper; @@ -145,9 +142,6 @@ public class ImporterApiController implements ImporterApi { @Value("${shanoir.import.directory}") private String importDir; - @Autowired - private RestTemplate restTemplate; - @Autowired private DicomDirGeneratorService dicomDirGeneratorService; @@ -279,6 +273,15 @@ public ResponseEntity startImportJob( } } + /** + * cleanSeries is important here for import-from-zip file: when the ImagesCreatorAndDicomFileAnalyzer + * has declared some series as e.g. erroneous, we have to remove them from the import. For import-from + * pacs or from-sh-up it is different, as the ImagesCreatorAndDicomFileAnalyzer is called afterwards. + * Same here for multi-exam-imports: it calls uploadDicomZipFile method, where series could be classed + * as erroneous and when startImportJob is called, we want them to be removed from the import. + * + * @param importJob + */ private void cleanSeries(final ImportJob importJob) { for (Iterator patientIt = importJob.getPatients().iterator(); patientIt.hasNext();) { Patient patient = patientIt.next(); @@ -309,7 +312,6 @@ public ResponseEntity queryPACS( importJob.setWorkFolder(""); importJob.setFromPacs(true); importJob.setUserId(KeycloakUtil.getTokenUserId()); - } catch (ShanoirException e) { throw new RestServiceException( new ErrorModel(HttpStatus.UNPROCESSABLE_ENTITY.value(), e.getMessage(), null)); @@ -332,12 +334,11 @@ public ResponseEntity importDicomZipFile( MockMultipartFile multiPartFile; try { multiPartFile = new MockMultipartFile(tempFile.getName(), tempFile.getName(), APPLICATION_ZIP, new FileInputStream(tempFile.getAbsolutePath())); - // Import dicomfile return uploadDicomZipFile(multiPartFile); } catch (IOException e) { LOG.error("ERROR while loading zip fiole, please contact an administrator"); - e.printStackTrace(); + LOG.error(e.getMessage(), e); return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE); } finally { // Delete temp file which is useless now @@ -374,12 +375,9 @@ public ResponseEntity uploadEEGZipFile( if (!userImportDir.exists()) { userImportDir.mkdirs(); // create if not yet existing } - // Unzip the file and get the elements File tempFile = ImportUtils.saveTempFile(userImportDir, eegFile); - File importJobDir = ImportUtils.saveTempFileCreateFolderAndUnzip(tempFile, eegFile, false); - EegImportJob importJob = new EegImportJob(); importJob.setUserId(userId); importJob.setArchive(eegFile.getOriginalFilename()); From 1280e83767145725adc7bceb6f125248a0245f65 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 15:07:13 +0200 Subject: [PATCH 26/32] Much better: now clean series at the right place --- .../ng/importer/ImporterApiController.java | 29 --------------- .../ng/importer/ImporterManagerService.java | 36 ++++++++++++++++--- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java index ca0c23c297..2747cbfd2f 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterApiController.java @@ -29,7 +29,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.regex.Matcher; @@ -262,7 +261,6 @@ public ResponseEntity startImportJob( final File importJobDir = new File(userImportDir, tempDirId); if (importJobDir.exists()) { importJob.setWorkFolder(importJobDir.getAbsolutePath()); - cleanSeries(importJob); LOG.info("Starting import job for user {} (userId: {}) with import job folder: {}", KeycloakUtil.getTokenUserName(), userId, importJob.getWorkFolder()); importerManagerService.manageImportJob(importJob); return new ResponseEntity<>(HttpStatus.OK); @@ -273,33 +271,6 @@ public ResponseEntity startImportJob( } } - /** - * cleanSeries is important here for import-from-zip file: when the ImagesCreatorAndDicomFileAnalyzer - * has declared some series as e.g. erroneous, we have to remove them from the import. For import-from - * pacs or from-sh-up it is different, as the ImagesCreatorAndDicomFileAnalyzer is called afterwards. - * Same here for multi-exam-imports: it calls uploadDicomZipFile method, where series could be classed - * as erroneous and when startImportJob is called, we want them to be removed from the import. - * - * @param importJob - */ - private void cleanSeries(final ImportJob importJob) { - for (Iterator patientIt = importJob.getPatients().iterator(); patientIt.hasNext();) { - Patient patient = patientIt.next(); - List studies = patient.getStudies(); - for (Iterator studyIt = studies.iterator(); studyIt.hasNext();) { - Study study = studyIt.next(); - List series = study.getSeries(); - for (Iterator serieIt = series.iterator(); serieIt.hasNext();) { - Serie serie = serieIt.next(); - if (serie.isIgnored() || serie.isErroneous() || !serie.getSelected()) { - LOG.info("Serie {} cleaned from import (ignored, erroneous, not selected).", serie.getSeriesDescription()); - serieIt.remove(); - } - } - } - } - } - @Override public ResponseEntity queryPACS( @Parameter(name = "DicomQuery", required = true) @Valid @RequestBody final DicomQuery dicomQuery) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java index 0783c081fd..60f8dc02cc 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/ImporterManagerService.java @@ -142,6 +142,9 @@ public void manageImportJob(final ImportJob importJob) { } else { throw new ShanoirException("Unsupported type of import."); } + // we do the clean series here: at this point we are sure for all imports, that the ImagesCreatorAndDicomFileAnalyzer + // has been run and correctly classified everything. So no need to check afterwards for erroneous series. + cleanSeries(importJob); event.setProgress(0.25F); eventService.publishEvent(event); @@ -168,6 +171,33 @@ public void manageImportJob(final ImportJob importJob) { } } + /** + * cleanSeries is important here for import-from-zip file: when the ImagesCreatorAndDicomFileAnalyzer + * has declared some series as e.g. erroneous, we have to remove them from the import. For import-from + * pacs or from-sh-up it is different, as the ImagesCreatorAndDicomFileAnalyzer is called afterwards. + * Same here for multi-exam-imports: it calls uploadDicomZipFile method, where series could be classed + * as erroneous and when startImportJob is called, we want them to be removed from the import. + * + * @param importJob + */ + private void cleanSeries(final ImportJob importJob) { + for (Iterator patientIt = importJob.getPatients().iterator(); patientIt.hasNext();) { + Patient patient = patientIt.next(); + List studies = patient.getStudies(); + for (Iterator studyIt = studies.iterator(); studyIt.hasNext();) { + Study study = studyIt.next(); + List series = study.getSeries(); + for (Iterator serieIt = series.iterator(); serieIt.hasNext();) { + Serie serie = serieIt.next(); + if (serie.isIgnored() || serie.isErroneous() || !serie.getSelected()) { + LOG.info("Serie {} cleaned from import (ignored, erroneous, not selected).", serie.getSeriesDescription()); + serieIt.remove(); + } + } + } + } + } + private void pseudonymize(final ImportJob importJob, ShanoirEvent event, final File importJobDir, Patient patient) throws FileNotFoundException, ShanoirException { if (importJob.getAnonymisationProfileToUse() == null || !importJob.getAnonymisationProfileToUse().isEmpty()) { @@ -207,7 +237,6 @@ private void sendFailureMail(ImportJob importJob, String errorMessage) { */ private void sendMail(ImportJob job, EmailBase email) { List recipients = new ArrayList<>(); - // Get all recpients List users = (List) studyUserRightRepo.findByStudyId(job.getStudyId()); for (StudyUser user : users) { @@ -220,7 +249,6 @@ private void sendMail(ImportJob job, EmailBase email) { return; } email.setRecipients(recipients); - try { rabbitTemplate.convertAndSend(RabbitMQConfiguration.IMPORT_DATASET_FAILED_MAIL_QUEUE, objectMapper.writeValueAsString(email)); } catch (AmqpException | JsonProcessingException e) { @@ -327,9 +355,7 @@ private ArrayList getDicomFilesForPatient(final ImportJob importJob, final List series = study.getSeries(); for (Iterator seriesIt = series.iterator(); seriesIt.hasNext();) { Serie serie = seriesIt.next(); - if (!serie.isErroneous() && serie.getSelected() && !serie.isIgnored()) { - handleSerie(workFolderPath, pathsSet, serie); - } + handleSerie(workFolderPath, pathsSet, serie); } } return new ArrayList<>(pathsSet); From f05f38c4de2d1c5715864b15018dba806b37b7f0 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 15:17:20 +0200 Subject: [PATCH 27/32] Avoid NPE from serie.getFirstDatasetFileForCurrentSerie() --- .../src/main/java/org/shanoir/ng/importer/dto/Serie.java | 9 +++++---- .../org/shanoir/ng/importer/service/ImporterService.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java index 86b6159ae4..8447d4c985 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java @@ -245,10 +245,11 @@ public void setSequenceName(String sequenceName) { public DatasetFile getFirstDatasetFileForCurrentSerie() { if (getDatasets() == null - || getDatasets().get(0) == null - || getDatasets().get(0).getExpressionFormats() == null - || getDatasets().get(0).getExpressionFormats().get(0) == null - || getDatasets().get(0).getExpressionFormats().get(0).getDatasetFiles() == null) { + || getDatasets().get(0) == null + || getDatasets().get(0).getExpressionFormats() == null + || getDatasets().get(0).getExpressionFormats().get(0) == null + || getDatasets().get(0).getExpressionFormats().get(0).getDatasetFiles() == null + || getDatasets().get(0).getExpressionFormats().get(0).getDatasetFiles().get(0) == null) { return null; } return getDatasets().get(0).getExpressionFormats().get(0).getDatasetFiles().get(0); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java index 93db909824..2f38a45451 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java @@ -221,7 +221,7 @@ private Set generateAcquisitions(Examination examination, Im try { dicomAttributes = dicomProcessing.getDicomObjectAttributes(serie.getFirstDatasetFileForCurrentSerie(), serie.getIsEnhanced()); } catch (IOException e) { - throw new ShanoirException("Unable to retrieve dicom attributes in file " + serie.getFirstDatasetFileForCurrentSerie().getPath(), e); + throw new ShanoirException("Unable to retrieve dicom attributes in serie: " + serie.getSeriesDescription(), e); } // Generate acquisition object with all sub objects : datasets, protocols, expressions, ... From dd322bb0ea3900ec5b30bfbe2ae37cc09a4b7073 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 15:26:22 +0200 Subject: [PATCH 28/32] Update query-pacs.component.html --- .../src/app/import/query-pacs/query-pacs.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shanoir-ng-front/src/app/import/query-pacs/query-pacs.component.html b/shanoir-ng-front/src/app/import/query-pacs/query-pacs.component.html index b0bc8e6ec0..0470f0d26e 100644 --- a/shanoir-ng-front/src/app/import/query-pacs/query-pacs.component.html +++ b/shanoir-ng-front/src/app/import/query-pacs/query-pacs.component.html @@ -14,7 +14,7 @@
-
1. Query Neurinfo PACS
+
1. Query PACS
    @@ -69,4 +69,4 @@
-
\ No newline at end of file + From 73fef5e665a7af464a9472b16bce0d1e93eb908b Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 15:30:55 +0200 Subject: [PATCH 29/32] Refactor --- .../ng/importer/dicom/query/QueryPACSService.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index e230933417..5c5fa791f1 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -42,7 +42,6 @@ import org.dcm4che3.net.pdu.AAssociateRQ; import org.dcm4che3.net.pdu.PresentationContext; import org.dcm4che3.net.service.QueryRetrieveLevel; -import org.dcm4che3.tool.findscu.FindSCU.InformationModel; import org.shanoir.ng.importer.dicom.DicomSerieAndInstanceAnalyzer; import org.shanoir.ng.importer.dicom.InstanceNumberSorter; import org.shanoir.ng.importer.dicom.SeriesNumberSorter; @@ -56,8 +55,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.weasis.core.api.util.FileUtil; -import org.weasis.dicom.op.CFind; import org.weasis.dicom.op.CMove; import org.weasis.dicom.param.AdvancedParams; import org.weasis.dicom.param.DicomNode; @@ -65,7 +62,6 @@ import org.weasis.dicom.param.DicomProgress; import org.weasis.dicom.param.DicomState; import org.weasis.dicom.param.ProgressListener; -import org.weasis.dicom.util.ServiceUtil; import jakarta.annotation.PostConstruct; @@ -162,13 +158,7 @@ private void initAssociation(DicomNode calling, DicomNode called) { UID.StudyRootQueryRetrieveInformationModelFind, UID.ImplicitVRLittleEndian)); this.association = callingAE.connect(calledConn, aarq); LOG.info("initAssociation finished between calling {} and called {}", calling.getAet(), called.getAet()); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - } catch (InterruptedException e) { - LOG.error(e.getMessage(), e); - } catch (IncompatibleConnectionException e) { - LOG.error(e.getMessage(), e); - } catch (GeneralSecurityException e) { + } catch (IOException | InterruptedException | IncompatibleConnectionException | GeneralSecurityException e) { LOG.error(e.getMessage(), e); } } From 137ca681d2eba563f2b96e29489096353451c299 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 15:45:48 +0200 Subject: [PATCH 30/32] Probably better version of association usage (avoid timeouts) --- .../dicom/query/QueryPACSService.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index 5c5fa791f1..8340eb8f21 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -112,7 +112,6 @@ private void initDicomNodes() { this.called = new DicomNode(calledName, calledHost, calledPort); LOG.info("Query: DicomNodes initialized via CDI: calling ({}, {}, {}) and called ({}, {}, {})", callingName, callingHost, callingPort, calledName, calledHost, calledPort); - initAssociation(calling, called); } /** @@ -129,10 +128,9 @@ public void setDicomNodes(DicomNode calling, DicomNode called, String calledName this.maxPatientsFromPACS = 10; LOG.info("Query: DicomNodes initialized via method call (ShUp): calling ({}, {}, {}) and called ({}, {}, {})", calling.getAet(), calling.getHostname(), calling.getPort(), called.getAet(), called.getHostname(), called.getPort()); - initAssociation(calling, called); } - private void initAssociation(DicomNode calling, DicomNode called) { + private void connectAssociation(DicomNode calling, DicomNode called) { ExecutorService executor = Executors.newSingleThreadExecutor(); ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); try { @@ -160,10 +158,11 @@ private void initAssociation(DicomNode calling, DicomNode called) { LOG.info("initAssociation finished between calling {} and called {}", calling.getAet(), called.getAet()); } catch (IOException | InterruptedException | IncompatibleConnectionException | GeneralSecurityException e) { LOG.error(e.getMessage(), e); - } + } } public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException { + connectAssociation(calling, called); ImportJob importJob = new ImportJob(); /** * In case of any patient specific search field is filled, work on patient level. Highest priority. @@ -191,9 +190,21 @@ public ImportJob queryCFIND(DicomQuery dicomQuery) throws ShanoirImportException } else { throw new ShanoirImportException("DicomQuery: missing parameters."); } + releaseAssociation(); return importJob; } + private void releaseAssociation() { + try { + this.association.release(); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + ExecutorService executorService = (ExecutorService) this.association.getDevice().getExecutor(); + executorService.shutdown(); + this.association.getDevice().getScheduledExecutor().shutdown(); + } + public void queryCMOVE(Serie serie) { queryCMOVE(serie.getSeriesInstanceUID()); } @@ -214,6 +225,7 @@ public void handleProgression(DicomProgress progress) { } public boolean queryECHO(String calledAET, String hostName, int port, String callingAET) { + connectAssociation(calling, called); LOG.info("DICOM ECHO: Starting with configuration {}, {}, {} <- {}", calledAET, hostName, port, callingAET); try { this.association.cecho(); @@ -224,6 +236,7 @@ public boolean queryECHO(String calledAET, String hostName, int port, String cal LOG.error(e.getMessage(), e); return false; } + releaseAssociation(); return true; } @@ -483,7 +496,7 @@ public void onDimseRSP(Association as, Attributes cmd, Attributes data) { return state.getDicomRSP(); } - public void addAttributes(Attributes attrs, DicomParam param) { + private void addAttributes(Attributes attrs, DicomParam param) { int tag = param.getTag(); String[] ss = param.getValues(); VR vr = ElementDictionary.vrOf(tag, attrs.getPrivateCreator(tag)); From 955148fecafb568f0746b993e1c6f9de8ec96a61 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 17:10:27 +0200 Subject: [PATCH 31/32] Better log --- .../org/shanoir/ng/importer/dicom/query/QueryPACSService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java index 8340eb8f21..d81580eff1 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/query/QueryPACSService.java @@ -155,7 +155,7 @@ private void connectAssociation(DicomNode calling, DicomNode called) { aarq.addPresentationContext(new PresentationContext(3, UID.StudyRootQueryRetrieveInformationModelFind, UID.ImplicitVRLittleEndian)); this.association = callingAE.connect(calledConn, aarq); - LOG.info("initAssociation finished between calling {} and called {}", calling.getAet(), called.getAet()); + LOG.info("connectAssociation finished between calling {} and called {}", calling.getAet(), called.getAet()); } catch (IOException | InterruptedException | IncompatibleConnectionException | GeneralSecurityException e) { LOG.error(e.getMessage(), e); } @@ -203,6 +203,7 @@ private void releaseAssociation() { ExecutorService executorService = (ExecutorService) this.association.getDevice().getExecutor(); executorService.shutdown(); this.association.getDevice().getScheduledExecutor().shutdown(); + LOG.info("releaseAssociation finished between calling {} and called {}", calling.getAet(), called.getAet()); } public void queryCMOVE(Serie serie) { From 1a87c3daa1ed43e423d3be6922a4fa89bdf1b779 Mon Sep 17 00:00:00 2001 From: michaelkain Date: Fri, 20 Oct 2023 17:27:50 +0200 Subject: [PATCH 32/32] Update .gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 3c81f317e2..dbd12221d2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,7 @@ docker-compose/boutiques/boutiques shanoir-ng-preclinical/null* shanoir-uploader/src/main/resources/key shanoir-uploader/src/main/resources/profile.OFSEP/key -shanoir-uploader/src/main/resources/profile.OFSEP-qualif/key -shanoir-uploader/src/main/resources/profile.OFSEP-NG-qualif/key -shanoir-uploader/src/main/resources/profile.OFSEP-NG/key +shanoir-uploader/src/main/resources/profile.OFSEP-Qualif/key shanoir-uploader/src/main/resources/pseudonymus shanoir-uploader/src/test/resources/acr_phantom_t1/ shanoir-ng-tests/tests/geckodriver.log