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 487f1da..b770354 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 @@ -29,6 +29,7 @@ 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.Identifier.IdentifierUse; import org.hl7.fhir.r4.model.ListResource; import org.hl7.fhir.r4.model.ListResource.ListEntryComponent; import org.hl7.fhir.r4.model.Patient; @@ -297,8 +298,14 @@ private List createReferences(List associations } private ListEntryComponent createReference(Association assoc, String refType) { - var ref = new ListEntryComponent(new Reference( - new IdType(refType, assoc.getTargetUuid()))); + Reference item = new Reference( + new IdType(refType, assoc.getTargetUuid())); + var id = new Identifier(); + id.setSystem(MappingSupport.URI_URN); + id.setValue(assoc.getEntryUuid()); + id.setUse(IdentifierUse.SECONDARY); + item.setIdentifier(id); + var ref = new ListEntryComponent(item); ref.setId(assoc.getEntryUuid()); return ref; } @@ -316,7 +323,7 @@ private List createReferences(List associations private ListEntryComponent createReference(Association assoc) { var ref = new Reference(); - Identifier id = new Identifier(); + var id = new Identifier(); id.setSystem(MappingSupport.URI_URN); id.setValue(assoc.getTargetUuid()); ref.setIdentifier(id); diff --git a/src/main/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessor.java b/src/main/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessor.java index 323e324..827bdd8 100644 --- a/src/main/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessor.java +++ b/src/main/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessor.java @@ -4,18 +4,23 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.TreeSet; +import java.util.stream.Collectors; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.BundleUtil; import lombok.RequiredArgsConstructor; import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseElement; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.DocumentReference; import org.hl7.fhir.r4.model.ListResource; import org.openehealth.app.xdstofhir.registry.common.MappingSupport; import org.openehealth.app.xdstofhir.registry.common.PagingFhirResultIterator; -import org.openehealth.app.xdstofhir.registry.common.fhir.MhdSubmissionSet; import org.openehealth.app.xdstofhir.registry.query.StoredQueryMapper; import org.openehealth.ipf.commons.ihe.xds.core.metadata.ObjectReference; import org.openehealth.ipf.commons.ihe.xds.core.requests.RemoveMetadata; @@ -33,40 +38,68 @@ public class RemoveDocumentsProcessor implements Iti62Service { @Override public Response remove(RemoveMetadata metadataToRemove) { + var errorInfo = new ArrayList(); var uuidsToDelete = new ArrayList(metadataToRemove.getReferences().stream().map(ObjectReference::getId).toList()); var builder = new BundleBuilder(client.getFhirContext()); var documentFhirQuery = client.search().forResource(DocumentReference.class) .withProfile(MappingSupport.MHD_COMPREHENSIVE_PROFILE) + .revInclude(ListResource.INCLUDE_ITEM) .where(DocumentReference.IDENTIFIER.exactly().systemAndValues(URI_URN,uuidsToDelete)) .returnBundle(Bundle.class); - var docResult = new PagingFhirResultIterator(documentFhirQuery.execute(), - DocumentReference.class, client); + var uniqueResults = new TreeSet((a, b) -> Comparator.comparing(IAnyResource::getId).compare(a, b)); + + var docBundleResult = documentFhirQuery.execute(); + var docResult = new PagingFhirResultIterator(docBundleResult, DocumentReference.class, client); + uniqueResults.addAll(BundleUtil.toListOfResourcesOfType(client.getFhirContext(), docBundleResult, ListResource.class)); docResult.forEachRemaining(doc -> { - checkAssociations(doc, uuidsToDelete, builder); + processAssociations(doc, doc.getRelatesTo(), uuidsToDelete, builder); addToDeleteTransaction(doc, uuidsToDelete, builder); }); + var reverseSearchCriteria = Collections.singletonMap("item:identifier", Collections.singletonList( + uuidsToDelete.stream().map(MappingSupport::toUrnCoded).map(urnCoded -> URI_URN + "|" + urnCoded).collect(Collectors.joining(",")))); + var referenceQuery = client.search().forResource(ListResource.class) + .whereMap(reverseSearchCriteria) + .revInclude(ListResource.INCLUDE_ITEM) + .returnBundle(Bundle.class); - var submissionSetFhirQuery = client.search().forResource(MhdSubmissionSet.class) - .withProfile(MappingSupport.MHD_COMPREHENSIVE_SUBMISSIONSET_PROFILE) - .where(ListResource.CODE.exactly().codings(MhdSubmissionSet.SUBMISSIONSET_CODEING.getCodingFirstRep())) + var listQuery = client.search().forResource(ListResource.class) .where(ListResource.IDENTIFIER.exactly().systemAndValues(URI_URN,uuidsToDelete)) + .revInclude(ListResource.INCLUDE_ITEM) .returnBundle(Bundle.class); - var submissionSetResults = new PagingFhirResultIterator(submissionSetFhirQuery.execute(), - MhdSubmissionSet.class, client); - submissionSetResults.forEachRemaining(sub -> { - checkAssociations(sub, uuidsToDelete, builder); - addToDeleteTransaction(sub, uuidsToDelete, builder); + new PagingFhirResultIterator(referenceQuery.execute(), + ListResource.class, client).forEachRemaining(uniqueResults::add); + + new PagingFhirResultIterator(listQuery.execute(), + ListResource.class, client).forEachRemaining(uniqueResults::add); + + uniqueResults.forEach(ref -> { + processAssociations(ref, ref.getEntry(), uuidsToDelete, builder); + }); + + uniqueResults.forEach(ref -> { + boolean toDeleteTransaction = addToDeleteTransaction(ref, uuidsToDelete, builder); + if (toDeleteTransaction && !ref.getEntry().isEmpty()) { + errorInfo.add(new ErrorInfo(ErrorCode.REFERENCE_EXISTS_EXCEPTION, "Some references still exists to " + ref.getId(), + Severity.ERROR, null, null)); + } else if (!toDeleteTransaction && ref.getEntry().isEmpty() + && ref.getMeta().hasProfile(MappingSupport.MHD_COMPREHENSIVE_SUBMISSIONSET_PROFILE)) { + errorInfo.add(new ErrorInfo(ErrorCode.UNREFERENCED_OBJECT_EXCEPTION, + "SubmissionSet without references not permitted " + ref.getId(), Severity.ERROR, null, null)); + } }); - final Response response; if (!uuidsToDelete.isEmpty()) { + errorInfo.add(new ErrorInfo(ErrorCode.UNRESOLVED_REFERENCE_EXCEPTION, + "Some references can not be resolved " + String.join(",", uuidsToDelete), Severity.ERROR, null, null)); + } + final Response response; + if (!errorInfo.isEmpty()) { response = new Response(Status.FAILURE); - response.setErrors(Collections.singletonList(new ErrorInfo(ErrorCode.UNRESOLVED_REFERENCE_EXCEPTION, - "Result exceed maximum of " + String.join(",", uuidsToDelete), Severity.ERROR, null, null))); + response.setErrors(errorInfo); } else { client.transaction().withBundle(builder.getBundle()).execute(); response = new Response(Status.SUCCESS); @@ -75,34 +108,36 @@ public Response remove(RemoveMetadata metadataToRemove) { return response; } - private void checkAssociations(MhdSubmissionSet sub, ArrayList uuidsToDelete, BundleBuilder builder) { - sub.getEntry().stream().filter(rel -> uuidsToDelete.contains(rel.getId())).findAny().ifPresent(rel -> { - uuidsToDelete.remove(rel.getId()); - var entryUuid = StoredQueryMapper.entryUuidFrom(sub); - if (!uuidsToDelete.contains(entryUuid)) { - sub.getEntry().remove(rel); - builder.addTransactionUpdateEntry(sub); - } - }); - } - - private void checkAssociations(DocumentReference doc, ArrayList uuidsToDelete, BundleBuilder builder) { - doc.getRelatesTo().stream().filter(rel -> uuidsToDelete.contains(rel.getId())).findAny().ifPresent(rel -> { + /** + * Remove metadata will also remove associations between registry entries. This method will ensure that these + * entries are correctly removed. + * + * @param resource + * @param associatedObjects + * @param uuidsToDelete + * @param builder + */ + private void processAssociations(IAnyResource resource, List associatedObjects, + List uuidsToDelete, BundleBuilder builder) { + final var deletedElements = new ArrayList(); + if (associatedObjects.stream().filter(Objects::nonNull).filter(rel -> uuidsToDelete.contains(rel.getId())).map(rel -> { uuidsToDelete.remove(rel.getId()); - var entryUuid = StoredQueryMapper.entryUuidFrom(doc); - if (!uuidsToDelete.contains(entryUuid)) { - doc.getRelatesTo().remove(rel); - builder.addTransactionUpdateEntry(doc); - } - }); + deletedElements.add(rel); + var entryUuid = StoredQueryMapper.entryUuidFrom(resource); + return !uuidsToDelete.contains(entryUuid); + }).filter(updateRequired -> updateRequired == true).count() > 0) + builder.addTransactionUpdateEntry(resource); + associatedObjects.removeAll(deletedElements); } - private void addToDeleteTransaction(IAnyResource resource, List uuidsToDelete, BundleBuilder builder) { + private boolean addToDeleteTransaction(IAnyResource resource, List uuidsToDelete, BundleBuilder builder) { var entryUuid = StoredQueryMapper.entryUuidFrom(resource); if (uuidsToDelete.contains(entryUuid)) { builder.addTransactionDeleteEntry(resource); uuidsToDelete.remove(entryUuid); + return true; } + return false; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index eda3615..c5a8e3e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,8 +2,8 @@ spring.application.name=XDS-TO-FHIR Adapter #fhir.server.base=https://demo.kodjin.com/fhir # successfully tested -#fhir.server.base=https://server.fire.ly -fhir.server.base=http://hapi.fhir.org/baseR4 +fhir.server.base=https://server.fire.ly +#fhir.server.base=http://hapi.fhir.org/baseR4 # # List of repositories in syntax xds.repositoryEndpoint.REPOSITORY-uniqueid=Download_endpoint diff --git a/src/test/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessorTest.java b/src/test/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessorTest.java new file mode 100644 index 0000000..7a37221 --- /dev/null +++ b/src/test/java/org/openehealth/app/xdstofhir/registry/remove/RemoveDocumentsProcessorTest.java @@ -0,0 +1,48 @@ +package org.openehealth.app.xdstofhir.registry.remove; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +import org.junit.jupiter.api.Test; +import org.mockserver.model.MediaType; +import org.openehealth.app.xdstofhir.registry.AbstractFhirMockserver; +import org.openehealth.app.xdstofhir.registry.common.MappingSupport; +import org.openehealth.ipf.commons.ihe.xds.core.SampleData; +import org.openehealth.ipf.commons.ihe.xds.core.responses.ErrorCode; +import org.openehealth.ipf.commons.ihe.xds.core.responses.Status; + +class RemoveDocumentsProcessorTest extends AbstractFhirMockserver { + private RemoveDocumentsProcessor classUnderTest; + + + @Override + protected void initClassUnderTest() { + classUnderTest = new RemoveDocumentsProcessor(newRestfulGenericClient); + } + + + @Test + void removeForNotPresent() { + mockServer.when(request()).respond(response().withStatusCode(200) + .withContentType(MediaType.APPLICATION_JSON).withBody(EMPTY_BUNDLE_RESULT)); + + var removeRemove = SampleData.createRemoveMetadata(); + var response = classUnderTest.remove(removeRemove); + assertEquals(Status.FAILURE, response.getStatus()); + assertEquals(1, response.getErrors().size()); + assertEquals(ErrorCode.UNRESOLVED_REFERENCE_EXCEPTION, response.getErrors().get(0).getErrorCode()); + + mockServer.verify(request("/DocumentReference") + .withQueryStringParameter("identifier", "urn:ietf:rfc:3986|urn:uuid:b2632452-1de7-480d-94b1-c2074d79c871,urn:ietf:rfc:3986|urn:uuid:b2632df2-1de7-480d-1045-c2074d79aabd") + .withQueryStringParameter("_revinclude", "List:item") + .withQueryStringParameter("_profile", + MappingSupport.MHD_COMPREHENSIVE_PROFILE)); + mockServer.verify(request("/List") + .withQueryStringParameter("item:identifier", "urn:ietf:rfc:3986|urn:uuid:b2632452-1de7-480d-94b1-c2074d79c871,urn:ietf:rfc:3986|urn:uuid:b2632df2-1de7-480d-1045-c2074d79aabd") + .withQueryStringParameter("_revinclude", "List:item")); + mockServer.verify(request("/List") + .withQueryStringParameter("identifier", "urn:ietf:rfc:3986|urn:uuid:b2632452-1de7-480d-94b1-c2074d79c871,urn:ietf:rfc:3986|urn:uuid:b2632df2-1de7-480d-1045-c2074d79aabd") + .withQueryStringParameter("_revinclude", "List:item")); + } +}