diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java
index 770aa1498c9..f0872b62287 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java
@@ -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.
+ *
+ * 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.
+ *
+ *
+ * The Bundle is converted using the following logic:
+ *
+ * - Bundle.type is changed to
transaction
+ * - Bundle.request.method is changed to
PUT
+ * - Bundle.request.url is changed to
[resourceType]/[id]
+ * - Bundle.fullUrl is changed to
[resourceType]/[id]
+ *
+ *
+ *
+ * @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 null
, 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
+ * null
, resource IDs are unchanged.
+ * @since 7.4.0
+ */
+ public static 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 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 theDeleteParts) {
if (theDeleteParts == null) {
throw new IllegalStateException(
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5945-add-bundleutil-utility-transaction-converter-method.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5945-add-bundleutil-utility-transaction-converter-method.yaml
new file mode 100644
index 00000000000..a397012f304
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5945-add-bundleutil-utility-transaction-converter-method.yaml
@@ -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."
diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java
index e697ef20b2a..34cf71c0435 100644
--- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java
+++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationR4Test.java
@@ -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());
diff --git a/hapi-fhir-jpaserver-ips/src/test/resources/large-patient-everything-4.json.gz b/hapi-fhir-jpaserver-ips/src/test/resources/large-patient-everything-4.json.gz
new file mode 100644
index 00000000000..cc2fd1e4eaa
Binary files /dev/null and b/hapi-fhir-jpaserver-ips/src/test/resources/large-patient-everything-4.json.gz differ
diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java
index ccbb3c60080..85d6c4b4a96 100644
--- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java
+++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/bundle/BundleUtilTest.java
@@ -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();