diff --git a/README.md b/README.md index 5561d2d..8ef4927 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ mvn clean spring-boot:run -Pboot ## Not yet implemented The application is not yet intended as a production ready application. -* Implemented profiles are not feature complete, e.g. ITI-18 only implement a some of the xds queries. * Security concerns are not yet covered (e.g. https, mllps, SAML, audit, ...) * More testing * IHE compliance test (using the [XDS Toolkit](https://github.com/usnistgov/iheos-toolkit2)) diff --git a/pom.xml b/pom.xml index 3ffe49e..39e91ab 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,11 @@ ${mockserver.version} test + + org.springframework.boot + spring-boot-configuration-processor + true + diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/XdsSpringContext.java b/src/main/java/org/openehealth/app/xdstofhir/registry/XdsSpringContext.java index 7252f3c..e298fd5 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/XdsSpringContext.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/XdsSpringContext.java @@ -12,6 +12,9 @@ import org.apache.cxf.BusFactory; import org.apache.cxf.bus.spring.SpringBus; import org.apache.cxf.ext.logging.LoggingFeature; +import org.openehealth.app.xdstofhir.registry.common.MappingSupport; +import org.openehealth.app.xdstofhir.registry.common.fhir.MhdFolder; +import org.openehealth.app.xdstofhir.registry.common.fhir.MhdSubmissionSet; import org.openehealth.ipf.commons.spring.map.config.CustomMappings; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Value; @@ -43,6 +46,8 @@ SpringBus springBus() { @Bean IGenericClient fhirClient(@Value("${fhir.server.base}") String fhirServerBase) { var ctx = FhirContext.forR4Cached(); + ctx.setDefaultTypeForProfile(MappingSupport.MHD_COMPREHENSIVE_FOLDER_PROFILE, MhdFolder.class); + ctx.setDefaultTypeForProfile(MappingSupport.MHD_COMPREHENSIVE_SUBMISSIONSET_PROFILE, MhdSubmissionSet.class); ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); var loggingInterceptor = new LoggingInterceptor(); loggingInterceptor.setLogRequestSummary(true); diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryMapper.java b/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryMapper.java index 4956264..4a0f549 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryMapper.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryMapper.java @@ -50,14 +50,14 @@ public static void map(TimeRange dateRange, DateClientParam date, IQuery } } - public static void map(List status, IQuery fhirQuery) { + public static void mapStatus(List status, TokenClientParam param, IQuery fhirQuery) { List fhirStatus = status.stream() .map(MappingSupport.STATUS_MAPPING_FROM_XDS::get) .filter(Objects::nonNull) .map(DocumentReferenceStatus::toCode) .toList(); if (!fhirStatus.isEmpty()) - fhirQuery.where(DocumentReference.STATUS.exactly().codes(fhirStatus)); + fhirQuery.where(param.exactly().codes(fhirStatus)); } public static void map (QueryList codes, TokenClientParam param, IQuery fhirQuery) { diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryVistorImpl.java b/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryVistorImpl.java index 5b1c0cb..0d8c93a 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryVistorImpl.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryVistorImpl.java @@ -6,6 +6,7 @@ import static org.openehealth.app.xdstofhir.registry.query.StoredQueryMapper.entryUuidFrom; import static org.openehealth.app.xdstofhir.registry.query.StoredQueryMapper.map; import static org.openehealth.app.xdstofhir.registry.query.StoredQueryMapper.mapPatientIdToQuery; +import static org.openehealth.app.xdstofhir.registry.query.StoredQueryMapper.mapStatus; import static org.openehealth.app.xdstofhir.registry.query.StoredQueryMapper.urnIdentifierList; import java.util.ArrayList; @@ -70,7 +71,7 @@ public class StoredQueryVistorImpl extends AbstractStoredQueryVisitor { * Hapi currently ignore "_list" parameter, workaround here with "_has" reverse chain search * https://github.com/hapifhir/hapi-fhir/issues/3761 */ - private static final String _HAS_LIST_ITEM_IDENTIFIER = "_has:List:item:identifier"; + private static final String HAS_LIST_ITEM_IDENTIFIER = "_has:List:item:identifier"; private final IGenericClient client; @@ -100,6 +101,8 @@ public void visit(GetDocumentsQuery query) { public void visit(FindFoldersQuery query) { var folderFhirQuery = initFolderQuery(); mapPatientIdToQuery(query, folderFhirQuery); + map(query.getLastUpdateTime(), ListResource.DATE, folderFhirQuery); + mapStatus(query.getStatus(),ListResource.STATUS, folderFhirQuery); mapFolders(buildResultForFolder(folderFhirQuery)); } @@ -134,7 +137,7 @@ public void visit(GetSubmissionSetAndContentsQuery query) { var folderFhirQuery = initFolderQuery(); var searchIdentifiers = urnIdentifierList(query); submissionSetfhirQuery.where(ListResource.IDENTIFIER.exactly().systemAndValues(URI_URN, searchIdentifiers)); - var reverseSearchCriteria = Collections.singletonMap(_HAS_LIST_ITEM_IDENTIFIER, searchIdentifiers); + var reverseSearchCriteria = Collections.singletonMap(HAS_LIST_ITEM_IDENTIFIER, searchIdentifiers); documentFhirQuery.whereMap(reverseSearchCriteria); folderFhirQuery.whereMap(reverseSearchCriteria); @@ -145,7 +148,7 @@ public void visit(GetSubmissionSetAndContentsQuery query) { mapAssociations(createAssociationsFrom(fhirSubmissions, fhirFolder)); var fdDocAssoc = createAssociationsFrom(fhirFolder, fhirDocuments); mapAssociations(fdDocAssoc); - mapAssociations(createAssociationsFrom(fhirSubmissions, fdDocAssoc)); + mapAssociations(createAssociationsBetween(fhirSubmissions, fdDocAssoc)); } @Override @@ -193,7 +196,7 @@ public void visit(GetFolderAndContentsQuery query) { var folderFhirQuery = initFolderQuery(); var searchIdentifiers = urnIdentifierList(query); folderFhirQuery.where(ListResource.IDENTIFIER.exactly().systemAndValues(URI_URN, searchIdentifiers)); - var reverseSearchCriteria = Collections.singletonMap(_HAS_LIST_ITEM_IDENTIFIER, searchIdentifiers); + var reverseSearchCriteria = Collections.singletonMap(HAS_LIST_ITEM_IDENTIFIER, searchIdentifiers); documentFhirQuery.whereMap(reverseSearchCriteria); var fhirDocuments = mapDocuments(buildResultForDocuments(documentFhirQuery)); var fhirFolder = mapFolders(buildResultForFolder(folderFhirQuery)); @@ -224,17 +227,42 @@ public void visit(GetDocumentsAndAssociationsQuery query) { @Override public void visit(GetAssociationsQuery query) { - throw new UnsupportedOperationException("Not yet implemented"); + var xdsAssocations = new ArrayList(); + + var documentFhirQuery = initDocumentQuery(); + documentFhirQuery.include(DocumentReference.INCLUDE_RELATESTO); + documentFhirQuery.revInclude(ListResource.INCLUDE_ITEM); + documentFhirQuery.where(DocumentReference.IDENTIFIER.exactly().systemAndValues(URI_URN, + query.getUuids())); + xdsAssocations.addAll(collectAssociationsOfDocument(documentFhirQuery)); + + var folderFhirQuery = initFolderQuery(); + folderFhirQuery.include(ListResource.INCLUDE_ITEM); + folderFhirQuery.where(ListResource.IDENTIFIER.exactly().systemAndValues(URI_URN, + query.getUuids())); + xdsAssocations.addAll(collectAssociationsOfFolders(folderFhirQuery)); + + var submissionSetfhirQuery = initSubmissionSetQuery(); + submissionSetfhirQuery.include(ListResource.INCLUDE_ITEM); + submissionSetfhirQuery.where(ListResource.IDENTIFIER.exactly().systemAndValues(URI_URN, + query.getUuids())); + xdsAssocations.addAll(collectAssociationsOfSubmissionSet(submissionSetfhirQuery)); + + mapAssociations(xdsAssocations); } + @Override public void visit(GetAllQuery query) { var documentFhirQuery = initDocumentQuery(); var submissionSetfhirQuery = initSubmissionSetQuery(); var folderFhirQuery = initFolderQuery(); mapPatientIdToQuery(query, documentFhirQuery); + mapStatus(query.getStatusDocuments(),DocumentReference.STATUS, documentFhirQuery); mapPatientIdToQuery(query, submissionSetfhirQuery); + mapStatus(query.getStatusSubmissionSets(),ListResource.STATUS, submissionSetfhirQuery); mapPatientIdToQuery(query, folderFhirQuery); + mapStatus(query.getStatusFolders(),ListResource.STATUS, folderFhirQuery); var fhirDocuments = mapDocuments(buildResultForDocuments(documentFhirQuery)); var fhirSubmissions = mapSubmissionSets(buildResultForSubmissionSet(submissionSetfhirQuery)); var fhirFolder = mapFolders(buildResultForFolder(folderFhirQuery)); @@ -242,7 +270,7 @@ public void visit(GetAllQuery query) { mapAssociations(createAssociationsFrom(fhirSubmissions, fhirFolder)); var fdDocAssoc = createAssociationsFrom(fhirFolder, fhirDocuments); mapAssociations(fdDocAssoc); - mapAssociations(createAssociationsFrom(fhirSubmissions, fdDocAssoc)); + mapAssociations(createAssociationsBetween(fhirSubmissions, fdDocAssoc)); mapAssociations(createAssociationsBetween(fhirDocuments)); } @@ -250,6 +278,8 @@ public void visit(GetAllQuery query) { public void visit(FindSubmissionSetsQuery query) { var submissionSetfhirQuery = initSubmissionSetQuery(); mapPatientIdToQuery(query, submissionSetfhirQuery); + map(query.getSubmissionTime(), ListResource.DATE, submissionSetfhirQuery); + mapStatus(query.getStatus(),ListResource.STATUS, submissionSetfhirQuery); if (query.getSourceIds() != null && !query.getSourceIds().isEmpty()) submissionSetfhirQuery.where(new TokenClientParam("sourceId").exactly().codes(query.getSourceIds())); mapSubmissionSets(buildResultForSubmissionSet(submissionSetfhirQuery)); @@ -276,16 +306,80 @@ private Iterable buildResultForFolder(IQuery folderFhirQuery) return () -> new PagingFhirResultIterator(folderFhirQuery.execute(), MhdFolder.class); } - private Iterable buildResultForDocuments(IQuery documentFhirQuery) { - return () -> new PagingFhirResultIterator(documentFhirQuery.execute(), + private Iterable buildResultForDocuments(Bundle documentSearchResult) { + return () -> new PagingFhirResultIterator(documentSearchResult, DocumentReference.class); } + private Iterable buildResultForDocuments(IQuery documentFhirQuery) { + return buildResultForDocuments(documentFhirQuery.execute()); + } + private Iterable buildResultForSubmissionSet(IQuery submissionSetfhirQuery) { return () -> new PagingFhirResultIterator(submissionSetfhirQuery.execute(), MhdSubmissionSet.class); } + private List collectAssociationsOfSubmissionSet( + IQuery submissionSetfhirQuery) { + var xdsAssocations = new ArrayList(); + var resultForSubmissionSet = buildResultForSubmissionSet(submissionSetfhirQuery); + resultForSubmissionSet.forEach(submission -> { + submission.getEntry().forEach(entry -> { + var assocEntryUuid = entry.getId() != null ? entry.getId() : new URN(UUID.randomUUID()).toString(); + if (entry.getItem().getResource() != null) { + xdsAssocations.add(new Association(AssociationType.HAS_MEMBER, assocEntryUuid, + entryUuidFrom(submission), entryUuidFrom(entry.getItem().getResource()))); + } else if (entry.getItem().getIdentifier() != null){ + xdsAssocations.add(new Association(AssociationType.HAS_MEMBER, assocEntryUuid, + entryUuidFrom(submission),entry.getItem().getIdentifier().getValue())); + } + }); + }); + return xdsAssocations; + } + + + private List collectAssociationsOfFolders(IQuery folderFhirQuery) { + var xdsAssocations = new ArrayList(); + var resultForFolder = buildResultForFolder(folderFhirQuery); + resultForFolder.forEach(folder -> { + folder.getEntry().forEach(entry -> { + if (entry.getItem().getResource() instanceof DocumentReference folderDoc) { + var assocEntryUuid = entry.getId() != null ? entry.getId() : new URN(UUID.randomUUID()).toString(); + xdsAssocations.add(new Association(AssociationType.HAS_MEMBER, assocEntryUuid, + entryUuidFrom(folder), entryUuidFrom(folderDoc))); + } + }); + }); + return xdsAssocations; + } + + + private List collectAssociationsOfDocument(IQuery documentFhirQuery) { + var xdsAssocations = new ArrayList(); + Bundle docResultBundle = documentFhirQuery.execute(); + var resultForDocuments = buildResultForDocuments(docResultBundle); + + resultForDocuments.forEach(doc -> { + var listResources = BundleUtil.toListOfResourcesOfType(client.getFhirContext(), docResultBundle, ListResource.class); + listResources.stream().filter(list -> list.getEntry().stream().anyMatch(entry -> doc.equals(entry.getItem().getResource()))).forEach(submission -> { + xdsAssocations.add(new Association(AssociationType.HAS_MEMBER, new URN(UUID.randomUUID()).toString(), + entryUuidFrom(submission), entryUuidFrom(doc))); + }); + doc.getRelatesTo().forEach(related -> { + if (related.getTarget().getResource() instanceof DocumentReference relatedDoc) { + var assocEntryUuid = related.getId() != null ? related.getId() + : new URN(UUID.randomUUID()).toString(); + var type = MappingSupport.DOC_DOC_XDS_ASSOCIATIONS.get(related.getCode()); + xdsAssocations + .add(new Association(type, assocEntryUuid, entryUuidFrom(doc), entryUuidFrom(relatedDoc))); + } + }); + }); + return xdsAssocations; + } + private IQuery prepareQuery(FindDocumentsQuery query) { IQuery documentFhirQuery = initDocumentQuery(); mapPatientIdToQuery(query, documentFhirQuery); @@ -295,7 +389,7 @@ private IQuery prepareQuery(FindDocumentsQuery query) { map(query.getPracticeSettingCodes(),DocumentReference.SETTING, documentFhirQuery); map(query.getHealthcareFacilityTypeCodes(),DocumentReference.FACILITY, documentFhirQuery); map(query.getFormatCodes(),DocumentReference.FORMAT, documentFhirQuery); - map(query.getStatus(), documentFhirQuery); + mapStatus(query.getStatus(),DocumentReference.STATUS, documentFhirQuery); map(query.getEventCodes(), DocumentReference.EVENT, documentFhirQuery); map(query.getConfidentialityCodes(), DocumentReference.SECURITY_LABEL, documentFhirQuery); map(query.getCreationTime(), DocumentReference.DATE, documentFhirQuery); @@ -397,7 +491,7 @@ private Collection createAssociationsBetween(List createAssociationsFrom(List fhirSubmissions, + private Collection createAssociationsBetween(List fhirSubmissions, Collection fdDocAssoc) { var xdsAssocations = new ArrayList(); for (var list : fhirSubmissions) {