Skip to content

Commit

Permalink
#1 XDS registry conformance Tests
Browse files Browse the repository at this point in the history
* Fix first observations in context of xdstools execution
* adapt default port to 8081 to avoid conflict with xdstools
  • Loading branch information
Thopap committed Oct 31, 2023
1 parent 8786713 commit 0100df2
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 15 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,15 +26,22 @@ public class MappingSupport {

public static final Map<Precision, TemporalPrecisionEnum> 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));
public static final Map<TemporalPrecisionEnum, Precision> PRECISION_MAP_FROM_FHIR = PRECISION_MAP_FROM_XDS
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, (x, y) -> y, LinkedHashMap::new));

public static final Map<AvailabilityStatus, DocumentReferenceStatus> STATUS_MAPPING_FROM_XDS = new EnumMap<>(
Map.of(AvailabilityStatus.APPROVED, DocumentReferenceStatus.CURRENT,
AvailabilityStatus.DEPRECATED, DocumentReferenceStatus.SUPERSEDED));
public static final Map<DocumentReferenceStatus, AvailabilityStatus> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -31,6 +33,7 @@ public class StoredQueryProcessor implements Iti18Service {
private int maxResultCount;
private final IGenericClient client;
private final Function<DocumentReference, DocumentEntry> documentMapper;
private static final Version DEFAULT_VERSION = new Version("1");

@Override
public QueryResponse processQuery(QueryRegistry query) {
Expand Down Expand Up @@ -71,7 +74,20 @@ private List<DocumentEntry> 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<? super DocumentEntry> assignDefaultVersioning() {
return doc -> {
doc.setLogicalUuid(doc.getEntryUuid());
doc.setVersion(DEFAULT_VERSION);
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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
Expand All @@ -86,11 +94,11 @@ public void visit(GetDocumentsQuery query) {
}

private void map(List<Code> 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])));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -24,6 +29,7 @@

@Component
@RequiredArgsConstructor
@Slf4j
public class RegisterDocumentsProcessor implements Iti42Service {
private final IGenericClient client;
private final Function<DocumentEntry, DocumentReference> documentMapper;
Expand All @@ -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
Expand All @@ -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())) {
Expand All @@ -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<DocumentEntry> assignPatientId() {
Expand Down
5 changes: 4 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
xds.endpoint.iti8=xds-iti8:0.0.0.0:2575

server.port=8081
1 change: 1 addition & 0 deletions src/test/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<logger name="org.springframework.web" level="WARN"/>
<logger name="org.apache.cxf" level="INFO"/>
<logger name="ca.uhn.fhir.rest.client.interceptor" level="DEBUG" />
<logger name="org.openehealth.app.xdstofhir.registry" level="DEBUG" />
<root level="warn" />
</configuration>

0 comments on commit 0100df2

Please sign in to comment.