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 com.google.common.collect.Sets;
|
||||||
import jakarta.annotation.Nonnull;
|
import jakarta.annotation.Nonnull;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.hl7.fhir.instance.model.api.IBase;
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
import org.hl7.fhir.instance.model.api.IBaseBinary;
|
||||||
|
@ -59,6 +60,7 @@ import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
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.isBlank;
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_PREV;
|
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
|
* Fetch resources from a bundle
|
||||||
*/
|
*/
|
||||||
public class BundleUtil {
|
public class BundleUtil {
|
||||||
|
|
||||||
|
/** Non instantiable */
|
||||||
|
private BundleUtil() {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(BundleUtil.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(BundleUtil.class);
|
||||||
|
|
||||||
private static final String PREVIOUS = LINK_PREV;
|
private static final String PREVIOUS = LINK_PREV;
|
||||||
|
@ -339,6 +347,66 @@ public class BundleUtil {
|
||||||
TerserUtil.setField(theContext, "entry", theBundle, retVal.toArray(new IBase[0]));
|
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) {
|
private static void validatePartsNotNull(LinkedHashSet<IBase> theDeleteParts) {
|
||||||
if (theDeleteParts == null) {
|
if (theDeleteParts == null) {
|
||||||
throw new IllegalStateException(
|
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.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.util.BundleUtil.convertBundleIntoTransaction;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.contains;
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.hamcrest.Matchers.stringContainsInOrder;
|
import static org.hamcrest.Matchers.stringContainsInOrder;
|
||||||
|
@ -81,11 +82,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
|
||||||
@Test
|
@Test
|
||||||
public void testGenerateLargePatientSummary() throws IOException {
|
public void testGenerateLargePatientSummary() throws IOException {
|
||||||
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything.json.gz");
|
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything.json.gz");
|
||||||
sourceData.setType(Bundle.BundleType.TRANSACTION);
|
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
|
||||||
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
|
|
||||||
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
|
|
||||||
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
|
||||||
}
|
|
||||||
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
||||||
ourLog.info("Created {} resources", outcome.getEntry().size());
|
ourLog.info("Created {} resources", outcome.getEntry().size());
|
||||||
|
|
||||||
|
@ -119,11 +116,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
|
||||||
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
|
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
|
||||||
|
|
||||||
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-2.json.gz");
|
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-2.json.gz");
|
||||||
sourceData.setType(Bundle.BundleType.TRANSACTION);
|
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
|
||||||
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
|
|
||||||
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
|
|
||||||
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
|
||||||
}
|
|
||||||
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
||||||
ourLog.info("Created {} resources", outcome.getEntry().size());
|
ourLog.info("Created {} resources", outcome.getEntry().size());
|
||||||
|
|
||||||
|
@ -145,11 +138,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
|
||||||
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
|
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
|
||||||
|
|
||||||
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-3.json.gz");
|
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-3.json.gz");
|
||||||
sourceData.setType(Bundle.BundleType.TRANSACTION);
|
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
|
||||||
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
|
|
||||||
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
|
|
||||||
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
|
||||||
}
|
|
||||||
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
||||||
ourLog.info("Created {} resources", outcome.getEntry().size());
|
ourLog.info("Created {} resources", outcome.getEntry().size());
|
||||||
|
|
||||||
|
@ -166,16 +155,33 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
|
||||||
assertEquals(80, output.getEntry().size());
|
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
|
@Test
|
||||||
public void testGenerateTinyPatientSummary() throws IOException {
|
public void testGenerateTinyPatientSummary() throws IOException {
|
||||||
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
|
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
|
||||||
|
|
||||||
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/tiny-patient-everything.json.gz");
|
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/tiny-patient-everything.json.gz");
|
||||||
sourceData.setType(Bundle.BundleType.TRANSACTION);
|
sourceData = convertBundleIntoTransaction(myFhirContext, sourceData, null);
|
||||||
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
|
|
||||||
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
|
|
||||||
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
|
||||||
}
|
|
||||||
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
|
||||||
ourLog.info("Created {} resources", outcome.getEntry().size());
|
ourLog.info("Created {} resources", outcome.getEntry().size());
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -680,6 +680,56 @@ public class BundleUtilTest {
|
||||||
assertNull(BundleUtil.getBundleTypeEnum(ourCtx, bundle));
|
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
|
@Nonnull
|
||||||
private static Bundle withBundle(Resource theResource) {
|
private static Bundle withBundle(Resource theResource) {
|
||||||
final Bundle bundle = new Bundle();
|
final Bundle bundle = new Bundle();
|
||||||
|
|
Loading…
Reference in New Issue