diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index 2b030a8953e..d8b7665be3d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -626,6 +626,16 @@ public class BundleBuilder { terser.setElement(myBundle, "Bundle.timestamp", theTimestamp.getValueAsString()); } + /** + * Adds a profile URL to Bundle.meta.profile + * + * @since 7.4.0 + */ + public void addProfile(String theProfile) { + FhirTerser terser = myContext.newTerser(); + terser.addElement(myBundle, "Bundle.meta.profile", theProfile); + } + public class DeleteBuilder extends BaseOperationBuilder { // nothing yet diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5938-include-bundle-profile-in-ips.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5938-include-bundle-profile-in-ips.yaml new file mode 100644 index 00000000000..c631d878c61 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/5938-include-bundle-profile-in-ips.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 5938 +title: "Generated IPS documents will now include a bundle profile declaration." diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java index bad1470d646..9be8fe35c2b 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java @@ -42,7 +42,10 @@ public interface IIpsGenerationStrategy { /** * This method returns the profile associated with the IPS document - * generated by this strategy. + * generated by this strategy. This URL will be added to generated + * IPS Bundles in Bundle.meta.profile, and can also + * be used to support the profile parameter on the + * $summary operation. */ String getBundleProfile(); diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java index 7562c5daf93..aa2db895631 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java @@ -146,6 +146,7 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { bundleBuilder.setType(Bundle.BundleType.DOCUMENT.toCode()); bundleBuilder.setIdentifier("urn:ietf:rfc:4122", UUID.randomUUID().toString()); bundleBuilder.setTimestamp(InstantType.now()); + bundleBuilder.addProfile(theStrategy.getBundleProfile()); // Add composition to document bundleBuilder.addDocumentEntry(composition); 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 bb9bf90b283..e697ef20b2a 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 @@ -5,7 +5,6 @@ import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider; @@ -18,6 +17,7 @@ import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.hl7.fhir.common.hapi.validation.support.NpmPackageValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -31,6 +31,7 @@ import org.hl7.fhir.r4.model.Immunization; import org.hl7.fhir.r4.model.MedicationStatement; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.PrimitiveType; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.junit.jupiter.api.AfterEach; @@ -41,6 +42,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; +import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -48,10 +50,10 @@ import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.hibernate.validator.internal.util.Contracts.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * This test uses a complete R4 JPA server as a backend and wires the @@ -77,7 +79,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { @Test - public void testGenerateLargePatientSummary() { + 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()) { @@ -97,6 +99,9 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output)); // Verify + assertThat(output.getMeta().getProfile().stream().map(PrimitiveType::getValue).toList(), contains( + "http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips" + )); validateDocument(output); assertEquals(117, output.getEntry().size()); String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue(); @@ -162,7 +167,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { } @Test - public void testGenerateTinyPatientSummary() { + public void testGenerateTinyPatientSummary() throws IOException { myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY); Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/tiny-patient-everything.json.gz"); @@ -251,7 +256,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { @Nonnull - private static Composition findCompositionSectionByDisplay(Bundle output, String theDisplay) { + private static Composition findCompositionSectionByDisplay(Bundle output, @SuppressWarnings("SameParameterValue") String theDisplay) { Composition composition = (Composition) output.getEntry().get(0).getResource(); Composition.SectionComponent section = composition .getSection() @@ -259,6 +264,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { .filter(t -> t.getCode().getCoding().get(0).getDisplay().equals(theDisplay)) .findFirst() .orElseThrow(); + assertNotNull(section); return composition; } @@ -266,23 +272,26 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { @Nonnull private static List extractSectionTitles(Bundle outcome) { Composition composition = (Composition) outcome.getEntry().get(0).getResource(); - List sectionTitles = composition + return composition .getSection() .stream() .map(Composition.SectionComponent::getTitle) .toList(); - return sectionTitles; } - private void validateDocument(Bundle theOutcome) { + private void validateDocument(Bundle theOutcome) throws IOException { FhirValidator validator = myFhirContext.newValidator(); FhirInstanceValidator instanceValidator = new FhirInstanceValidator(myFhirContext); - instanceValidator.setValidationSupport(new ValidationSupportChain(new IpsTerminologySvc(), myFhirContext.getValidationSupport())); + + NpmPackageValidationSupport npmSupport = new NpmPackageValidationSupport(myFhirContext); + npmSupport.loadPackageFromClasspath("/ips-package-1.1.0.tgz"); + + instanceValidator.setValidationSupport(new ValidationSupportChain(npmSupport, new IpsTerminologySvc(), myFhirContext.getValidationSupport())); validator.registerValidatorModule(instanceValidator); ValidationResult validation = validator.validateWithResult(theOutcome); Optional failure = validation.getMessages().stream().filter(t -> t.getSeverity().ordinal() >= ResultSeverityEnum.ERROR.ordinal()).findFirst(); - assertFalse(failure.isPresent(), () -> failure.get().toString()); + assertFalse(failure.isPresent(), () -> failure.orElseThrow().toString()); } @Configuration @@ -294,7 +303,7 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { } @Bean - public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { + public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) { return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy); } diff --git a/hapi-fhir-jpaserver-ips/src/test/resources/ips-package-1.1.0.tgz b/hapi-fhir-jpaserver-ips/src/test/resources/ips-package-1.1.0.tgz new file mode 100644 index 00000000000..d43871cdd48 Binary files /dev/null and b/hapi-fhir-jpaserver-ips/src/test/resources/ips-package-1.1.0.tgz differ