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:
parent
bff59b6c50
commit
97cfb6de37
|
@ -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(
|
||||
|
|
|
@ -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."
|
|
@ -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());
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue