Skip to content

Commit

Permalink
#2 Add support for Remove Metadata (ITI-62)
Browse files Browse the repository at this point in the history
* Add support for Folder and Assocations in RMD transactio
* Add basic test
* switch to firely for integration tests
  • Loading branch information
Thopap committed Mar 29, 2024
1 parent b8d581d commit 0bc6a64
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -297,8 +298,14 @@ private List<ListEntryComponent> createReferences(List<Association> 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;
}
Expand All @@ -316,7 +323,7 @@ private List<ListEntryComponent> createReferences(List<Association> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,40 +38,68 @@ public class RemoveDocumentsProcessor implements Iti62Service {

@Override
public Response remove(RemoveMetadata metadataToRemove) {
var errorInfo = new ArrayList<ErrorInfo>();
var uuidsToDelete = new ArrayList<String>(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<DocumentReference>(documentFhirQuery.execute(),
DocumentReference.class, client);
var uniqueResults = new TreeSet<ListResource>((a, b) -> Comparator.comparing(IAnyResource::getId).compare(a, b));

var docBundleResult = documentFhirQuery.execute();
var docResult = new PagingFhirResultIterator<DocumentReference>(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<MhdSubmissionSet>(submissionSetFhirQuery.execute(),
MhdSubmissionSet.class, client);
submissionSetResults.forEachRemaining(sub -> {
checkAssociations(sub, uuidsToDelete, builder);
addToDeleteTransaction(sub, uuidsToDelete, builder);
new PagingFhirResultIterator<ListResource>(referenceQuery.execute(),
ListResource.class, client).forEachRemaining(uniqueResults::add);

new PagingFhirResultIterator<ListResource>(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);
Expand All @@ -75,34 +108,36 @@ public Response remove(RemoveMetadata metadataToRemove) {
return response;
}

private void checkAssociations(MhdSubmissionSet sub, ArrayList<String> 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<String> 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<? extends IBaseElement> associatedObjects,
List<String> uuidsToDelete, BundleBuilder builder) {
final var deletedElements = new ArrayList<IBaseElement>();
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<String> uuidsToDelete, BundleBuilder builder) {
private boolean addToDeleteTransaction(IAnyResource resource, List<String> uuidsToDelete, BundleBuilder builder) {
var entryUuid = StoredQueryMapper.entryUuidFrom(resource);
if (uuidsToDelete.contains(entryUuid)) {
builder.addTransactionDeleteEntry(resource);
uuidsToDelete.remove(entryUuid);
return true;
}
return false;
}

}
4 changes: 2 additions & 2 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}
}

0 comments on commit 0bc6a64

Please sign in to comment.