Skip to content

Commit

Permalink
#1 Resolve some missing IHE validations, fix folder support for existing
Browse files Browse the repository at this point in the history
docs and / or folders
  • Loading branch information
Thopap committed Dec 23, 2023
1 parent d30cbac commit 95a57d4
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.openehealth.app.xdstofhir.registry.common.mapper;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

Expand All @@ -13,7 +13,6 @@
import org.openehealth.app.xdstofhir.registry.common.MappingSupport;
import org.openehealth.app.xdstofhir.registry.common.fhir.MhdFolder;
import org.openehealth.ipf.commons.ihe.xds.core.metadata.Folder;
import org.openehealth.ipf.commons.ihe.xds.core.metadata.Timestamp;
import org.springframework.stereotype.Component;

@Component
Expand All @@ -28,8 +27,7 @@ public MhdFolder apply(Folder xdFolder, List<ListEntryComponent> references) {
mhdList.addIdentifier(
fromIdentifier(MappingSupport.OID_URN + xdFolder.getUniqueId(), Identifier.IdentifierUse.USUAL));
mhdList.setSubject(patientReferenceFrom(xdFolder));
var lastUpdateTime = Objects.requireNonNullElse(xdFolder.getLastUpdateTime(), Timestamp.now());
mhdList.setDateElement(fromTimestamp(lastUpdateTime));
mhdList.setDate(new Date());
mhdList.setDesignationType(xdFolder.getCodeList().stream().map(this::fromCode).collect(Collectors.toList()));
if (xdFolder.getTitle() != null)
mhdList.setTitle(xdFolder.getTitle().getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,17 @@ public void visit(FindDocumentsByReferenceIdQuery query) {


private Iterable<MhdFolder> buildResultForFolder(IQuery<Bundle> folderFhirQuery) {
return () -> new PagingFhirResultIterator<MhdFolder>(folderFhirQuery.execute(), MhdFolder.class);
return () -> new PagingFhirResultIterator<MhdFolder>(folderFhirQuery.execute(), MhdFolder.class);
}

private Iterable<DocumentReference> buildResultForDocuments(IQuery<Bundle> documentFhirQuery) {
return () -> new PagingFhirResultIterator<DocumentReference>(documentFhirQuery.execute(), DocumentReference.class);
return () -> new PagingFhirResultIterator<DocumentReference>(documentFhirQuery.execute(),
DocumentReference.class);
}

private Iterable<MhdSubmissionSet> buildResultForSubmissionSet(IQuery<Bundle> submissionSetfhirQuery) {
return () -> new PagingFhirResultIterator<MhdSubmissionSet>(submissionSetfhirQuery.execute(), MhdSubmissionSet.class);
return () -> new PagingFhirResultIterator<MhdSubmissionSet>(submissionSetfhirQuery.execute(),
MhdSubmissionSet.class);
}

private IQuery<Bundle> prepareQuery(FindDocumentsQuery query) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@

@FunctionalInterface
public interface Iti42Service {

/**
* Perform the ITI-42 processing as described by IHE for https://profiles.ihe.net/ITI/TF/Volume2/ITI-42.html
*
* @param register - the Register message containing documents, submissionssets and folder metadata.
* @return Response with Success or Failure, depending on the operation outcome.
*/
public Response processRegister(RegisterDocumentSet register);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import static org.openehealth.app.xdstofhir.registry.common.MappingSupport.DOC_DOC_FHIR_ASSOCIATIONS;
import static org.openehealth.app.xdstofhir.registry.common.MappingSupport.OID_URN;
import static org.openehealth.app.xdstofhir.registry.common.MappingSupport.URI_URN;
import static org.openehealth.app.xdstofhir.registry.common.MappingSupport.UUID_URN;

import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand All @@ -18,18 +22,21 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.DocumentReference;
import org.hl7.fhir.r4.model.DocumentReference.DocumentReferenceRelatesToComponent;
import org.hl7.fhir.r4.model.Enumerations.DocumentReferenceStatus;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.ListResource;
import org.hl7.fhir.r4.model.ListResource.ListEntryComponent;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.openehealth.app.xdstofhir.registry.common.MappingSupport;
import org.openehealth.app.xdstofhir.registry.common.RegistryConfiguration;
import org.openehealth.app.xdstofhir.registry.common.fhir.MhdFolder;
import org.openehealth.app.xdstofhir.registry.common.fhir.MhdSubmissionSet;
import org.openehealth.ipf.commons.core.URN;
import org.openehealth.ipf.commons.ihe.xds.core.metadata.Association;
import org.openehealth.ipf.commons.ihe.xds.core.metadata.AssociationType;
import org.openehealth.ipf.commons.ihe.xds.core.metadata.AvailabilityStatus;
Expand All @@ -38,7 +45,10 @@
import org.openehealth.ipf.commons.ihe.xds.core.metadata.SubmissionSet;
import org.openehealth.ipf.commons.ihe.xds.core.metadata.XDSMetaClass;
import org.openehealth.ipf.commons.ihe.xds.core.requests.RegisterDocumentSet;
import org.openehealth.ipf.commons.ihe.xds.core.responses.ErrorCode;
import org.openehealth.ipf.commons.ihe.xds.core.responses.ErrorInfo;
import org.openehealth.ipf.commons.ihe.xds.core.responses.Response;
import org.openehealth.ipf.commons.ihe.xds.core.responses.Severity;
import org.openehealth.ipf.commons.ihe.xds.core.responses.Status;
import org.openehealth.ipf.commons.ihe.xds.core.validate.ValidationMessage;
import org.openehealth.ipf.commons.ihe.xds.core.validate.XDSMetaDataException;
Expand All @@ -58,6 +68,7 @@ public class RegisterDocumentsProcessor implements Iti42Service {
@Override
public Response processRegister(RegisterDocumentSet register) {
validateKnownRepository(register);
register.getDocumentEntries().forEach(this::validateResubmission);
register.getDocumentEntries().forEach(doc -> assignRegistryValues(doc, register.getAssociations()));
register.getDocumentEntries().forEach(this::assignPatientId);
register.getFolders().forEach(folder -> assignRegistryValues(folder, register.getAssociations()));
Expand All @@ -75,8 +86,16 @@ public Response processRegister(RegisterDocumentSet register) {
var documentMap = register.getDocumentEntries().stream()
.collect(Collectors.toMap(DocumentEntry::getEntryUuid, Function.identity()));

var folderReferences = createReferences(register.getAssociations(), documentMap,
register.getFolders().stream().map(XDSMetaClass::getEntryUuid).collect(Collectors.toList()));
var folderAssociations = register.getAssociations().stream()
.filter(assoc -> !assoc.getSourceUuid().equals(register.getSubmissionSet().getEntryUuid()))
.filter(assoc -> AssociationType.HAS_MEMBER.equals(assoc.getAssociationType()))
.collect(Collectors.toList());

var folderUuids = register.getFolders().stream().map(XDSMetaClass::getEntryUuid).collect(Collectors.toList());
var externalFolderUuid = folderAssociations.stream().map(Association::getSourceUuid).filter(folderId -> !folderUuids.contains(folderId)).collect(Collectors.toList());
createUpdateOfExistingFolders(externalFolderUuid, folderAssociations, documentMap).forEach(builder::addTransactionUpdateEntry);

var folderReferences = createReferences(folderAssociations, documentMap, folderUuids);
register.getFolders().forEach(folder -> builder.addTransactionCreateEntry(folderMapper.apply(folder, folderReferences)));

var submissionReferences = createReferences(register.getAssociations(), documentMap,
Expand All @@ -92,7 +111,107 @@ public Response processRegister(RegisterDocumentSet register) {
// Execute the transaction
client.transaction().withBundle(builder.getBundle()).execute();

return new Response(Status.SUCCESS);
var response = new Response(Status.SUCCESS);

addWarningForExtraMetadataIfPresent(register, response);

return response;
}

/**
* This registry implementation currently do not store extra-metadata. Notify with a client warning that no
* extra metadata is being stored.
*
* @param register
* @param response
*/
private void addWarningForExtraMetadataIfPresent(RegisterDocumentSet register, Response response) {
if (register.getDocumentEntries().stream().anyMatch(doc -> !doc.getExtraMetadata().isEmpty())) {
response.getErrors().add(new ErrorInfo(ErrorCode.EXTRA_METADATA_NOT_SAVED,
"Register do not yet support storing extra metadata", Severity.WARNING, null, null));
}
}

/**
* Validate the resubmission preconditions:
* - entryUUID shall not be used before (in case client use a UUID based id)
* - same uniqueid is only allowed if hash and size is the same as the existing document.
*
* @param doc
*/
private void validateResubmission(DocumentEntry doc) {
DocumentReference existingDoc;
try {
if (doc.getEntryUuid().startsWith(UUID_URN)) {
existingDoc = lookupExistingDocument(doc.getEntryUuid(), MappingSupport.toUrnCoded(doc.getUniqueId()));
} else {
existingDoc = lookupExistingDocument(MappingSupport.toUrnCoded(doc.getUniqueId()));
}
} catch (XDSMetaDataException notPresent) {
return;
}
if (existingDoc.getIdentifier().stream().filter(id -> id.getValue().equals(doc.getEntryUuid())).findAny().isPresent()) {
throw new XDSMetaDataException(ValidationMessage.UUID_NOT_UNIQUE);
}
if (!(doc.getHash().equals(existingDoc.getContentFirstRep().getAttachment().getHashElement().asStringValue()))) {
throw new XDSMetaDataException(ValidationMessage.DIFFERENT_HASH_CODE_IN_RESUBMISSION);
}
if (!(doc.getSize() == existingDoc.getContentFirstRep().getAttachment().getSize())) {
throw new XDSMetaDataException(ValidationMessage.DIFFERENT_SIZE_IN_RESUBMISSION);
}
}

/**
* Update an existing folder and add a link to a document.
*
* @param externalFolderUuid
* @param folderAssociations
* @param documentMap
* @return a list of folder objects that need to be updated.
*/
private List<MhdFolder> createUpdateOfExistingFolders(List<String> externalFolderUuid,
List<Association> folderAssociations, Map<String, DocumentEntry> documentMap) {
return folderAssociations.stream().filter(assoc -> externalFolderUuid.contains(assoc.getSourceUuid()))
.map(assoc -> {
var folder = lookupExistingFolder(assoc.getSourceUuid());
folder.setDate(new Date());
var documentEntry = documentMap.get(assoc.getTargetUuid());
if (documentEntry != null) {
if (!folder.getSubject().getIdentifier().getValue()
.equals(documentEntry.getPatientId().getId())) {
throw new XDSMetaDataException(ValidationMessage.FOLDER_PATIENT_ID_WRONG);
}
folder.addEntry(createReference(assoc, DocumentReference.class.getSimpleName()));
} else {
var existingDoc = lookupExistingDocument(assoc.getTargetUuid());
if (!folder.getSubject().getIdentifier().getValue()
.equals(existingDoc.getSubject().getIdentifier().getValue())) {
throw new XDSMetaDataException(ValidationMessage.FOLDER_PATIENT_ID_WRONG);
}
var ref = new ListEntryComponent(new Reference(existingDoc));
ref.setId(assoc.getEntryUuid());
folder.addEntry(ref);
folder.setDate(new Date());
}
return folder;
}).collect(Collectors.toList());
}

/**
* lookup an existing folder in the FHIR server. In case this folder do not exists, throw a XDS metadata exception to
* reject the transaction.
*
* @param entryUuid
* @return The folder associated with the given uuid.
*/
private MhdFolder lookupExistingFolder(String entryUuid) {
var result = client.search().forResource(MhdFolder.class).count(1)
.where(MhdFolder.IDENTIFIER.exactly().systemAndValues(URI_URN, entryUuid))
.returnBundle(Bundle.class).execute();
if (result.getEntry().isEmpty()) {
throw new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE, entryUuid);
}
return (MhdFolder)result.getEntryFirstRep().getResource();
}


Expand All @@ -113,13 +232,41 @@ private List<DocumentReferenceRelatesToComponent> createDocToDocReferences(List<

private void evaluateDocumentReplacement(RegisterDocumentSet register, BundleBuilder builder) {
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(doc -> documentMapper.apply(doc, emptyList()))
.orElseThrow(() -> new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE,
assoc.getSourceUuid())))));
.forEach(assoc -> {
var replacingDoc = register.getDocumentEntries().stream()
.filter(doc -> doc.getEntryUuid().equals(assoc.getSourceUuid()))
.findFirst().map(doc -> documentMapper.apply(doc, emptyList()))
.orElseThrow(() -> new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE,
assoc.getSourceUuid()));
var replacePreviousDocument = replacePreviousDocument(assoc.getTargetUuid(),
replacingDoc);
replaceFolderAssocations(replacePreviousDocument, replacingDoc).forEach(builder::addTransactionUpdateEntry);
builder.addTransactionUpdateEntry(replacePreviousDocument);
});
}

/**
* Takeover folder relationsship from replaced document to the new document.
*
* @param replacePreviousDocument
* @param replacingDoc
* @return Folders to update.
*/
private List<MhdFolder> replaceFolderAssocations(DocumentReference replacePreviousDocument,
DocumentReference replacingDoc) {
var folderResult = client.search().forResource(MhdFolder.class)
.withProfile(MappingSupport.MHD_COMPREHENSIVE_FOLDER_PROFILE)
.where(MhdFolder.CODE.exactly().codings(MhdFolder.FOLDER_CODEING.getCodingFirstRep()))
.where(MhdFolder.ITEM.hasId(replacePreviousDocument.getId())).returnBundle(Bundle.class).execute();
return folderResult.getEntry().stream().map(BundleEntryComponent::getResource).map(MhdFolder.class::cast)
.map(folder -> {
var ref = new ListEntryComponent(new Reference(replacingDoc));
ref.setId(new URN(UUID.randomUUID()).toString());
folder.setDate(new Date());
folder.addEntry(ref);
return folder;
}).collect(Collectors.toList());
}

/**
* Build the references for the given assocations, where the sourceId is ony of sourceId and the target is one of the docs from the
Expand All @@ -133,15 +280,20 @@ private void evaluateDocumentReplacement(RegisterDocumentSet register, BundleBui
private List<ListEntryComponent> createReferences(List<Association> associations, Map<String, ? extends XDSMetaClass> xdsObjectMap,
List<String> sourceId) {
return associations.stream()
.filter(assoc -> AssociationType.HAS_MEMBER.equals(assoc.getAssociationType()))
.filter(assoc -> sourceId.contains(assoc.getSourceUuid()))
.filter(assoc -> xdsObjectMap.containsKey(assoc.getTargetUuid()))
.map(assoc -> createReference(assoc, xdsObjectMap.get(assoc.getTargetUuid()).getEntryUuid()))
.map(assoc -> {
var metaClass = xdsObjectMap.get(assoc.getTargetUuid());
var refType = metaClass instanceof DocumentEntry ? DocumentReference.class.getSimpleName() : ListResource.class.getSimpleName();
return createReference(assoc, refType);
})
.collect(Collectors.toList());
}

private ListEntryComponent createReference(Association assoc, String targetEntryUuid) {
private ListEntryComponent createReference(Association assoc, String refType) {
var ref = new ListEntryComponent(new Reference(
new IdType(DocumentReference.class.getSimpleName(), targetEntryUuid)));
new IdType(refType, assoc.getTargetUuid())));
ref.setId(assoc.getEntryUuid());
return ref;
}
Expand Down Expand Up @@ -192,12 +344,13 @@ private DocumentReference replacePreviousDocument(String entryUuid, DocumentRefe
}


private DocumentReference lookupExistingDocument(String entryUuid) {
private DocumentReference lookupExistingDocument(String... ids) {
var result = client.search().forResource(DocumentReference.class).count(1)
.where(DocumentReference.IDENTIFIER.exactly().systemAndValues(URI_URN, entryUuid))
.where(DocumentReference.IDENTIFIER.exactly().systemAndValues(URI_URN, ids))
.cacheControl(new CacheControlDirective().setNoCache(true).setNoStore(true))
.returnBundle(Bundle.class).execute();
if (result.getEntry().isEmpty()) {
throw new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE, entryUuid);
throw new XDSMetaDataException(ValidationMessage.UNRESOLVED_REFERENCE, Arrays.toString(ids));
}
return (DocumentReference)result.getEntryFirstRep().getResource();
}
Expand All @@ -210,6 +363,12 @@ private void validateKnownRepository(RegisterDocumentSet register) {
});
}

/**
* Set entryUUID and availability Status.
*
* @param xdsObject
* @param associations
*/
private void assignRegistryValues(XDSMetaClass xdsObject, List<Association> associations) {
if (!xdsObject.getEntryUuid().startsWith(MappingSupport.UUID_URN)) {
var previousIdentifier = xdsObject.getEntryUuid();
Expand Down Expand Up @@ -240,6 +399,11 @@ private void assignRegistryValues(List<Association> associations) {
}
}

/**
* Assign the ID of the fhir patient resource to the xds object.
*
* @param xdsObject
*/
private void assignPatientId(XDSMetaClass xdsObject) {
var result = client.search().forResource(Patient.class).count(1)
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier(
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ fhir.server.base=http://hapi.fhir.org/baseR4
# 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
xds.repositoryEndpoint.1.1.1.1=http://gazelle/binary2/$documentUniqueId
xds.repositoryEndpoint.1.2.2.3.4.5=http://gazelle/binary3/$documentUniqueId
xds.repositoryEndpoint.1.2.456.786.3.2.4.56.1=http://gazelle/binary4/$documentUniqueId

# Any document in fhir that can not be mapped will get a placeholder repository uniquieid
xds.unknownRepositoryId=2.999.1.2.3
Expand Down
Loading

0 comments on commit 95a57d4

Please sign in to comment.