diff --git a/README.md b/README.md index fc4adcc..2631cc1 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,12 @@ Target goal is a blueprint project to see how [IPF](https://github.com/oehf/ipf) * Implementation try to stay as simple as possible to allow a blueprint (Design Principles DRY and KISS) ## Features -* [ITI-42](https://profiles.ihe.net/ITI/TF/Volume2/ITI-42.html) to register documents to the registry -* [ITI-18](https://profiles.ihe.net/ITI/TF/Volume2/ITI-18.html) to query documents from the registry -* [ITI-8](https://profiles.ihe.net/ITI/TF/Volume2/ITI-8.html) to receive a patient-identity-feed and make sure the patient exists +* [ITI-42](https://profiles.ihe.net/ITI/TF/Volume2/ITI-42.html) to register documents to the registry +(default endpoint: http://localhost:8081/services/registry/iti42) +* [ITI-18](https://profiles.ihe.net/ITI/TF/Volume2/ITI-18.html) to query documents from the registry +(default endpoint: http://localhost:8081/services/registry/iti18) +* [ITI-8](https://profiles.ihe.net/ITI/TF/Volume2/ITI-8.html) to receive a patient-identity-feed and make sure the patient exists +(default endpoint: MLLP Port 2575) ## Build and run diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/common/MappingSupport.java b/src/main/java/org/openehealth/app/xdstofhir/registry/common/MappingSupport.java index 6139ac0..9550772 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/common/MappingSupport.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/common/MappingSupport.java @@ -9,9 +9,11 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import lombok.experimental.UtilityClass; +import org.hl7.fhir.r4.model.codesystems.DocumentReferenceStatus; import org.ietf.jgss.GSSException; import org.ietf.jgss.Oid; import org.openehealth.ipf.commons.core.URN; +import org.openehealth.ipf.commons.ihe.xds.core.metadata.AvailabilityStatus; import org.openehealth.ipf.commons.ihe.xds.core.metadata.Timestamp.Precision; @UtilityClass @@ -24,8 +26,8 @@ public class MappingSupport { public static final Map PRECISION_MAP_FROM_XDS = new EnumMap<>( Map.of(Precision.DAY, TemporalPrecisionEnum.DAY, - Precision.HOUR, TemporalPrecisionEnum.MINUTE, - Precision.MINUTE, TemporalPrecisionEnum.MINUTE, + Precision.HOUR, TemporalPrecisionEnum.SECOND, + Precision.MINUTE, TemporalPrecisionEnum.SECOND, Precision.MONTH, TemporalPrecisionEnum.MONTH, Precision.SECOND, TemporalPrecisionEnum.SECOND, Precision.YEAR, TemporalPrecisionEnum.YEAR)); @@ -33,6 +35,13 @@ public class MappingSupport { .entrySet().stream() .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, (x, y) -> y, LinkedHashMap::new)); + public static final Map STATUS_MAPPING_FROM_XDS = new EnumMap<>( + Map.of(AvailabilityStatus.APPROVED, DocumentReferenceStatus.CURRENT, + AvailabilityStatus.DEPRECATED, DocumentReferenceStatus.SUPERSEDED)); + public static final Map STATUS_MAPPING_FROM_FHIR = STATUS_MAPPING_FROM_XDS + .entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, (x, y) -> y, LinkedHashMap::new)); + public static String toUrnCoded(String value) { String adaptedValue = value; diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/common/mapper/XdsToFhirDocumentMapper.java b/src/main/java/org/openehealth/app/xdstofhir/registry/common/mapper/XdsToFhirDocumentMapper.java index 0871b8b..79fa101 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/common/mapper/XdsToFhirDocumentMapper.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/common/mapper/XdsToFhirDocumentMapper.java @@ -160,8 +160,10 @@ private Reference fromAuthor(final Author author) { var role = new PractitionerRole(); var doc = new Practitioner(); if(!author.getAuthorPerson().isEmpty()) { - doc.setName(singletonList(fromName(author.getAuthorPerson().getName()))); - doc.addIdentifier(fromIdentifier(author.getAuthorPerson().getId())); + if (!author.getAuthorPerson().getName().isEmpty()) + doc.setName(singletonList(fromName(author.getAuthorPerson().getName()))); + if (!author.getAuthorPerson().getId().isEmpty()) + doc.addIdentifier(fromIdentifier(author.getAuthorPerson().getId())); } doc.setTelecom(author.getAuthorTelecom().stream().map(this::fromTelecom).collect(Collectors.toList())); var reference = new Reference(); diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryProcessor.java b/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryProcessor.java index 01034c1..1d4df7c 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryProcessor.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/query/StoredQueryProcessor.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -14,6 +15,7 @@ import org.hl7.fhir.r4.model.DocumentReference; import org.openehealth.ipf.commons.ihe.xds.core.metadata.DocumentEntry; import org.openehealth.ipf.commons.ihe.xds.core.metadata.ObjectReference; +import org.openehealth.ipf.commons.ihe.xds.core.metadata.Version; import org.openehealth.ipf.commons.ihe.xds.core.requests.QueryRegistry; import org.openehealth.ipf.commons.ihe.xds.core.requests.query.QueryReturnType; import org.openehealth.ipf.commons.ihe.xds.core.responses.ErrorCode; @@ -31,6 +33,7 @@ public class StoredQueryProcessor implements Iti18Service { private int maxResultCount; private final IGenericClient client; private final Function documentMapper; + private static final Version DEFAULT_VERSION = new Version("1"); @Override public QueryResponse processQuery(QueryRegistry query) { @@ -71,7 +74,20 @@ private List getDocumentsFrom(Bundle resultBundle) { .map(documentMapper) .filter(Objects::nonNull) .collect(Collectors.toList()); + xdsDocuments.forEach(assignDefaultVersioning()); return xdsDocuments; } + /** + * ebRIM chapter 2.5.1 requires versionInfo and lid to be set. + * + * @return consumer setting proper defaults for lid and versionInfo + */ + private Consumer assignDefaultVersioning() { + return doc -> { + doc.setLogicalUuid(doc.getEntryUuid()); + doc.setVersion(DEFAULT_VERSION); + }; + } + } 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 a2e1a40..3c70208 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 @@ -13,8 +13,10 @@ import ca.uhn.fhir.rest.gclient.TokenClientParam; import lombok.Getter; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DocumentReference; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.codesystems.DocumentReferenceStatus; import org.openehealth.app.xdstofhir.registry.common.MappingSupport; import org.openehealth.ipf.commons.ihe.xds.core.metadata.Code; import org.openehealth.ipf.commons.ihe.xds.core.requests.query.FetchQuery; @@ -69,6 +71,12 @@ public void visit(FindDocumentsQuery query) { map(query.getPracticeSettingCodes(),DocumentReference.SETTING); map(query.getHealthcareFacilityTypeCodes(),DocumentReference.FACILITY); map(query.getFormatCodes(),DocumentReference.FORMAT); + List fhirStatus = query.getStatus().stream() + .map(status -> MappingSupport.STATUS_MAPPING_FROM_XDS.get(status)) + .map(DocumentReferenceStatus::toCode) + .collect(Collectors.toList()); + if (!fhirStatus.isEmpty()) + fhirQuery.where(DocumentReference.STATUS.exactly().codes(fhirStatus)); } @Override @@ -86,11 +94,11 @@ public void visit(GetDocumentsQuery query) { } private void map(List codes, TokenClientParam param) { - if (codes != null) { - codes.forEach(code -> { - var codeCriteria = param.exactly().systemAndCode(toUrnCoded(code.getSchemeName()), code.getCode()); - fhirQuery.where(codeCriteria); - }); + if (codes != null && !codes.isEmpty()) { + fhirQuery.where(param.exactly() + .codings(codes.stream() + .map(xdsCode -> new Coding(toUrnCoded(xdsCode.getSchemeName()), xdsCode.getCode(), null)) + .collect(Collectors.toList()).toArray(new Coding[0]))); } } diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/register/RegisterDocumentsProcessor.java b/src/main/java/org/openehealth/app/xdstofhir/registry/register/RegisterDocumentsProcessor.java index 733a23c..e083a39 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/register/RegisterDocumentsProcessor.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/register/RegisterDocumentsProcessor.java @@ -1,6 +1,7 @@ package org.openehealth.app.xdstofhir.registry.register; import static org.openehealth.app.xdstofhir.registry.common.MappingSupport.OID_URN; +import static org.openehealth.app.xdstofhir.registry.common.MappingSupport.URI_URN; import java.util.function.Consumer; import java.util.function.Function; @@ -9,11 +10,15 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.util.BundleBuilder; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.DocumentReference; +import org.hl7.fhir.r4.model.Enumerations.DocumentReferenceStatus; import org.hl7.fhir.r4.model.Patient; import org.openehealth.app.xdstofhir.registry.common.MappingSupport; import org.openehealth.app.xdstofhir.registry.common.RegistryConfiguration; +import org.openehealth.ipf.commons.ihe.xds.core.metadata.AssociationType; +import org.openehealth.ipf.commons.ihe.xds.core.metadata.AvailabilityStatus; import org.openehealth.ipf.commons.ihe.xds.core.metadata.DocumentEntry; import org.openehealth.ipf.commons.ihe.xds.core.requests.RegisterDocumentSet; import org.openehealth.ipf.commons.ihe.xds.core.responses.Response; @@ -24,6 +29,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class RegisterDocumentsProcessor implements Iti42Service { private final IGenericClient client; private final Function documentMapper; @@ -34,8 +40,14 @@ public Response processRegister(RegisterDocumentSet register) { BundleBuilder builder = new BundleBuilder(client.getFhirContext()); validateKnownRepository(register); - register.getDocumentEntries().forEach(this::assignEntryUuid); + register.getDocumentEntries().forEach(this::assignRegistryValues); register.getDocumentEntries().forEach(assignPatientId()); + register.getAssociations().stream().filter(assoc -> assoc.getAssociationType() == AssociationType.REPLACE) + .forEach(assoc -> builder.addTransactionUpdateEntry(replacePreviousDocument(assoc.getTargetUuid(), + register.getDocumentEntries().stream() + .filter(doc -> doc.getEntryUuid().equals(assoc.getSourceUuid())).findFirst().map(documentMapper) + .orElseThrow(() -> new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE, + assoc.getSourceUuid()))))); register.getDocumentEntries().forEach(doc -> builder.addTransactionCreateEntry(documentMapper.apply(doc))); // Execute the transaction @@ -44,6 +56,33 @@ public Response processRegister(RegisterDocumentSet register) { return new Response(Status.SUCCESS); } + /** + * Perform replace according to https://profiles.ihe.net/ITI/TF/Volume2/ITI-42.html#3.42.4.1.3.5 + * + * @param entryUuid + * @param replacingDocument + * @return Replaced document with status set to superseded + */ + private DocumentReference replacePreviousDocument(String entryUuid, DocumentReference replacingDocument) { + var result = client.search().forResource(DocumentReference.class).count(1) + .where(DocumentReference.IDENTIFIER.exactly().systemAndValues(URI_URN, entryUuid)) + .returnBundle(Bundle.class).execute(); + if (result.getEntry().isEmpty()) { + throw new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE, entryUuid); + } + var replacedDocument = (DocumentReference)result.getEntryFirstRep().getResource(); + if (replacedDocument.getStatus() != DocumentReferenceStatus.CURRENT) { + throw new XDSMetaDataException(ValidationMessage.DEPRECATED_OBJ_CANNOT_BE_TRANSFORMED); + } + if (!replacedDocument.getSubject().getReference().equals(replacingDocument.getSubject().getReference())) { + log.debug("Replacing and replaced document do not have the same patientid {} and {}", + replacedDocument.getSubject().getReference(), replacingDocument.getSubject().getReference()); + throw new XDSMetaDataException(ValidationMessage.DOC_ENTRY_PATIENT_ID_WRONG); + } + replacedDocument.setStatus(DocumentReferenceStatus.SUPERSEDED); + return replacedDocument; + } + private void validateKnownRepository(RegisterDocumentSet register) { register.getDocumentEntries().forEach(doc -> { if (!registryConfig.getRepositoryEndpoint().containsKey(doc.getRepositoryUniqueId())) { @@ -52,10 +91,11 @@ private void validateKnownRepository(RegisterDocumentSet register) { }); } - private void assignEntryUuid(DocumentEntry doc) { + private void assignRegistryValues(DocumentEntry doc) { if (!doc.getEntryUuid().startsWith(MappingSupport.UUID_URN)) { doc.assignEntryUuid(); } + doc.setAvailabilityStatus(AvailabilityStatus.APPROVED); } private Consumer assignPatientId() { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6c62e07..3a5f6f0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,6 +6,7 @@ fhir.server.base=http://hapi.fhir.org/baseR4 # List of repositories in syntax xds.repositoryEndpoint.REPOSITORY-uniqueid=Download_endpoint # Is used for mapping from xds to fhir and reverse. xds.repositoryEndpoint.1.2.3.4=http://my.doc.retrieve/binary/$documentUniqueId +xds.repositoryEndpoint.1.19.6.24.109.42.1=http://gazelle/binary/$documentUniqueId # Any document in fhir that can not be mapped will get a placeholder repository uniquieid xds.unknownRepositoryId=2.999.1.2.3 @@ -14,4 +15,6 @@ xds.defaultHash=0000000000000000000000000000000000000000 xds.endpoint.iti18=xds-iti18:registry/iti18 xds.endpoint.iti42=xds-iti42:registry/iti42 -xds.endpoint.iti8=xds-iti8:0.0.0.0:2575 \ No newline at end of file +xds.endpoint.iti8=xds-iti8:0.0.0.0:2575 + +server.port=8081 \ No newline at end of file diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index ecff8bc..98cd2ca 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -4,5 +4,6 @@ + \ No newline at end of file