Add utility method to convert Bundle into transaction (#5945)

* Add utility method to convert Bundle into transaction

* Add utility method

* Spotless

* Build fix
This commit is contained in:
James Agnew 2024-05-20 17:00:18 -04:00 committed by GitHub
parent bff59b6c50
commit 97cfb6de37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 150 additions and 20 deletions

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.util.bundle.SearchBundleEntryParts;
import com.google.common.collect.Sets;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBinary;
@ -59,6 +60,7 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_PREV;
@ -67,6 +69,12 @@ import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_PREV;
* Fetch resources from a bundle
*/
public class BundleUtil {
/** Non instantiable */
private BundleUtil() {
// nothing
}
private static final Logger ourLog = LoggerFactory.getLogger(BundleUtil.class);
private static final String PREVIOUS = LINK_PREV;
@ -339,6 +347,66 @@ public class BundleUtil {
TerserUtil.setField(theContext, "entry", theBundle, retVal.toArray(new IBase[0]));
}
/**
* Converts a Bundle containing resources into a FHIR transaction which
* creates/updates the resources. This method does not modify the original
* bundle, but returns a new copy.
* <p>
* This method is mostly intended for test scenarios where you have a Bundle
* containing search results or other sourced resources, and want to upload
* these resources to a server using a single FHIR transaction.
* </p>
* <p>
* The Bundle is converted using the following logic:
* <ul>
* <li>Bundle.type is changed to <code>transaction</code></li>
* <li>Bundle.request.method is changed to <code>PUT</code></li>
* <li>Bundle.request.url is changed to <code>[resourceType]/[id]</code></li>
* <li>Bundle.fullUrl is changed to <code>[resourceType]/[id]</code></li>
* </ul>
* </p>
*
* @param theContext The FhirContext to use with the bundle
* @param theBundle The Bundle to modify. All resources in the Bundle should have an ID.
* @param thePrefixIdsOrNull If not <code>null</code>, all resource IDs and all references in the Bundle will be
* modified to such that their IDs contain the given prefix. For example, for a value
* of "A", the resource "Patient/123" will be changed to be "Patient/A123". If set to
* <code>null</code>, resource IDs are unchanged.
* @since 7.4.0
*/
public static <T extends IBaseBundle> T convertBundleIntoTransaction(
@Nonnull FhirContext theContext, @Nonnull T theBundle, @Nullable String thePrefixIdsOrNull) {
String prefix = defaultString(thePrefixIdsOrNull);
BundleBuilder bb = new BundleBuilder(theContext);
FhirTerser terser = theContext.newTerser();
List<IBase> entries = terser.getValues(theBundle, "Bundle.entry");
for (var entry : entries) {
IBaseResource resource = terser.getSingleValueOrNull(entry, "resource", IBaseResource.class);
if (resource != null) {
Validate.isTrue(resource.getIdElement().hasIdPart(), "Resource in bundle has no ID");
String newId = theContext.getResourceType(resource) + "/" + prefix
+ resource.getIdElement().getIdPart();
IBaseResource resourceClone = terser.clone(resource);
resourceClone.setId(newId);
if (isNotBlank(prefix)) {
for (var ref : terser.getAllResourceReferences(resourceClone)) {
var refElement = ref.getResourceReference().getReferenceElement();
ref.getResourceReference()
.setReference(refElement.getResourceType() + "/" + prefix + refElement.getIdPart());
}
}
bb.addTransactionUpdateEntry(resourceClone);
}
}
return bb.getBundleTyped();
}
private static void validatePartsNotNull(LinkedHashSet<IBase> theDeleteParts) {
if (theDeleteParts == null) {
throw new IllegalStateException(

View File

@ -0,0 +1,6 @@
---
type: add
issue: 5945
title: "A new utility method has been added to `BundleUtil` which converts a FHIR Bundle
containing resources (e.g. a search result bundle) into a FHIR transaction bundle which
could be used to upload those resources to a server."

View File

@ -47,6 +47,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static ca.uhn.fhir.util.BundleUtil.convertBundleIntoTransaction;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.stringContainsInOrder;
@ -81,11 +82,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
@Test
public void testGenerateLargePatientSummary() throws IOException {
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
@ -119,11 +116,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-2.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
@ -145,11 +138,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-3.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
@ -166,16 +155,33 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
assertEquals(80, output.getEntry().size());
}
@Test
public void testGenerateLargePatientSummary4() {
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-4.json.gz");
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, "EPD");
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
Bundle output = myClient
.operation()
.onInstance("Patient/EPD2223")
.named(JpaConstants.OPERATION_SUMMARY)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
// Verify
assertEquals(55, output.getEntry().size());
}
@Test
public void testGenerateTinyPatientSummary() throws IOException {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/tiny-patient-everything.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());

View File

@ -680,6 +680,56 @@ public class BundleUtilTest {
assertNull(BundleUtil.getBundleTypeEnum(ourCtx, bundle));
}
@Test
public void testConvertBundleIntoTransaction() {
Bundle input = createBundleWithPatientAndObservation();
Bundle output = BundleUtil.convertBundleIntoTransaction(ourCtx, input, null);
assertEquals(Bundle.BundleType.TRANSACTION, output.getType());
assertEquals("Patient/123", output.getEntry().get(0).getFullUrl());
assertEquals("Patient/123", output.getEntry().get(0).getRequest().getUrl());
assertEquals(Bundle.HTTPVerb.PUT, output.getEntry().get(0).getRequest().getMethod());
assertTrue(((Patient) output.getEntry().get(0).getResource()).getActive());
assertEquals("Observation/456", output.getEntry().get(1).getFullUrl());
assertEquals("Observation/456", output.getEntry().get(1).getRequest().getUrl());
assertEquals(Bundle.HTTPVerb.PUT, output.getEntry().get(1).getRequest().getMethod());
assertEquals("Patient/123", ((Observation)output.getEntry().get(1).getResource()).getSubject().getReference());
assertEquals(Observation.ObservationStatus.AMENDED, ((Observation)output.getEntry().get(1).getResource()).getStatus());
}
@Test
public void testConvertBundleIntoTransaction_WithPrefix() {
Bundle input = createBundleWithPatientAndObservation();
Bundle output = BundleUtil.convertBundleIntoTransaction(ourCtx, input, "A");
assertEquals(Bundle.BundleType.TRANSACTION, output.getType());
assertEquals("Patient/A123", output.getEntry().get(0).getFullUrl());
assertEquals("Patient/A123", output.getEntry().get(0).getRequest().getUrl());
assertEquals(Bundle.HTTPVerb.PUT, output.getEntry().get(0).getRequest().getMethod());
assertTrue(((Patient) output.getEntry().get(0).getResource()).getActive());
assertEquals("Observation/A456", output.getEntry().get(1).getFullUrl());
assertEquals("Observation/A456", output.getEntry().get(1).getRequest().getUrl());
assertEquals(Bundle.HTTPVerb.PUT, output.getEntry().get(1).getRequest().getMethod());
assertEquals("Patient/A123", ((Observation)output.getEntry().get(1).getResource()).getSubject().getReference());
assertEquals(Observation.ObservationStatus.AMENDED, ((Observation)output.getEntry().get(1).getResource()).getStatus());
}
private static @Nonnull Bundle createBundleWithPatientAndObservation() {
Bundle input = new Bundle();
input.setType(Bundle.BundleType.COLLECTION);
Patient patient = new Patient();
patient.setActive(true);
patient.setId("123");
input.addEntry().setResource(patient);
Observation observation = new Observation();
observation.setId("456");
observation.setStatus(Observation.ObservationStatus.AMENDED);
observation.setSubject(new Reference("Patient/123"));
input.addEntry().setResource(observation);
return input;
}
@Nonnull
private static Bundle withBundle(Resource theResource) {
final Bundle bundle = new Bundle();