From 12eb2d6f35dddad35f6128f3aa071157ba8d3d67 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sun, 11 Feb 2024 10:43:56 -0500 Subject: [PATCH] IPS API Refactor (#5682) * IPS enhancements * API design complete * Work on section registry * Work on external fetch * IPS rewrite * Cleanup * Work * IPS refactor * Add changelog * Changelog updates * Spotless * Compile fix * Address review comments * Address review comments * License header * Revert narrative builder change * Address review comments * Addres review comments * Cleanup --- .editorconfig | 5 + .../BaseThymeleafNarrativeGenerator.java | 2 + .../NarrativeGeneratorTemplateUtils.java | 53 ++ .../java/ca/uhn/fhir/util/BundleBuilder.java | 12 +- .../java/ca/uhn/fhir/util/ValidateUtil.java | 7 + .../ca/uhn/fhir/util/ValidateUtilTest.java | 15 +- ...der-doesnt-include-version-in-fullurl.yaml | 6 + .../7_2_0/5682-ips-api-refactor.yaml | 9 + .../7_2_0/5682-ips-enhancements.yaml | 26 + ...682-ips-generator-no-uuids-by-default.yaml | 5 + .../ca/uhn/hapi/fhir/docs/server_jpa/ips.md | 19 +- .../jpa/ips/api/IIpsGenerationStrategy.java | 97 ++-- .../fhir/jpa/ips/api/INoInfoGenerator.java | 39 ++ .../jpa/ips/api/ISectionResourceSupplier.java | 125 +++++ .../ca/uhn/fhir/jpa/ips/api/IpsContext.java | 26 +- .../fhir/jpa/ips/api/IpsSectionContext.java | 43 ++ .../java/ca/uhn/fhir/jpa/ips/api/Section.java | 223 ++++++++ .../uhn/fhir/jpa/ips/api/SectionRegistry.java | 470 ---------------- .../jpa/ips/generator/IIpsGeneratorSvc.java | 4 +- .../ips/generator/IpsGeneratorSvcImpl.java | 528 +++++++++++------- .../jpa/DefaultJpaIpsGenerationStrategy.java | 477 ++++++++++++++++ .../ips/jpa/IJpaSectionSearchStrategy.java | 68 +++ .../jpa/ips/jpa/IpsGenerationCtxConfig.java | 25 + .../ips/jpa/JpaSectionResourceSupplier.java | 128 +++++ .../JpaSectionSearchStrategy.java} | 23 +- .../JpaSectionSearchStrategyCollection.java | 62 ++ ...nceDirectivesJpaSectionSearchStrategy.java | 41 ++ ...gyIntoleranceJpaSectionSearchStrategy.java | 44 ++ ...SectionSearchStrategyDiagnosticReport.java | 42 ++ ...tsJpaSectionSearchStrategyObservation.java | 56 ++ ...ctionalStatusJpaSectionSearchStrategy.java | 40 ++ ...llnessHistoryJpaSectionSearchStrategy.java | 53 ++ ...ImmunizationsJpaSectionSearchStrategy.java | 50 ++ ...edicalDevicesJpaSectionSearchStrategy.java | 47 ++ ...earchStrategyMedicationAdministration.java | 51 ++ ...ctionSearchStrategyMedicationDispense.java | 51 ++ ...ectionSearchStrategyMedicationRequest.java | 51 ++ ...tionSearchStrategyMedicationStatement.java | 54 ++ .../PlanOfCareJpaSectionSearchStrategy.java | 47 ++ .../PregnancyJpaSectionSearchStrategy.java | 75 +++ .../ProblemListJpaSectionSearchStrategy.java | 47 ++ .../ProceduresJpaSectionSearchStrategy.java | 40 ++ ...SocialHistoryJpaSectionSearchStrategy.java | 54 ++ .../VitalSignsJpaSectionSearchStrategy.java | 51 ++ .../ips/provider/IpsOperationProvider.java | 32 +- .../AllergyIntoleranceNoInfoR4Generator.java | 46 ++ .../strategy/BaseIpsGenerationStrategy.java | 130 +++++ .../DefaultIpsGenerationStrategy.java | 446 --------------- .../strategy/MedicationNoInfoR4Generator.java | 45 ++ .../strategy/ProblemNoInfoR4Generator.java | 47 ++ .../jpa/ips/narrative/advancedirectives.html | 6 +- .../jpa/ips/narrative/allergyintolerance.html | 10 +- .../jpa/ips/narrative/diagnosticresults.html | 126 +++-- .../jpa/ips/narrative/functionalstatus.html | 8 +- .../ips/narrative/historyofprocedures.html | 8 +- .../fhir/jpa/ips/narrative/immunizations.html | 12 +- .../jpa/ips/narrative/medicaldevices.html | 8 +- .../jpa/ips/narrative/medicationsummary.html | 131 +++-- .../ips/narrative/pasthistoryofillness.html | 8 +- .../fhir/jpa/ips/narrative/planofcare.html | 4 +- .../uhn/fhir/jpa/ips/narrative/pregnancy.html | 10 +- .../fhir/jpa/ips/narrative/problemlist.html | 8 +- .../fhir/jpa/ips/narrative/socialhistory.html | 12 +- .../fhir/jpa/ips/narrative/vitalsigns.html | 14 +- .../ips/generator/IpsGenerationR4Test.java | 39 +- .../generator/IpsGeneratorSvcImplTest.java | 196 +++++-- .../provider/IpsOperationProviderTest.java | 152 +++++ .../ca/uhn/fhirtest/config/TestR4Config.java | 10 +- .../NarrativeGeneratorTemplateUtilsTest.java | 28 + 69 files changed, 3449 insertions(+), 1478 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java delete mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java rename hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/{api/IpsSectionEnum.java => jpa/JpaSectionSearchStrategy.java} (69%) create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java delete mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java create mode 100644 hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java create mode 100644 hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java diff --git a/.editorconfig b/.editorconfig index 31b0bd4a6a7..c42d0f67472 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,11 @@ tab_width = 4 indent_size = 4 charset = utf-8 +[*.html] +indent_style = tab +tab_width = 3 +indent_size = 3 + [*.xml] indent_style = tab tab_width = 3 diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java index a2b8919e50a..ea192901aa2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.narrative2.BaseNarrativeGenerator; import ca.uhn.fhir.narrative2.INarrativeTemplate; +import ca.uhn.fhir.narrative2.NarrativeGeneratorTemplateUtils; import ca.uhn.fhir.narrative2.TemplateTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.collect.Sets; @@ -109,6 +110,7 @@ public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGener Context context = new Context(); context.setVariable("resource", theTargetContext); context.setVariable("context", theTargetContext); + context.setVariable("narrativeUtil", NarrativeGeneratorTemplateUtils.INSTANCE); context.setVariable( "fhirVersion", theFhirContext.getVersion().getVersion().name()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java new file mode 100644 index 00000000000..21cdab2dcde --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java @@ -0,0 +1,53 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.narrative2; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.BundleUtil; +import org.apache.commons.lang3.tuple.Pair; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; +import java.util.Objects; + +/** + * An instance of this class is added to the Thymeleaf context as a variable with + * name "narrativeUtil" and can be accessed from narrative templates. + * + * @since 7.0.0 + */ +public class NarrativeGeneratorTemplateUtils { + + public static final NarrativeGeneratorTemplateUtils INSTANCE = new NarrativeGeneratorTemplateUtils(); + + /** + * Given a Bundle as input, are any entries present with a given resource type + */ + public boolean bundleHasEntriesWithResourceType(IBaseBundle theBaseBundle, String theResourceType) { + FhirContext ctx = theBaseBundle.getStructureFhirVersionEnum().newContextCached(); + List> entryResources = + BundleUtil.getBundleEntryUrlsAndResources(ctx, theBaseBundle); + return entryResources.stream() + .map(Pair::getValue) + .filter(Objects::nonNull) + .anyMatch(t -> ctx.getResourceType(t).equals(theResourceType)); + } +} 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 c5f25cb3744..2b030a8953e 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 @@ -435,7 +435,7 @@ public class BundleBuilder { */ public void addCollectionEntry(IBaseResource theResource) { setType("collection"); - addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); + addEntryAndReturnRequest(theResource); } /** @@ -443,7 +443,7 @@ public class BundleBuilder { */ public void addDocumentEntry(IBaseResource theResource) { setType("document"); - addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); + addEntryAndReturnRequest(theResource); } /** @@ -475,6 +475,14 @@ public class BundleBuilder { return (IBaseBackboneElement) searchInstance; } + private IBase addEntryAndReturnRequest(IBaseResource theResource) { + IIdType id = theResource.getIdElement(); + if (id.hasVersionIdPart()) { + id = id.toVersionless(); + } + return addEntryAndReturnRequest(theResource, id.getValue()); + } + private IBase addEntryAndReturnRequest(IBaseResource theResource, String theFullUrl) { Validate.notNull(theResource, "theResource must not be null"); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java index 192ca9e3c90..672943539f3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ValidateUtil.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.util; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -82,6 +83,12 @@ public class ValidateUtil { } } + public static void isTrueOrThrowResourceNotFound(boolean theSuccess, String theMessage, Object... theValues) { + if (!theSuccess) { + throw new ResourceNotFoundException(Msg.code(2494) + String.format(theMessage, theValues)); + } + } + public static void exactlyOneNotNullOrThrowInvalidRequestException(Object[] theObjects, String theMessage) { int count = 0; for (Object next : theObjects) { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java index fd96f2cf4ad..412662a972b 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ValidateUtilTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.junit.jupiter.api.Test; @@ -12,7 +13,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class ValidateUtilTest { @Test - public void testValidate() { + public void testIsTrueOrThrowInvalidRequest() { ValidateUtil.isTrueOrThrowInvalidRequest(true, ""); try { @@ -23,6 +24,18 @@ public class ValidateUtilTest { } } + @Test + public void testIsTrueOrThrowResourceNotFound() { + ValidateUtil.isTrueOrThrowResourceNotFound(true, ""); + + try { + ValidateUtil.isTrueOrThrowResourceNotFound(false, "The message"); + fail(); + } catch (ResourceNotFoundException e) { + assertEquals(Msg.code(2494) + "The message", e.getMessage()); + } + } + @Test public void testIsGreaterThan() { ValidateUtil.isGreaterThan(2L, 1L, ""); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml new file mode 100644 index 00000000000..7304616964a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-bundlebuilder-doesnt-include-version-in-fullurl.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 5682 +title: "The BundleBuilder utility class will no longer include the `/_version/xxx` portion of the + resource ID in the `Bundle.entry.fullUrl` it generates, as the FHIR specification states that this + should be omitted." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml new file mode 100644 index 00000000000..9af888831bf --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-api-refactor.yaml @@ -0,0 +1,9 @@ +--- +type: change +issue: 5682 +title: "The IPS $summary generation API has been overhauled to make it more flexible for + future use cases. Specifically, the section registry has been removed and folded into + the generation strategy, and support has been added for non-JPA sources of data. This is + a breaking change to the API, and implementers will need to update their code. This updated + API incorporates community feedback, and should now be considered a stable API for IPS + generation." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml new file mode 100644 index 00000000000..80b29d1fb39 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-enhancements.yaml @@ -0,0 +1,26 @@ +--- +type: add +issue: 5682 +title: "Several enhancements have been made to the International Patient Summary generator based on + feedback from implementers: + " diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml new file mode 100644 index 00000000000..ef6bca8549a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5682-ips-generator-no-uuids-by-default.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 5682 +title: "The IPS Generator will no longer replace resource IDs with placeholder IDs in the resulting + bundle by default, although this can be overridden in the generation strategy object." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md index 13362b43668..aec6fdbba94 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md @@ -14,24 +14,17 @@ The IPS Generator uses FHIR resources stored in your repository as its input. Th # Generation Strategy -A user supplied strategy class is used to determine various properties of the IPS. This class must implement the `IIpsGenerationStrategy` interface. A default implementation called `DefaultIpsGenerationStrategy` is included. You may use this default implementation, use a subclassed version of it that adds additional logic, or use en entirely new implementation. +A user supplied strategy class is used to determine various properties of the IPS. This class must implement the `IIpsGenerationStrategy` interface. A default implementation called `DefaultJpaIpsGenerationStrategy` is included. You may use this default implementation, use a subclassed version of it that adds additional logic, or use en entirely new implementation. -The generation strategy also supplies the [Section Registry](#section-registry) and [Narrative Templates](#narrative-templates) implementations, so it can be considered the central part of your IPS configuration. +The generation strategy also supplies the [Narrative Templates](#narrative-templates) implementations, so it can be considered the central part of your IPS configuration. * JavaDoc: [IIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.html) * Source Code: [IIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java) -* JavaDoc: [DefaultIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.html) -* Source Code: [DefaultIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java) +The default generation strategy defines the sections that will be included in your IPS. Out of the box, the standard IPS sections are all included. See the [IG homepage](http://hl7.org/fhir/uv/ips/) for a list of the standard sections. - - -# Section Registry - -The IPS SectionRegistry class defines the sections that will be included in your IPS. Out of the box, the standard IPS sections are all included. See the [IG homepage](http://hl7.org/fhir/uv/ips/) for a list of the standard sections. - -* JavaDoc: [SectionRegistry](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/api/SectionRegistry.html) -* Source Code: [SectionRegistry.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java) +* JavaDoc: [DefaultJpaIpsGenerationStrategy](/hapi-fhir/apidocs/hapi-fhir-jpaserver-ips/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.html) +* Source Code: [DefaultJpaIpsGenerationStrategy.java](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java) @@ -44,7 +37,7 @@ The IPS generator uses HAPI FHIR [Narrative Generation](/hapi-fhir/docs/model/na Narrative templates for individual sections will be supplied a Bundle resource containing only the matched resources for the individual section as entries (ie. the Composition itself will not be present and no other resources will be present). So, for example, when generating the _Allergies / Intolerances_ IPS section narrative, the input to the narrative generator will be a _Bundle_ resource containing only _AllergyIntolerance_ resources. -The narrative properties file should contain definitions using the profile URL of the individual section (as defined in the [section registry](#section-registry)) as the `.profile` qualifier. For example: +The narrative properties file should contain definitions using the profile URL of the individual section (as defined in the section definition within the generation strategy) as the `.profile` qualifier. For example: ```properties ips-allergyintolerance.resourceType=Bundle 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 8fd6f4a5554..bad1470d646 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 @@ -19,15 +19,17 @@ */ package ca.uhn.fhir.jpa.ips.api; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.jpa.ips.strategy.BaseIpsGenerationStrategy; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import java.util.List; -import java.util.Set; /** * This interface is the primary configuration and strategy provider for the @@ -39,11 +41,34 @@ import java.util.Set; public interface IIpsGenerationStrategy { /** - * Provides a registry which defines the various sections that will be - * included when generating an IPS. It can be subclassed and customized - * as needed in order to add, change, or remove sections. + * This method returns the profile associated with the IPS document + * generated by this strategy. */ - SectionRegistry getSectionRegistry(); + String getBundleProfile(); + + /** + * This method will be called once by the framework. It can be + * used to perform any initialization. + */ + void initialize(); + + /** + * This method should return a list of the sections to include in the + * generated IPS. Note that each section must have a unique value for the + * {@link Section#getProfile()} value. + */ + @Nonnull + List
getSections(); + + /** + * Returns the resource supplier for the given section. The resource supplier + * is used to supply the resources which will be used for a given + * section. + * + * @param theSection The section + */ + @Nonnull + ISectionResourceSupplier getSectionResourceSupplier(@Nonnull Section theSection); /** * Provides a list of configuration property files for the IPS narrative generator. @@ -53,7 +78,7 @@ public interface IIpsGenerationStrategy { *

* If more than one file is provided, the files will be evaluated in order. Therefore you * might choose to include a custom file, followed by - * {@link ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy#DEFAULT_IPS_NARRATIVES_PROPERTIES} + * {@link BaseIpsGenerationStrategy#DEFAULT_IPS_NARRATIVES_PROPERTIES} * in order to fall back to the default templates for any sections you have not * provided an explicit template for. *

@@ -85,7 +110,13 @@ public interface IIpsGenerationStrategy { /** * This method is used to determine the resource ID to assign to a resource that * will be added to the IPS document Bundle. Implementations will probably either - * return the resource ID as-is, or generate a placeholder UUID to replace it with. + * return null to leave the resource ID as-is, or generate a + * placeholder UUID to replace it with. + *

+ * If you want to replace the native resource ID with a placeholder so as not + * to leak the server-generated IDs, the recommended way is to + * return IdType.newRandomUuid() + *

* * @param theIpsContext The associated context for the specific IPS document being * generated. Note that this will be null when @@ -93,43 +124,33 @@ public interface IIpsGenerationStrategy { * be populated for all subsequent calls for a given IPS * document generation. * @param theResource The resource to massage the resource ID for - * @return An ID to assign to the resource + * @return An ID to assign to the resource, or null to leave the existing ID intact, + * meaning that the server-assigned IDs will be used in the bundle. */ + @Nullable IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource); /** - * This method can manipulate the {@link SearchParameterMap} that will - * be used to find candidate resources for the given IPS section. The map will already have - * a subject/patient parameter added to it. The map provided in {@literal theSearchParameterMap} - * will contain a subject/patient reference, but no other parameters. This method can add other - * parameters. - *

- * For example, for a Vital Signs section, the implementation might add a parameter indicating - * the parameter category=vital-signs. + * Fetches and returns the patient to include in the generated IPS for the given patient ID. * - * @param theIpsSectionContext The context, which indicates the IPS section and the resource type - * being searched for. - * @param theSearchParameterMap The map to manipulate. - */ - void massageResourceSearch( - IpsContext.IpsSectionContext theIpsSectionContext, SearchParameterMap theSearchParameterMap); - - /** - * Return a set of Include directives to be added to the resource search - * for resources to include for a given IPS section. These include statements will - * be added to the same {@link SearchParameterMap} provided to - * {@link #massageResourceSearch(IpsContext.IpsSectionContext, SearchParameterMap)}. - * This is a separate method in order to make subclassing easier. - * - * @param theIpsSectionContext The context, which indicates the IPS section and the resource type - * being searched for. + * @throws ResourceNotFoundException If the ID is not known. */ @Nonnull - Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext); + IBaseResource fetchPatient(IIdType thePatientId, RequestDetails theRequestDetails) throws ResourceNotFoundException; /** - * This method will be called for each found resource candidate for inclusion in the - * IPS document. The strategy can decide whether to include it or not. + * Fetches and returns the patient to include in the generated IPS for the given patient identifier. + * + * @throws ResourceNotFoundException If the ID is not known. */ - boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate); + @Nonnull + IBaseResource fetchPatient(TokenParam thePatientIdentifier, RequestDetails theRequestDetails); + + /** + * This method is called once for each generated IPS document, after all other processing is complete. It can + * be used by the strategy to make direct manipulations prior to returning the document. + */ + default void postManipulateIpsBundle(IBaseBundle theBundle) { + // nothing + } } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java new file mode 100644 index 00000000000..cbb1c18375d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/INoInfoGenerator.java @@ -0,0 +1,39 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.api; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +/** + * This interface is invoked when a section has no resources found, and should generate + * a "stub" resource explaining why. Typically this would be content such as "no information + * is available for this section", and might indicate for example that the absence of + * AllergyIntolerance resources only indicates that the allergy status is not known, not that + * the patient has no allergies. + */ +public interface INoInfoGenerator { + + /** + * Generate an appropriate no-info resource. The resource does not need to have an ID populated, + * although it can if it is a resource found in the repository. + */ + IBaseResource generate(IIdType theSubjectId); +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java new file mode 100644 index 00000000000..63543eb749d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/ISectionResourceSupplier.java @@ -0,0 +1,125 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.api; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.thymeleaf.util.Validate; + +import java.util.List; + +/** + * This interface is invoked for each section of the IPS, and fetches/returns the + * resources which will be included in the IPS document for that section. This + * might be by performing a search in a local repository, but could also be + * done by calling a remote repository, performing a calculation, making + * JDBC database calls directly, etc. + *

+ * Note that you only need to implement this interface directly if you want to + * provide manual logic for gathering and preparing resources to include in + * an IPS document. If your resources can be collected by querying a JPS + * repository, you can use {@link ca.uhn.fhir.jpa.ips.jpa.JpaSectionResourceSupplier} + * as the implementation of this interface, and + * {@link ca.uhn.fhir.jpa.ips.jpa.IJpaSectionSearchStrategy} becomes the class + * that is used to define your searches. + *

+ * + * @since 7.2.0 + * @see ca.uhn.fhir.jpa.ips.jpa.JpaSectionResourceSupplier + */ +public interface ISectionResourceSupplier { + + /** + * This method will be called once for each section context (section and resource type combination), + * and will be used to supply the resources to include in the given IPS section. This method can + * be used if you wish to fetch resources for a given section from a source other than + * the repository. This could mean fetching resources using a FHIR REST client to an + * external server, or could even mean fetching data directly from a database using JDBC + * or similar. + * + * @param theIpsContext The IPS context, containing the identity of the patient whose IPS is being generated. + * @param theSectionContext The section context, containing the section name and resource type. + * @param theRequestDetails The RequestDetails object associated with the HTTP request associated with this generation. + * @return Returns a list of resources to add to the given section, or null. + */ + @Nullable + List fetchResourcesForSection( + IpsContext theIpsContext, IpsSectionContext theSectionContext, RequestDetails theRequestDetails); + + /** + * This enum specifies how an individual {@link ResourceEntry resource entry} that + * is returned by {@link #fetchResourcesForSection(IpsContext, IpsSectionContext, RequestDetails)} + * should be included in the resulting IPS document bundle. + */ + enum InclusionTypeEnum { + + /** + * The resource should be included in the document bundle and linked to + * from the Composition via the Composition.section.entry + * reference. + */ + PRIMARY_RESOURCE, + + /** + * The resource should be included in the document bundle, but not directly + * linked from the composition. This typically means that it is referenced + * by at least one primary resource. + */ + SECONDARY_RESOURCE, + + /** + * Do not include this resource in the document + */ + EXCLUDE + } + + /** + * This class is the return type for {@link #fetchResourcesForSection(IpsContext, IpsSectionContext, RequestDetails)}. + */ + class ResourceEntry { + + private final IBaseResource myResource; + + private final InclusionTypeEnum myInclusionType; + + /** + * Constructor + * + * @param theResource The resource to include (must not be null) + * @param theInclusionType The inclusion type (must not be null) + */ + public ResourceEntry(@Nonnull IBaseResource theResource, @Nonnull InclusionTypeEnum theInclusionType) { + Validate.notNull(theResource, "theResource must not be null"); + Validate.notNull(theInclusionType, "theInclusionType must not be null"); + myResource = theResource; + myInclusionType = theInclusionType; + } + + public IBaseResource getResource() { + return myResource; + } + + public InclusionTypeEnum getInclusionType() { + return myInclusionType; + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java index 5424d8791cc..f57d8c381ec 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java @@ -58,28 +58,8 @@ public class IpsContext { return mySubjectId; } - public IpsSectionContext newSectionContext(IpsSectionEnum theSection, String theResourceType) { - return new IpsSectionContext(mySubject, mySubjectId, theSection, theResourceType); - } - - public static class IpsSectionContext extends IpsContext { - - private final IpsSectionEnum mySection; - private final String myResourceType; - - private IpsSectionContext( - IBaseResource theSubject, IIdType theSubjectId, IpsSectionEnum theSection, String theResourceType) { - super(theSubject, theSubjectId); - mySection = theSection; - myResourceType = theResourceType; - } - - public String getResourceType() { - return myResourceType; - } - - public IpsSectionEnum getSection() { - return mySection; - } + public IpsSectionContext newSectionContext( + Section theSection, Class theResourceType) { + return new IpsSectionContext<>(mySubject, mySubjectId, theSection, theResourceType); } } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java new file mode 100644 index 00000000000..669c8c5d389 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionContext.java @@ -0,0 +1,43 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.api; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +public class IpsSectionContext extends IpsContext { + + private final Section mySection; + private final Class myResourceType; + + IpsSectionContext(IBaseResource theSubject, IIdType theSubjectId, Section theSection, Class theResourceType) { + super(theSubject, theSubjectId); + mySection = theSection; + myResourceType = theResourceType; + } + + public Class getResourceType() { + return myResourceType; + } + + public Section getSection() { + return mySection; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java new file mode 100644 index 00000000000..7dfb47fcfc2 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/Section.java @@ -0,0 +1,223 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.api; + +import jakarta.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.ArrayList; +import java.util.List; + +/** + * Call {@link #newBuilder()} to create a new instance of this class. + */ +public class Section { + + private final String myTitle; + private final String mySectionCode; + private final String mySectionDisplay; + private final List> myResourceTypes; + private final String myProfile; + private final INoInfoGenerator myNoInfoGenerator; + + private final String mySectionSystem; + + private Section( + String theTitle, + String theSectionSystem, + String theSectionCode, + String theSectionDisplay, + List> theResourceTypes, + String theProfile, + INoInfoGenerator theNoInfoGenerator) { + myTitle = theTitle; + mySectionSystem = theSectionSystem; + mySectionCode = theSectionCode; + mySectionDisplay = theSectionDisplay; + myResourceTypes = List.copyOf(theResourceTypes); + myProfile = theProfile; + myNoInfoGenerator = theNoInfoGenerator; + } + + @Nullable + public INoInfoGenerator getNoInfoGenerator() { + return myNoInfoGenerator; + } + + public List> getResourceTypes() { + return myResourceTypes; + } + + public String getProfile() { + return myProfile; + } + + public String getTitle() { + return myTitle; + } + + public String getSectionSystem() { + return mySectionSystem; + } + + public String getSectionCode() { + return mySectionCode; + } + + public String getSectionDisplay() { + return mySectionDisplay; + } + + @Override + public boolean equals(Object theO) { + if (theO instanceof Section) { + Section o = (Section) theO; + return StringUtils.equals(myProfile, o.myProfile); + } + return false; + } + + @Override + public int hashCode() { + return myProfile.hashCode(); + } + + /** + * Create a new empty section builder + */ + public static SectionBuilder newBuilder() { + return new SectionBuilder(); + } + + /** + * Create a new section builder which is a clone of an existing section + */ + public static SectionBuilder newBuilder(Section theSection) { + return new SectionBuilder( + theSection.myTitle, + theSection.mySectionSystem, + theSection.mySectionCode, + theSection.mySectionDisplay, + theSection.myProfile, + theSection.myNoInfoGenerator, + theSection.myResourceTypes); + } + + public static class SectionBuilder { + + private String myTitle; + private String mySectionSystem; + private String mySectionCode; + private String mySectionDisplay; + private List> myResourceTypes = new ArrayList<>(); + private String myProfile; + private INoInfoGenerator myNoInfoGenerator; + + private SectionBuilder() { + super(); + } + + public SectionBuilder( + String theTitle, + String theSectionSystem, + String theSectionCode, + String theSectionDisplay, + String theProfile, + INoInfoGenerator theNoInfoGenerator, + List> theResourceTypes) { + myTitle = theTitle; + mySectionSystem = theSectionSystem; + mySectionCode = theSectionCode; + mySectionDisplay = theSectionDisplay; + myNoInfoGenerator = theNoInfoGenerator; + myProfile = theProfile; + myResourceTypes = new ArrayList<>(theResourceTypes); + } + + public SectionBuilder withTitle(String theTitle) { + Validate.notBlank(theTitle); + myTitle = theTitle; + return this; + } + + public SectionBuilder withSectionSystem(String theSectionSystem) { + Validate.notBlank(theSectionSystem); + mySectionSystem = theSectionSystem; + return this; + } + + public SectionBuilder withSectionCode(String theSectionCode) { + Validate.notBlank(theSectionCode); + mySectionCode = theSectionCode; + return this; + } + + public SectionBuilder withSectionDisplay(String theSectionDisplay) { + Validate.notBlank(theSectionDisplay); + mySectionDisplay = theSectionDisplay; + return this; + } + + /** + * This method may be called multiple times if the section will contain multiple resource types + */ + public SectionBuilder withResourceType(Class theResourceType) { + Validate.notNull(theResourceType, "theResourceType must not be null"); + Validate.isTrue(!myResourceTypes.contains(theResourceType), "theResourceType has already been added"); + myResourceTypes.add(theResourceType); + return this; + } + + public SectionBuilder withProfile(String theProfile) { + Validate.notBlank(theProfile); + myProfile = theProfile; + return this; + } + + /** + * Supplies a {@link INoInfoGenerator} which is used to create a stub resource + * to place in this section if no actual contents are found. This can be + * {@literal null} if you do not want any such stub to be included for this + * section. + */ + @SuppressWarnings("UnusedReturnValue") + public SectionBuilder withNoInfoGenerator(@Nullable INoInfoGenerator theNoInfoGenerator) { + myNoInfoGenerator = theNoInfoGenerator; + return this; + } + + public Section build() { + Validate.notBlank(mySectionSystem, "No section system has been defined for this section"); + Validate.notBlank(mySectionCode, "No section code has been defined for this section"); + Validate.notBlank(mySectionDisplay, "No section display has been defined for this section"); + + return new Section( + myTitle, + mySectionSystem, + mySectionCode, + mySectionDisplay, + myResourceTypes, + myProfile, + myNoInfoGenerator); + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java deleted file mode 100644 index e7940638d78..00000000000 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java +++ /dev/null @@ -1,470 +0,0 @@ -/*- - * #%L - * HAPI FHIR JPA Server - International Patient Summary (IPS) - * %% - * Copyright (C) 2014 - 2024 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package ca.uhn.fhir.jpa.ips.api; - -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.AllergyIntolerance; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Condition; -import org.hl7.fhir.r4.model.MedicationStatement; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.ResourceType; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -/** - * This class is the registry for sections for the IPS document. It can be extended - * and customized if you wish to add / remove / change sections. - *

- * By default, all standard sections in the - * base IPS specification IG - * are included. You can customize this to remove sections, or to add new ones - * as permitted by the IG. - *

- *

- * To customize the sections, you may override the {@link #addSections()} method - * in order to add new sections or remove them. You may also override individual - * section methods such as {@link #addSectionAllergyIntolerance()} or - * {@link #addSectionAdvanceDirectives()}. - *

- */ -public class SectionRegistry { - - private final ArrayList
mySections = new ArrayList<>(); - private List> myGlobalCustomizers = new ArrayList<>(); - - /** - * Constructor - */ - public SectionRegistry() { - super(); - } - - /** - * This method should be automatically called by the Spring context. It initializes - * the registry. - */ - @PostConstruct - public final void initialize() { - Validate.isTrue(mySections.isEmpty(), "Sections are already initialized"); - addSections(); - } - - public boolean isInitialized() { - return !mySections.isEmpty(); - } - - /** - * Add the various sections to the registry in order. This method can be overridden for - * customization. - */ - protected void addSections() { - addSectionAllergyIntolerance(); - addSectionMedicationSummary(); - addSectionProblemList(); - addSectionImmunizations(); - addSectionProcedures(); - addSectionMedicalDevices(); - addSectionDiagnosticResults(); - addSectionVitalSigns(); - addSectionPregnancy(); - addSectionSocialHistory(); - addSectionIllnessHistory(); - addSectionFunctionalStatus(); - addSectionPlanOfCare(); - addSectionAdvanceDirectives(); - } - - protected void addSectionAllergyIntolerance() { - addSection(IpsSectionEnum.ALLERGY_INTOLERANCE) - .withTitle("Allergies and Intolerances") - .withSectionCode("48765-2") - .withSectionDisplay("Allergies and adverse reactions Document") - .withResourceTypes(ResourceType.AllergyIntolerance.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies") - .withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator()) - .build(); - } - - protected void addSectionMedicationSummary() { - addSection(IpsSectionEnum.MEDICATION_SUMMARY) - .withTitle("Medication List") - .withSectionCode("10160-0") - .withSectionDisplay("History of Medication use Narrative") - .withResourceTypes( - ResourceType.MedicationStatement.name(), - ResourceType.MedicationRequest.name(), - ResourceType.MedicationAdministration.name(), - ResourceType.MedicationDispense.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications") - .withNoInfoGenerator(new MedicationNoInfoR4Generator()) - .build(); - } - - protected void addSectionProblemList() { - addSection(IpsSectionEnum.PROBLEM_LIST) - .withTitle("Problem List") - .withSectionCode("11450-4") - .withSectionDisplay("Problem list - Reported") - .withResourceTypes(ResourceType.Condition.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems") - .withNoInfoGenerator(new ProblemNoInfoR4Generator()) - .build(); - } - - protected void addSectionImmunizations() { - addSection(IpsSectionEnum.IMMUNIZATIONS) - .withTitle("History of Immunizations") - .withSectionCode("11369-6") - .withSectionDisplay("History of Immunization Narrative") - .withResourceTypes(ResourceType.Immunization.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations") - .build(); - } - - protected void addSectionProcedures() { - addSection(IpsSectionEnum.PROCEDURES) - .withTitle("History of Procedures") - .withSectionCode("47519-4") - .withSectionDisplay("History of Procedures Document") - .withResourceTypes(ResourceType.Procedure.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx") - .build(); - } - - protected void addSectionMedicalDevices() { - addSection(IpsSectionEnum.MEDICAL_DEVICES) - .withTitle("Medical Devices") - .withSectionCode("46264-8") - .withSectionDisplay("History of medical device use") - .withResourceTypes(ResourceType.DeviceUseStatement.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices") - .build(); - } - - protected void addSectionDiagnosticResults() { - addSection(IpsSectionEnum.DIAGNOSTIC_RESULTS) - .withTitle("Diagnostic Results") - .withSectionCode("30954-2") - .withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative") - .withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults") - .build(); - } - - protected void addSectionVitalSigns() { - addSection(IpsSectionEnum.VITAL_SIGNS) - .withTitle("Vital Signs") - .withSectionCode("8716-3") - .withSectionDisplay("Vital signs") - .withResourceTypes(ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns") - .build(); - } - - protected void addSectionPregnancy() { - addSection(IpsSectionEnum.PREGNANCY) - .withTitle("Pregnancy Information") - .withSectionCode("10162-6") - .withSectionDisplay("History of pregnancies Narrative") - .withResourceTypes(ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx") - .build(); - } - - protected void addSectionSocialHistory() { - addSection(IpsSectionEnum.SOCIAL_HISTORY) - .withTitle("Social History") - .withSectionCode("29762-2") - .withSectionDisplay("Social history Narrative") - .withResourceTypes(ResourceType.Observation.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory") - .build(); - } - - protected void addSectionIllnessHistory() { - addSection(IpsSectionEnum.ILLNESS_HISTORY) - .withTitle("History of Past Illness") - .withSectionCode("11348-0") - .withSectionDisplay("History of Past illness Narrative") - .withResourceTypes(ResourceType.Condition.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx") - .build(); - } - - protected void addSectionFunctionalStatus() { - addSection(IpsSectionEnum.FUNCTIONAL_STATUS) - .withTitle("Functional Status") - .withSectionCode("47420-5") - .withSectionDisplay("Functional status assessment note") - .withResourceTypes(ResourceType.ClinicalImpression.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus") - .build(); - } - - protected void addSectionPlanOfCare() { - addSection(IpsSectionEnum.PLAN_OF_CARE) - .withTitle("Plan of Care") - .withSectionCode("18776-5") - .withSectionDisplay("Plan of care note") - .withResourceTypes(ResourceType.CarePlan.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare") - .build(); - } - - protected void addSectionAdvanceDirectives() { - addSection(IpsSectionEnum.ADVANCE_DIRECTIVES) - .withTitle("Advance Directives") - .withSectionCode("42348-3") - .withSectionDisplay("Advance directives") - .withResourceTypes(ResourceType.Consent.name()) - .withProfile( - "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives") - .build(); - } - - private SectionBuilder addSection(IpsSectionEnum theSectionEnum) { - return new SectionBuilder(theSectionEnum); - } - - public SectionRegistry addGlobalCustomizer(Consumer theGlobalCustomizer) { - Validate.notNull(theGlobalCustomizer, "theGlobalCustomizer must not be null"); - myGlobalCustomizers.add(theGlobalCustomizer); - return this; - } - - public List
getSections() { - Validate.isTrue(isInitialized(), "Section registry has not been initialized"); - return Collections.unmodifiableList(mySections); - } - - public Section getSection(IpsSectionEnum theSectionEnum) { - return getSections().stream() - .filter(t -> t.getSectionEnum() == theSectionEnum) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No section for type: " + theSectionEnum)); - } - - public interface INoInfoGenerator { - - /** - * Generate an appropriate no-info resource. The resource does not need to have an ID populated, - * although it can if it is a resource found in the repository. - */ - IBaseResource generate(IIdType theSubjectId); - } - - public class SectionBuilder { - - private final IpsSectionEnum mySectionEnum; - private String myTitle; - private String mySectionCode; - private String mySectionDisplay; - private List myResourceTypes; - private String myProfile; - private INoInfoGenerator myNoInfoGenerator; - - public SectionBuilder(IpsSectionEnum theSectionEnum) { - mySectionEnum = theSectionEnum; - } - - public SectionBuilder withTitle(String theTitle) { - Validate.notBlank(theTitle); - myTitle = theTitle; - return this; - } - - public SectionBuilder withSectionCode(String theSectionCode) { - Validate.notBlank(theSectionCode); - mySectionCode = theSectionCode; - return this; - } - - public SectionBuilder withSectionDisplay(String theSectionDisplay) { - Validate.notBlank(theSectionDisplay); - mySectionDisplay = theSectionDisplay; - return this; - } - - public SectionBuilder withResourceTypes(String... theResourceTypes) { - Validate.isTrue(theResourceTypes.length > 0); - myResourceTypes = Arrays.asList(theResourceTypes); - return this; - } - - public SectionBuilder withProfile(String theProfile) { - Validate.notBlank(theProfile); - myProfile = theProfile; - return this; - } - - public SectionBuilder withNoInfoGenerator(INoInfoGenerator theNoInfoGenerator) { - myNoInfoGenerator = theNoInfoGenerator; - return this; - } - - public void build() { - myGlobalCustomizers.forEach(t -> t.accept(this)); - mySections.add(new Section( - mySectionEnum, - myTitle, - mySectionCode, - mySectionDisplay, - myResourceTypes, - myProfile, - myNoInfoGenerator)); - } - } - - private static class AllergyIntoleranceNoInfoR4Generator implements INoInfoGenerator { - @Override - public IBaseResource generate(IIdType theSubjectId) { - AllergyIntolerance allergy = new AllergyIntolerance(); - allergy.setCode(new CodeableConcept() - .addCoding(new Coding() - .setCode("no-allergy-info") - .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") - .setDisplay("No information about allergies"))) - .setPatient(new Reference(theSubjectId)) - .setClinicalStatus(new CodeableConcept() - .addCoding(new Coding() - .setCode("active") - .setSystem("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"))); - return allergy; - } - } - - private static class MedicationNoInfoR4Generator implements INoInfoGenerator { - @Override - public IBaseResource generate(IIdType theSubjectId) { - MedicationStatement medication = new MedicationStatement(); - // setMedicationCodeableConcept is not available - medication - .setMedication(new CodeableConcept() - .addCoding(new Coding() - .setCode("no-medication-info") - .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") - .setDisplay("No information about medications"))) - .setSubject(new Reference(theSubjectId)) - .setStatus(MedicationStatement.MedicationStatementStatus.UNKNOWN); - // .setEffective(new - // Period().addExtension().setUrl("http://hl7.org/fhir/StructureDefinition/data-absent-reason").setValue((new Coding().setCode("not-applicable")))) - return medication; - } - } - - private static class ProblemNoInfoR4Generator implements INoInfoGenerator { - @Override - public IBaseResource generate(IIdType theSubjectId) { - Condition condition = new Condition(); - condition - .setCode(new CodeableConcept() - .addCoding(new Coding() - .setCode("no-problem-info") - .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") - .setDisplay("No information about problems"))) - .setSubject(new Reference(theSubjectId)) - .setClinicalStatus(new CodeableConcept() - .addCoding(new Coding() - .setCode("active") - .setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical"))); - return condition; - } - } - - public static class Section { - - private final IpsSectionEnum mySectionEnum; - private final String myTitle; - private final String mySectionCode; - private final String mySectionDisplay; - private final List myResourceTypes; - private final String myProfile; - private final INoInfoGenerator myNoInfoGenerator; - - public Section( - IpsSectionEnum theSectionEnum, - String theTitle, - String theSectionCode, - String theSectionDisplay, - List theResourceTypes, - String theProfile, - INoInfoGenerator theNoInfoGenerator) { - mySectionEnum = theSectionEnum; - myTitle = theTitle; - mySectionCode = theSectionCode; - mySectionDisplay = theSectionDisplay; - myResourceTypes = Collections.unmodifiableList(new ArrayList<>(theResourceTypes)); - myProfile = theProfile; - myNoInfoGenerator = theNoInfoGenerator; - } - - @Nullable - public INoInfoGenerator getNoInfoGenerator() { - return myNoInfoGenerator; - } - - public List getResourceTypes() { - return myResourceTypes; - } - - public String getProfile() { - return myProfile; - } - - public IpsSectionEnum getSectionEnum() { - return mySectionEnum; - } - - public String getTitle() { - return myTitle; - } - - public String getSectionCode() { - return mySectionCode; - } - - public String getSectionDisplay() { - return mySectionDisplay; - } - } -} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java index 1f53fcebd51..fcd0330319c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java @@ -30,11 +30,11 @@ public interface IIpsGeneratorSvc { * Generates an IPS document and returns the complete document bundle * for the given patient by ID */ - IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId); + IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile); /** * Generates an IPS document and returns the complete document bundle * for the given patient by identifier */ - IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier); + IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile); } 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 f3b837623c0..7562c5daf93 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 @@ -20,33 +20,23 @@ package ca.uhn.fhir.jpa.ips.generator; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; import ca.uhn.fhir.jpa.ips.api.IpsContext; -import ca.uhn.fhir.jpa.ips.api.IpsSectionEnum; -import ca.uhn.fhir.jpa.ips.api.SectionRegistry; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.dstu2.resource.Observation; -import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.api.Section; import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator; -import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.CompositionBuilder; import ca.uhn.fhir.util.ResourceReferenceInfo; -import ca.uhn.fhir.util.ValidateUtil; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseExtension; @@ -58,94 +48,100 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.InstantType; -import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Resource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; -import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { - public static final int CHUNK_SIZE = 10; - private static final Logger ourLog = LoggerFactory.getLogger(IpsGeneratorSvcImpl.class); - private final IIpsGenerationStrategy myGenerationStrategy; - private final DaoRegistry myDaoRegistry; + public static final String RESOURCE_ENTRY_INCLUSION_TYPE = "RESOURCE_ENTRY_INCLUSION_TYPE"; + public static final String URL_NARRATIVE_LINK = "http://hl7.org/fhir/StructureDefinition/narrativeLink"; + private final List myGenerationStrategies; private final FhirContext myFhirContext; /** * Constructor */ - public IpsGeneratorSvcImpl( - FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { - myGenerationStrategy = theGenerationStrategy; - myDaoRegistry = theDaoRegistry; + public IpsGeneratorSvcImpl(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) { + this(theFhirContext, List.of(theGenerationStrategy)); + } + + public IpsGeneratorSvcImpl(FhirContext theFhirContext, List theIpsGenerationStrategies) { + myGenerationStrategies = theIpsGenerationStrategies; myFhirContext = theFhirContext; + + myGenerationStrategies.forEach(IIpsGenerationStrategy::initialize); } + /** + * Generate an IPS using a patient ID + */ @Override - public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId) { - IBaseResource patient = myDaoRegistry.getResourceDao("Patient").read(thePatientId, theRequestDetails); - - return generateIpsForPatient(theRequestDetails, patient); + public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId, String theProfile) { + IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile); + IBaseResource patient = strategy.fetchPatient(thePatientId, theRequestDetails); + return generateIpsForPatient(strategy, theRequestDetails, patient); } + /** + * Generate an IPS using a patient identifier + */ @Override - public IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier) { - SearchParameterMap searchParameterMap = - new SearchParameterMap().setLoadSynchronousUpTo(2).add(Patient.SP_IDENTIFIER, thePatientIdentifier); - IBundleProvider searchResults = - myDaoRegistry.getResourceDao("Patient").search(searchParameterMap, theRequestDetails); - - ValidateUtil.isTrueOrThrowInvalidRequest( - searchResults.sizeOrThrowNpe() > 0, "No Patient could be found matching given identifier"); - ValidateUtil.isTrueOrThrowInvalidRequest( - searchResults.sizeOrThrowNpe() == 1, "Multiple Patient resources were found matching given identifier"); - - IBaseResource patient = searchResults.getResources(0, 1).get(0); - - return generateIpsForPatient(theRequestDetails, patient); + public IBaseBundle generateIps( + RequestDetails theRequestDetails, TokenParam thePatientIdentifier, String theProfile) { + IIpsGenerationStrategy strategy = selectGenerationStrategy(theProfile); + IBaseResource patient = strategy.fetchPatient(thePatientIdentifier, theRequestDetails); + return generateIpsForPatient(strategy, theRequestDetails, patient); } - private IBaseBundle generateIpsForPatient(RequestDetails theRequestDetails, IBaseResource thePatient) { + IIpsGenerationStrategy selectGenerationStrategy(@Nullable String theRequestedProfile) { + return myGenerationStrategies.stream() + .filter(t -> isBlank(theRequestedProfile) || theRequestedProfile.equals(t.getBundleProfile())) + .findFirst() + .orElse(myGenerationStrategies.get(0)); + } + + private IBaseBundle generateIpsForPatient( + IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, IBaseResource thePatient) { IIdType originalSubjectId = myFhirContext .getVersion() .newIdType() .setValue(thePatient.getIdElement().getValue()) .toUnqualifiedVersionless(); - massageResourceId(null, thePatient); + massageResourceId(theStrategy, theRequestDetails, null, thePatient); IpsContext context = new IpsContext(thePatient, originalSubjectId); ResourceInclusionCollection globalResourcesToInclude = new ResourceInclusionCollection(); globalResourcesToInclude.addResourceIfNotAlreadyPresent(thePatient, originalSubjectId.getValue()); - IBaseResource author = myGenerationStrategy.createAuthor(); - massageResourceId(context, author); + IBaseResource author = theStrategy.createAuthor(); + massageResourceId(theStrategy, theRequestDetails, context, author); - CompositionBuilder compositionBuilder = createComposition(thePatient, context, author); - determineInclusions( - theRequestDetails, originalSubjectId, context, compositionBuilder, globalResourcesToInclude); + CompositionBuilder compositionBuilder = createComposition(theStrategy, thePatient, context, author); + determineInclusions(theStrategy, theRequestDetails, context, compositionBuilder, globalResourcesToInclude); IBaseResource composition = compositionBuilder.getComposition(); // Create the narrative for the Composition itself - CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(globalResourcesToInclude); + CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, globalResourcesToInclude); generator.populateResourceNarrative(myFhirContext, composition); - return createCompositionDocument(author, composition, globalResourcesToInclude); + return createDocumentBundleForComposition(theStrategy, author, composition, globalResourcesToInclude); } - private IBaseBundle createCompositionDocument( - IBaseResource author, IBaseResource composition, ResourceInclusionCollection theResourcesToInclude) { + private IBaseBundle createDocumentBundleForComposition( + IIpsGenerationStrategy theStrategy, + IBaseResource author, + IBaseResource composition, + ResourceInclusionCollection theResourcesToInclude) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); bundleBuilder.setType(Bundle.BundleType.DOCUMENT.toCode()); bundleBuilder.setIdentifier("urn:ietf:rfc:4122", UUID.randomUUID().toString()); @@ -162,124 +158,51 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { // Add author to document bundleBuilder.addDocumentEntry(author); - return bundleBuilder.getBundle(); + IBaseBundle retVal = bundleBuilder.getBundle(); + + theStrategy.postManipulateIpsBundle(retVal); + + return retVal; } - @Nonnull - private ResourceInclusionCollection determineInclusions( + private void determineInclusions( + IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, - IIdType originalSubjectId, - IpsContext context, + IpsContext theIpsContext, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theGlobalResourcesToInclude) { - SectionRegistry sectionRegistry = myGenerationStrategy.getSectionRegistry(); - for (SectionRegistry.Section nextSection : sectionRegistry.getSections()) { + for (Section nextSection : theStrategy.getSections()) { determineInclusionsForSection( + theStrategy, theRequestDetails, - originalSubjectId, - context, + theIpsContext, theCompositionBuilder, theGlobalResourcesToInclude, nextSection); } - return theGlobalResourcesToInclude; } private void determineInclusionsForSection( + IIpsGenerationStrategy theStrategy, RequestDetails theRequestDetails, - IIdType theOriginalSubjectId, IpsContext theIpsContext, CompositionBuilder theCompositionBuilder, - ResourceInclusionCollection theGlobalResourcesToInclude, - SectionRegistry.Section theSection) { - ResourceInclusionCollection sectionResourcesToInclude = new ResourceInclusionCollection(); - for (String nextResourceType : theSection.getResourceTypes()) { + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection) { + ResourceInclusionCollection sectionResourceCollectionToPopulate = new ResourceInclusionCollection(); + ISectionResourceSupplier resourceSupplier = theStrategy.getSectionResourceSupplier(theSection); - SearchParameterMap searchParameterMap = new SearchParameterMap(); - String subjectSp = determinePatientCompartmentSearchParameterName(nextResourceType); - searchParameterMap.add(subjectSp, new ReferenceParam(theOriginalSubjectId)); + determineInclusionsForSectionResourceTypes( + theStrategy, + theRequestDetails, + theIpsContext, + theGlobalResourceCollectionToPopulate, + theSection, + resourceSupplier, + sectionResourceCollectionToPopulate); - IpsSectionEnum sectionEnum = theSection.getSectionEnum(); - IpsContext.IpsSectionContext ipsSectionContext = - theIpsContext.newSectionContext(sectionEnum, nextResourceType); - myGenerationStrategy.massageResourceSearch(ipsSectionContext, searchParameterMap); - - Set includes = myGenerationStrategy.provideResourceSearchIncludes(ipsSectionContext); - includes.forEach(searchParameterMap::addInclude); - - IFhirResourceDao dao = myDaoRegistry.getResourceDao(nextResourceType); - IBundleProvider searchResult = dao.search(searchParameterMap, theRequestDetails); - for (int startIndex = 0; ; startIndex += CHUNK_SIZE) { - int endIndex = startIndex + CHUNK_SIZE; - List resources = searchResult.getResources(startIndex, endIndex); - if (resources.isEmpty()) { - break; - } - - for (IBaseResource nextCandidate : resources) { - - boolean candidateIsSearchInclude = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate) - == BundleEntrySearchModeEnum.INCLUDE; - boolean addResourceToBundle; - if (candidateIsSearchInclude) { - addResourceToBundle = true; - } else { - addResourceToBundle = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate); - } - - if (addResourceToBundle) { - - String originalResourceId = nextCandidate - .getIdElement() - .toUnqualifiedVersionless() - .getValue(); - - // Check if we already have this resource included so that we don't - // include it twice - IBaseResource previouslyExistingResource = - theGlobalResourcesToInclude.getResourceByOriginalId(originalResourceId); - if (previouslyExistingResource != null) { - BundleEntrySearchModeEnum candidateSearchEntryMode = - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate); - if (candidateSearchEntryMode == BundleEntrySearchModeEnum.MATCH) { - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put( - previouslyExistingResource, BundleEntrySearchModeEnum.MATCH); - } - - nextCandidate = previouslyExistingResource; - sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); - } else if (theGlobalResourcesToInclude.hasResourceWithReplacementId(originalResourceId)) { - if (!candidateIsSearchInclude) { - sectionResourcesToInclude.addResourceIfNotAlreadyPresent( - nextCandidate, originalResourceId); - } - } else { - IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, nextCandidate); - nextCandidate.setId(id); - theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent( - nextCandidate, originalResourceId); - if (!candidateIsSearchInclude) { - sectionResourcesToInclude.addResourceIfNotAlreadyPresent( - nextCandidate, originalResourceId); - } - } - } - } - } - } - - if (sectionResourcesToInclude.isEmpty() && theSection.getNoInfoGenerator() != null) { - IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId()); - String id = IdType.newRandomUuid().getValue(); - if (noInfoResource.getIdElement().isEmpty()) { - noInfoResource.setId(id); - } - ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(noInfoResource, BundleEntrySearchModeEnum.MATCH); - theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent( - noInfoResource, - noInfoResource.getIdElement().toUnqualifiedVersionless().getValue()); - sectionResourcesToInclude.addResourceIfNotAlreadyPresent(noInfoResource, id); - } + generateSectionNoInfoResourceIfNoInclusionsFound( + theIpsContext, theGlobalResourceCollectionToPopulate, theSection, sectionResourceCollectionToPopulate); /* * Update any references within the added candidates - This is important @@ -287,7 +210,23 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { * the summary, so we need to also update the references to those * resources. */ - for (IBaseResource nextResource : theGlobalResourcesToInclude.getResources()) { + updateReferencesInInclusionsForSection(theGlobalResourceCollectionToPopulate); + + if (sectionResourceCollectionToPopulate.isEmpty()) { + return; + } + + addSection( + theStrategy, + theSection, + theCompositionBuilder, + sectionResourceCollectionToPopulate, + theGlobalResourceCollectionToPopulate); + } + + private void updateReferencesInInclusionsForSection( + ResourceInclusionCollection theGlobalResourceCollectionToPopulate) { + for (IBaseResource nextResource : theGlobalResourceCollectionToPopulate.getResources()) { List references = myFhirContext.newTerser().getAllResourceReferences(nextResource); for (ResourceReferenceInfo nextReference : references) { String existingReference = nextReference @@ -298,12 +237,12 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { existingReference = new IdType(existingReference) .toUnqualifiedVersionless() .getValue(); - String replacement = theGlobalResourcesToInclude.getIdSubstitution(existingReference); + String replacement = theGlobalResourceCollectionToPopulate.getIdSubstitution(existingReference); if (isNotBlank(replacement)) { if (!replacement.equals(existingReference)) { nextReference.getResourceReference().setReference(replacement); } - } else if (theGlobalResourcesToInclude.getResourceById(existingReference) == null) { + } else if (theGlobalResourceCollectionToPopulate.getResourceById(existingReference) == null) { // If this reference doesn't point to something we have actually // included in the bundle, clear the reference. nextReference.getResourceReference().setReference(null); @@ -312,17 +251,184 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { } } } + } - if (sectionResourcesToInclude.isEmpty()) { - return; + private static void generateSectionNoInfoResourceIfNoInclusionsFound( + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection, + ResourceInclusionCollection sectionResourceCollectionToPopulate) { + if (sectionResourceCollectionToPopulate.isEmpty() && theSection.getNoInfoGenerator() != null) { + IBaseResource noInfoResource = theSection.getNoInfoGenerator().generate(theIpsContext.getSubjectId()); + String id = IdType.newRandomUuid().getValue(); + if (noInfoResource.getIdElement().isEmpty()) { + noInfoResource.setId(id); + } + noInfoResource.setUserData( + RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); + theGlobalResourceCollectionToPopulate.addResourceIfNotAlreadyPresent( + noInfoResource, + noInfoResource.getIdElement().toUnqualifiedVersionless().getValue()); + sectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(noInfoResource, id); + } + } + + private void determineInclusionsForSectionResourceTypes( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection, + ISectionResourceSupplier resourceSupplier, + ResourceInclusionCollection sectionResourceCollectionToPopulate) { + for (Class nextResourceType : theSection.getResourceTypes()) { + determineInclusionsForSectionResourceType( + theStrategy, + theRequestDetails, + theIpsContext, + theGlobalResourceCollectionToPopulate, + theSection, + nextResourceType, + resourceSupplier, + sectionResourceCollectionToPopulate); + } + } + + private void determineInclusionsForSectionResourceType( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourceCollectionToPopulate, + Section theSection, + Class nextResourceType, + ISectionResourceSupplier resourceSupplier, + ResourceInclusionCollection sectionResourceCollectionToPopulate) { + IpsSectionContext ipsSectionContext = theIpsContext.newSectionContext(theSection, nextResourceType); + + List resources = + resourceSupplier.fetchResourcesForSection(theIpsContext, ipsSectionContext, theRequestDetails); + if (resources != null) { + for (ISectionResourceSupplier.ResourceEntry nextEntry : resources) { + IBaseResource resource = nextEntry.getResource(); + Validate.isTrue( + resource.getIdElement().hasIdPart(), + "fetchResourcesForSection(..) returned resource(s) with no ID populated"); + resource.setUserData(RESOURCE_ENTRY_INCLUSION_TYPE, nextEntry.getInclusionType()); + } + addResourcesToIpsContents( + theStrategy, + theRequestDetails, + theIpsContext, + resources, + theGlobalResourceCollectionToPopulate, + sectionResourceCollectionToPopulate); + } + } + + /** + * Given a collection of resources that have been fetched, analyze them and add them as appropriate + * to the collection that will be included in a given IPS section context. + * + * @param theStrategy The generation strategy + * @param theIpsContext The overall IPS generation context for this IPS. + * @param theCandidateResources The resources that have been fetched for inclusion in the IPS bundle + */ + private void addResourcesToIpsContents( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + List theCandidateResources, + ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, + ResourceInclusionCollection theSectionResourceCollectionToPopulate) { + for (ISectionResourceSupplier.ResourceEntry nextCandidateEntry : theCandidateResources) { + if (nextCandidateEntry.getInclusionType() == ISectionResourceSupplier.InclusionTypeEnum.EXCLUDE) { + continue; + } + + IBaseResource nextCandidate = nextCandidateEntry.getResource(); + boolean primaryResource = nextCandidateEntry.getInclusionType() + == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE; + + String originalResourceId = + nextCandidate.getIdElement().toUnqualifiedVersionless().getValue(); + + // Check if we already have this resource included so that we don't + // include it twice + IBaseResource previouslyExistingResource = + theGlobalResourcesCollectionToPopulate.getResourceByOriginalId(originalResourceId); + + if (previouslyExistingResource != null) { + reuseAlreadyIncludedGlobalResourceInSectionCollection( + theSectionResourceCollectionToPopulate, + previouslyExistingResource, + primaryResource, + originalResourceId); + } else if (theGlobalResourcesCollectionToPopulate.hasResourceWithReplacementId(originalResourceId)) { + addResourceToSectionCollectionOnlyIfPrimary( + theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); + } else { + addResourceToGlobalCollectionAndSectionCollection( + theStrategy, + theRequestDetails, + theIpsContext, + theGlobalResourcesCollectionToPopulate, + theSectionResourceCollectionToPopulate, + nextCandidate, + originalResourceId, + primaryResource); + } + } + } + + private static void addResourceToSectionCollectionOnlyIfPrimary( + ResourceInclusionCollection theSectionResourceCollectionToPopulate, + boolean primaryResource, + IBaseResource nextCandidate, + String originalResourceId) { + if (primaryResource) { + theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); + } + } + + private void addResourceToGlobalCollectionAndSectionCollection( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + ResourceInclusionCollection theGlobalResourcesCollectionToPopulate, + ResourceInclusionCollection theSectionResourceCollectionToPopulate, + IBaseResource nextCandidate, + String originalResourceId, + boolean primaryResource) { + massageResourceId(theStrategy, theRequestDetails, theIpsContext, nextCandidate); + theGlobalResourcesCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); + addResourceToSectionCollectionOnlyIfPrimary( + theSectionResourceCollectionToPopulate, primaryResource, nextCandidate, originalResourceId); + } + + private static void reuseAlreadyIncludedGlobalResourceInSectionCollection( + ResourceInclusionCollection theSectionResourceCollectionToPopulate, + IBaseResource previouslyExistingResource, + boolean primaryResource, + String originalResourceId) { + IBaseResource nextCandidate; + ISectionResourceSupplier.InclusionTypeEnum previouslyIncludedResourceInclusionType = + (ISectionResourceSupplier.InclusionTypeEnum) + previouslyExistingResource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); + if (previouslyIncludedResourceInclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { + if (primaryResource) { + previouslyExistingResource.setUserData( + RESOURCE_ENTRY_INCLUSION_TYPE, ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE); + } } - addSection(theSection, theCompositionBuilder, sectionResourcesToInclude, theGlobalResourcesToInclude); + nextCandidate = previouslyExistingResource; + theSectionResourceCollectionToPopulate.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); } @SuppressWarnings("unchecked") private void addSection( - SectionRegistry.Section theSection, + IIpsGenerationStrategy theStrategy, + Section theSection, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theResourcesToInclude, ResourceInclusionCollection theGlobalResourcesToInclude) { @@ -330,34 +436,44 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection(); sectionBuilder.setTitle(theSection.getTitle()); - sectionBuilder.addCodeCoding(LOINC_URI, theSection.getSectionCode(), theSection.getSectionDisplay()); + sectionBuilder.addCodeCoding( + theSection.getSectionSystem(), theSection.getSectionCode(), theSection.getSectionDisplay()); for (IBaseResource next : theResourcesToInclude.getResources()) { - if (ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next) == BundleEntrySearchModeEnum.INCLUDE) { + ISectionResourceSupplier.InclusionTypeEnum inclusionType = + (ISectionResourceSupplier.InclusionTypeEnum) next.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); + if (inclusionType != ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { continue; } - IBaseExtension narrativeLink = ((IBaseHasExtensions) next).addExtension(); - narrativeLink.setUrl("http://hl7.org/fhir/StructureDefinition/narrativeLink"); - String narrativeLinkValue = - theCompositionBuilder.getComposition().getIdElement().getValue() - + "#" - + myFhirContext.getResourceType(next) - + "-" - + next.getIdElement().getValue(); - IPrimitiveType narrativeLinkUri = (IPrimitiveType) - myFhirContext.getElementDefinition("url").newInstance(); - narrativeLinkUri.setValueAsString(narrativeLinkValue); - narrativeLink.setValue(narrativeLinkUri); + IBaseHasExtensions extensionHolder = (IBaseHasExtensions) next; + if (extensionHolder.getExtension().stream() + .noneMatch(t -> t.getUrl().equals(URL_NARRATIVE_LINK))) { + IBaseExtension narrativeLink = extensionHolder.addExtension(); + narrativeLink.setUrl(URL_NARRATIVE_LINK); + String narrativeLinkValue = + theCompositionBuilder.getComposition().getIdElement().getValue() + + "#" + + myFhirContext.getResourceType(next) + + "-" + + next.getIdElement().getValue(); + IPrimitiveType narrativeLinkUri = + (IPrimitiveType) requireNonNull(myFhirContext.getElementDefinition("url")) + .newInstance(); + narrativeLinkUri.setValueAsString(narrativeLinkValue); + narrativeLink.setValue(narrativeLinkUri); + } sectionBuilder.addEntry(next.getIdElement()); } - String narrative = createSectionNarrative(theSection, theResourcesToInclude, theGlobalResourcesToInclude); + String narrative = + createSectionNarrative(theStrategy, theSection, theResourcesToInclude, theGlobalResourcesToInclude); sectionBuilder.setText("generated", narrative); } - private CompositionBuilder createComposition(IBaseResource thePatient, IpsContext context, IBaseResource author) { + private CompositionBuilder createComposition( + IIpsGenerationStrategy theStrategy, IBaseResource thePatient, IpsContext context, IBaseResource author) { CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext); compositionBuilder.setId(IdType.newRandomUuid()); @@ -365,43 +481,44 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { compositionBuilder.setSubject(thePatient.getIdElement().toUnqualifiedVersionless()); compositionBuilder.addTypeCoding("http://loinc.org", "60591-5", "Patient Summary Document"); compositionBuilder.setDate(InstantType.now()); - compositionBuilder.setTitle(myGenerationStrategy.createTitle(context)); - compositionBuilder.setConfidentiality(myGenerationStrategy.createConfidentiality(context)); + compositionBuilder.setTitle(theStrategy.createTitle(context)); + compositionBuilder.setConfidentiality(theStrategy.createConfidentiality(context)); compositionBuilder.addAuthor(author.getIdElement()); return compositionBuilder; } - private String determinePatientCompartmentSearchParameterName(String theResourceType) { - RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType); - Set searchParams = resourceDef.getSearchParamsForCompartmentName("Patient").stream() - .map(RuntimeSearchParam::getName) - .collect(Collectors.toSet()); - // Prefer "patient", then "subject" then anything else - if (searchParams.contains(Observation.SP_PATIENT)) { - return Observation.SP_PATIENT; - } - if (searchParams.contains(Observation.SP_SUBJECT)) { - return Observation.SP_SUBJECT; - } - return searchParams.iterator().next(); - } + private void massageResourceId( + IIpsGenerationStrategy theStrategy, + RequestDetails theRequestDetails, + IpsContext theIpsContext, + IBaseResource theResource) { + String base = theRequestDetails.getFhirServerBase(); - private void massageResourceId(IpsContext theIpsContext, IBaseResource theResource) { - IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, theResource); - theResource.setId(id); + IIdType id = theResource.getIdElement(); + if (!id.hasBaseUrl() && id.hasResourceType() && id.hasIdPart()) { + id = id.withServerBase(base, id.getResourceType()); + theResource.setId(id); + } + + id = theStrategy.massageResourceId(theIpsContext, theResource); + if (id != null) { + theResource.setId(id); + } } private String createSectionNarrative( - SectionRegistry.Section theSection, + IIpsGenerationStrategy theStrategy, + Section theSection, ResourceInclusionCollection theResources, ResourceInclusionCollection theGlobalResourceCollection) { - CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theGlobalResourceCollection); + CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theStrategy, theGlobalResourceCollection); Bundle bundle = new Bundle(); for (IBaseResource resource : theResources.getResources()) { - BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(resource); - if (searchMode == BundleEntrySearchModeEnum.MATCH) { + ISectionResourceSupplier.InclusionTypeEnum inclusionType = + (ISectionResourceSupplier.InclusionTypeEnum) resource.getUserData(RESOURCE_ENTRY_INCLUSION_TYPE); + if (inclusionType == ISectionResourceSupplier.InclusionTypeEnum.PRIMARY_RESOURCE) { bundle.addEntry().setResource((Resource) resource); } } @@ -414,14 +531,13 @@ public class IpsGeneratorSvcImpl implements IIpsGeneratorSvc { @Nonnull private CustomThymeleafNarrativeGenerator newNarrativeGenerator( - ResourceInclusionCollection theGlobalResourceCollection) { - List narrativePropertyFiles = myGenerationStrategy.getNarrativePropertyFiles(); + IIpsGenerationStrategy theStrategy, ResourceInclusionCollection theGlobalResourceCollection) { + List narrativePropertyFiles = theStrategy.getNarrativePropertyFiles(); CustomThymeleafNarrativeGenerator generator = new CustomThymeleafNarrativeGenerator(narrativePropertyFiles); generator.setFhirPathEvaluationContext(new IFhirPathEvaluationContext() { @Override public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { - IBaseResource resource = theGlobalResourceCollection.getResourceById(theReference); - return resource; + return theGlobalResourceCollection.getResourceById(theReference); } }); return generator; diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java new file mode 100644 index 00000000000..6a9368e052e --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/DefaultJpaIpsGenerationStrategy.java @@ -0,0 +1,477 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.ips.api.Section; +import ca.uhn.fhir.jpa.ips.jpa.section.AdvanceDirectivesJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.AllergyIntoleranceJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport; +import ca.uhn.fhir.jpa.ips.jpa.section.DiagnosticResultsJpaSectionSearchStrategyObservation; +import ca.uhn.fhir.jpa.ips.jpa.section.FunctionalStatusJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.IllnessHistoryJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.ImmunizationsJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicalDevicesJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationAdministration; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationDispense; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationRequest; +import ca.uhn.fhir.jpa.ips.jpa.section.MedicationSummaryJpaSectionSearchStrategyMedicationStatement; +import ca.uhn.fhir.jpa.ips.jpa.section.PlanOfCareJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.PregnancyJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.ProblemListJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.ProceduresJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.SocialHistoryJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.jpa.section.VitalSignsJpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.ips.strategy.AllergyIntoleranceNoInfoR4Generator; +import ca.uhn.fhir.jpa.ips.strategy.BaseIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.strategy.MedicationNoInfoR4Generator; +import ca.uhn.fhir.jpa.ips.strategy.ProblemNoInfoR4Generator; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.ValidateUtil; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.ClinicalImpression; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.DeviceUseStatement; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.Immunization; +import org.hl7.fhir.r4.model.MedicationAdministration; +import org.hl7.fhir.r4.model.MedicationDispense; +import org.hl7.fhir.r4.model.MedicationRequest; +import org.hl7.fhir.r4.model.MedicationStatement; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Procedure; +import org.springframework.beans.factory.annotation.Autowired; +import org.thymeleaf.util.Validate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.function.Function; + +/** + * This {@link ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy generation strategy} contains default rules for fetching + * IPS section contents for each of the base (universal realm) IPS definition sections. It fetches contents for each + * section from the JPA server repository. + *

+ * This class can be used directly, but it can also be subclassed and extended if you want to + * create an IPS strategy that is based on the defaults but add or change the inclusion rules or + * sections. If you are subclassing this class, the typical approach is to override the + * {@link #addSections()} method and replace it with your own implementation. You can include + * any of the same sections that are defined in the parent class, but you can also omit any + * you don't want to include, and add your own as well. + *

+ */ +public class DefaultJpaIpsGenerationStrategy extends BaseIpsGenerationStrategy { + + public static final String SECTION_CODE_ALLERGY_INTOLERANCE = "48765-2"; + public static final String SECTION_CODE_MEDICATION_SUMMARY = "10160-0"; + public static final String SECTION_CODE_PROBLEM_LIST = "11450-4"; + public static final String SECTION_CODE_IMMUNIZATIONS = "11369-6"; + public static final String SECTION_CODE_PROCEDURES = "47519-4"; + public static final String SECTION_CODE_MEDICAL_DEVICES = "46264-8"; + public static final String SECTION_CODE_DIAGNOSTIC_RESULTS = "30954-2"; + public static final String SECTION_CODE_VITAL_SIGNS = "8716-3"; + public static final String SECTION_CODE_PREGNANCY = "10162-6"; + public static final String SECTION_CODE_SOCIAL_HISTORY = "29762-2"; + public static final String SECTION_CODE_ILLNESS_HISTORY = "11348-0"; + public static final String SECTION_CODE_FUNCTIONAL_STATUS = "47420-5"; + public static final String SECTION_CODE_PLAN_OF_CARE = "18776-5"; + public static final String SECTION_CODE_ADVANCE_DIRECTIVES = "42348-3"; + public static final String SECTION_SYSTEM_LOINC = ITermLoaderSvc.LOINC_URI; + private final List> myGlobalSectionCustomizers = new ArrayList<>(); + + @Autowired + private DaoRegistry myDaoRegistry; + + @Autowired + private FhirContext myFhirContext; + + private boolean myInitialized; + + public void setDaoRegistry(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; + } + + public void setFhirContext(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + /** + * Subclasses may call this method to add customers that will customize every section + * added to the strategy. + */ + public void addGlobalSectionCustomizer(@Nonnull Function theCustomizer) { + Validate.isTrue(!myInitialized, "This method must not be called after the strategy is initialized"); + Validate.notNull(theCustomizer, "theCustomizer must not be null"); + myGlobalSectionCustomizers.add(theCustomizer); + } + + @Override + public final void initialize() { + Validate.isTrue(!myInitialized, "Strategy must not be initialized twice"); + Validate.isTrue(myDaoRegistry != null, "No DaoRegistry has been supplied"); + Validate.isTrue(myFhirContext != null, "No FhirContext has been supplied"); + addSections(); + myInitialized = true; + } + + @Nonnull + @Override + public IBaseResource fetchPatient(IIdType thePatientId, RequestDetails theRequestDetails) { + return myDaoRegistry.getResourceDao("Patient").read(thePatientId, theRequestDetails); + } + + @Nonnull + @Override + public IBaseResource fetchPatient(TokenParam thePatientIdentifier, RequestDetails theRequestDetails) { + SearchParameterMap searchParameterMap = + new SearchParameterMap().setLoadSynchronousUpTo(2).add(Patient.SP_IDENTIFIER, thePatientIdentifier); + IBundleProvider searchResults = + myDaoRegistry.getResourceDao("Patient").search(searchParameterMap, theRequestDetails); + + ValidateUtil.isTrueOrThrowResourceNotFound( + searchResults.sizeOrThrowNpe() > 0, "No Patient could be found matching given identifier"); + ValidateUtil.isTrueOrThrowInvalidRequest( + searchResults.sizeOrThrowNpe() == 1, "Multiple Patient resources were found matching given identifier"); + + return searchResults.getResources(0, 1).get(0); + } + + /** + * Add the various sections to the registry in order. This method can be overridden for + * customization. + */ + protected void addSections() { + addJpaSectionAllergyIntolerance(); + addJpaSectionMedicationSummary(); + addJpaSectionProblemList(); + addJpaSectionImmunizations(); + addJpaSectionProcedures(); + addJpaSectionMedicalDevices(); + addJpaSectionDiagnosticResults(); + addJpaSectionVitalSigns(); + addJpaSectionPregnancy(); + addJpaSectionSocialHistory(); + addJpaSectionIllnessHistory(); + addJpaSectionFunctionalStatus(); + addJpaSectionPlanOfCare(); + addJpaSectionAdvanceDirectives(); + } + + protected void addJpaSectionAllergyIntolerance() { + Section section = Section.newBuilder() + .withTitle("Allergies and Intolerances") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_ALLERGY_INTOLERANCE) + .withSectionDisplay("Allergies and adverse reactions Document") + .withResourceType(AllergyIntolerance.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies") + .withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator()) + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(AllergyIntolerance.class, new AllergyIntoleranceJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionMedicationSummary() { + Section section = Section.newBuilder() + .withTitle("Medication List") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_MEDICATION_SUMMARY) + .withSectionDisplay("History of Medication use Narrative") + .withResourceType(MedicationStatement.class) + .withResourceType(MedicationRequest.class) + .withResourceType(MedicationAdministration.class) + .withResourceType(MedicationDispense.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedications") + .withNoInfoGenerator(new MedicationNoInfoR4Generator()) + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy( + MedicationAdministration.class, + new MedicationSummaryJpaSectionSearchStrategyMedicationAdministration()) + .addStrategy( + MedicationDispense.class, new MedicationSummaryJpaSectionSearchStrategyMedicationDispense()) + .addStrategy(MedicationRequest.class, new MedicationSummaryJpaSectionSearchStrategyMedicationRequest()) + .addStrategy( + MedicationStatement.class, new MedicationSummaryJpaSectionSearchStrategyMedicationStatement()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionProblemList() { + Section section = Section.newBuilder() + .withTitle("Problem List") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PROBLEM_LIST) + .withSectionDisplay("Problem list - Reported") + .withResourceType(Condition.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems") + .withNoInfoGenerator(new ProblemNoInfoR4Generator()) + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Condition.class, new ProblemListJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionImmunizations() { + Section section = Section.newBuilder() + .withTitle("History of Immunizations") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_IMMUNIZATIONS) + .withSectionDisplay("History of Immunization Narrative") + .withResourceType(Immunization.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Immunization.class, new ImmunizationsJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionProcedures() { + Section section = Section.newBuilder() + .withTitle("History of Procedures") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PROCEDURES) + .withSectionDisplay("History of Procedures Document") + .withResourceType(Procedure.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Procedure.class, new ProceduresJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionMedicalDevices() { + Section section = Section.newBuilder() + .withTitle("Medical Devices") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_MEDICAL_DEVICES) + .withSectionDisplay("History of medical device use") + .withResourceType(DeviceUseStatement.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(DeviceUseStatement.class, new MedicalDevicesJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionDiagnosticResults() { + Section section = Section.newBuilder() + .withTitle("Diagnostic Results") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_DIAGNOSTIC_RESULTS) + .withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative") + .withResourceType(DiagnosticReport.class) + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(DiagnosticReport.class, new DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport()) + .addStrategy(Observation.class, new DiagnosticResultsJpaSectionSearchStrategyObservation()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionVitalSigns() { + Section section = Section.newBuilder() + .withTitle("Vital Signs") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_VITAL_SIGNS) + .withSectionDisplay("Vital signs") + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Observation.class, new VitalSignsJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionPregnancy() { + Section section = Section.newBuilder() + .withTitle("Pregnancy Information") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PREGNANCY) + .withSectionDisplay("History of pregnancies Narrative") + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Observation.class, new PregnancyJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionSocialHistory() { + Section section = Section.newBuilder() + .withTitle("Social History") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_SOCIAL_HISTORY) + .withSectionDisplay("Social history Narrative") + .withResourceType(Observation.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Observation.class, new SocialHistoryJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionIllnessHistory() { + Section section = Section.newBuilder() + .withTitle("History of Past Illness") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_ILLNESS_HISTORY) + .withSectionDisplay("History of Past illness Narrative") + .withResourceType(Condition.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Condition.class, new IllnessHistoryJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionFunctionalStatus() { + Section section = Section.newBuilder() + .withTitle("Functional Status") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_FUNCTIONAL_STATUS) + .withSectionDisplay("Functional status assessment note") + .withResourceType(ClinicalImpression.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(ClinicalImpression.class, new FunctionalStatusJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionPlanOfCare() { + Section section = Section.newBuilder() + .withTitle("Plan of Care") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_PLAN_OF_CARE) + .withSectionDisplay("Plan of care note") + .withResourceType(CarePlan.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(CarePlan.class, new PlanOfCareJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSectionAdvanceDirectives() { + Section section = Section.newBuilder() + .withTitle("Advance Directives") + .withSectionSystem(SECTION_SYSTEM_LOINC) + .withSectionCode(SECTION_CODE_ADVANCE_DIRECTIVES) + .withSectionDisplay("Advance directives") + .withResourceType(Consent.class) + .withProfile( + "https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives") + .build(); + + JpaSectionSearchStrategyCollection searchStrategyCollection = JpaSectionSearchStrategyCollection.newBuilder() + .addStrategy(Consent.class, new AdvanceDirectivesJpaSectionSearchStrategy()) + .build(); + + addJpaSection(section, searchStrategyCollection); + } + + protected void addJpaSection( + Section theSection, JpaSectionSearchStrategyCollection theSectionSearchStrategyCollection) { + Section section = theSection; + for (var next : myGlobalSectionCustomizers) { + section = next.apply(section); + } + + Validate.isTrue( + theSection.getResourceTypes().size() + == theSectionSearchStrategyCollection.getResourceTypes().size(), + "Search strategy types does not match section types"); + Validate.isTrue( + new HashSet<>(theSection.getResourceTypes()) + .containsAll(theSectionSearchStrategyCollection.getResourceTypes()), + "Search strategy types does not match section types"); + + addSection( + section, + new JpaSectionResourceSupplier(theSectionSearchStrategyCollection, myDaoRegistry, myFhirContext)); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..b6294bc499e --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IJpaSectionSearchStrategy.java @@ -0,0 +1,68 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; + +/** + * Implementations of this interface are used to fetch resources to include + * for a given IPS section by performing a search in a local JPA repository. + * + * @since 7.2.0 + */ +public interface IJpaSectionSearchStrategy { + + /** + * This method can manipulate the {@link SearchParameterMap} that will + * be used to find candidate resources for the given IPS section. The map will already have + * a subject/patient parameter added to it. The map provided in {@literal theSearchParameterMap} + * will contain a subject/patient reference (e.g. ?patient=Patient/123), but no + * other parameters. This method can add other parameters. The default implementation of this + * interface performs no action. + *

+ * For example, for a Vital Signs section, the implementation might add a parameter indicating + * the parameter category=vital-signs. + *

+ * + * @param theIpsSectionContext The context, which indicates the IPS section and the resource type + * being searched for. + * @param theSearchParameterMap The map to manipulate. + */ + default void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull SearchParameterMap theSearchParameterMap) { + // no action taken by default + } + + /** + * This method will be called for each found resource candidate for inclusion in the + * IPS document. The strategy can decide whether to include it or not. Note that the + * default implementation will always return {@literal true}. + *

+ * This method is called once for every resource that is being considered for inclusion + * in an IPS section. + *

+ */ + default boolean shouldInclude(@Nonnull IpsSectionContext theIpsSectionContext, @Nonnull T theCandidate) { + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java new file mode 100644 index 00000000000..2f365aae674 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/IpsGenerationCtxConfig.java @@ -0,0 +1,25 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IpsGenerationCtxConfig {} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java new file mode 100644 index 00000000000..e50bec7c1ac --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionResourceSupplier.java @@ -0,0 +1,128 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; +import ca.uhn.fhir.jpa.ips.api.IpsContext; +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.ReferenceParam; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Coverage; +import org.thymeleaf.util.Validate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class JpaSectionResourceSupplier implements ISectionResourceSupplier { + public static final int CHUNK_SIZE = 10; + + private final JpaSectionSearchStrategyCollection mySectionSearchStrategyCollection; + private final DaoRegistry myDaoRegistry; + private final FhirContext myFhirContext; + + public JpaSectionResourceSupplier( + @Nonnull JpaSectionSearchStrategyCollection theSectionSearchStrategyCollection, + @Nonnull DaoRegistry theDaoRegistry, + @Nonnull FhirContext theFhirContext) { + Validate.notNull(theSectionSearchStrategyCollection, "theSectionSearchStrategyCollection must not be null"); + Validate.notNull(theDaoRegistry, "theDaoRegistry must not be null"); + Validate.notNull(theFhirContext, "theFhirContext must not be null"); + mySectionSearchStrategyCollection = theSectionSearchStrategyCollection; + myDaoRegistry = theDaoRegistry; + myFhirContext = theFhirContext; + } + + @Nullable + @Override + public List fetchResourcesForSection( + IpsContext theIpsContext, IpsSectionContext theIpsSectionContext, RequestDetails theRequestDetails) { + + IJpaSectionSearchStrategy searchStrategy = + mySectionSearchStrategyCollection.getSearchStrategy(theIpsSectionContext.getResourceType()); + + SearchParameterMap searchParameterMap = new SearchParameterMap(); + + String subjectSp = determinePatientCompartmentSearchParameterName(theIpsSectionContext.getResourceType()); + searchParameterMap.add(subjectSp, new ReferenceParam(theIpsContext.getSubjectId())); + + searchStrategy.massageResourceSearch(theIpsSectionContext, searchParameterMap); + + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theIpsSectionContext.getResourceType()); + IBundleProvider searchResult = dao.search(searchParameterMap, theRequestDetails); + + List retVal = null; + for (int startIndex = 0; ; startIndex += CHUNK_SIZE) { + int endIndex = startIndex + CHUNK_SIZE; + List resources = searchResult.getResources(startIndex, endIndex); + if (resources.isEmpty()) { + break; + } + + for (IBaseResource next : resources) { + if (!next.getClass().isAssignableFrom(theIpsSectionContext.getResourceType()) + || searchStrategy.shouldInclude(theIpsSectionContext, (T) next)) { + if (retVal == null) { + retVal = new ArrayList<>(); + } + InclusionTypeEnum inclusionType = + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(next) == BundleEntrySearchModeEnum.INCLUDE + ? InclusionTypeEnum.SECONDARY_RESOURCE + : InclusionTypeEnum.PRIMARY_RESOURCE; + retVal.add(new ResourceEntry(next, inclusionType)); + } + } + } + + return retVal; + } + + private String determinePatientCompartmentSearchParameterName(Class theResourceType) { + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType); + Set searchParams = resourceDef.getSearchParamsForCompartmentName("Patient").stream() + .map(RuntimeSearchParam::getName) + .collect(Collectors.toSet()); + // A few we prefer + if (searchParams.contains(Observation.SP_PATIENT)) { + return Observation.SP_PATIENT; + } + if (searchParams.contains(Observation.SP_SUBJECT)) { + return Observation.SP_SUBJECT; + } + if (searchParams.contains(Coverage.SP_BENEFICIARY)) { + return Observation.SP_SUBJECT; + } + return searchParams.iterator().next(); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategy.java similarity index 69% rename from hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java rename to hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategy.java index 7992dfefa1e..5eb41ab6f4c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategy.java @@ -17,21 +17,12 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.ips.api; +package ca.uhn.fhir.jpa.ips.jpa; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class JpaSectionSearchStrategy implements IJpaSectionSearchStrategy { + + // nothing for now, interface has default methods -public enum IpsSectionEnum { - ALLERGY_INTOLERANCE, - MEDICATION_SUMMARY, - PROBLEM_LIST, - IMMUNIZATIONS, - PROCEDURES, - MEDICAL_DEVICES, - DIAGNOSTIC_RESULTS, - VITAL_SIGNS, - ILLNESS_HISTORY, - PREGNANCY, - SOCIAL_HISTORY, - FUNCTIONAL_STATUS, - PLAN_OF_CARE, - ADVANCE_DIRECTIVES } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java new file mode 100644 index 00000000000..e281ae31844 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/JpaSectionSearchStrategyCollection.java @@ -0,0 +1,62 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class JpaSectionSearchStrategyCollection { + + private Map, Object> mySearchStrategies; + + private JpaSectionSearchStrategyCollection(Map, Object> theSearchStrategies) { + mySearchStrategies = theSearchStrategies; + } + + @SuppressWarnings("unchecked") + public IJpaSectionSearchStrategy getSearchStrategy(Class theClass) { + return (IJpaSectionSearchStrategy) mySearchStrategies.get(theClass); + } + + public Collection> getResourceTypes() { + return mySearchStrategies.keySet(); + } + + public static JpaSectionSearchStrategyCollectionBuilder newBuilder() { + return new JpaSectionSearchStrategyCollectionBuilder(); + } + + public static class JpaSectionSearchStrategyCollectionBuilder { + private Map, Object> mySearchStrategies = new HashMap<>(); + + public JpaSectionSearchStrategyCollectionBuilder addStrategy( + Class theType, IJpaSectionSearchStrategy theSearchStrategy) { + mySearchStrategies.put(theType, theSearchStrategy); + return this; + } + + public JpaSectionSearchStrategyCollection build() { + return new JpaSectionSearchStrategyCollection(mySearchStrategies); + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..501f581b8c5 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AdvanceDirectivesJpaSectionSearchStrategy.java @@ -0,0 +1,41 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Consent; + +public class AdvanceDirectivesJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Consent.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + Consent.ConsentState.ACTIVE.getSystem(), Consent.ConsentState.ACTIVE.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..f485091d80c --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/AllergyIntoleranceJpaSectionSearchStrategy.java @@ -0,0 +1,44 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.AllergyIntolerance; + +public class AllergyIntoleranceJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull AllergyIntolerance theCandidate) { + return !theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "inactive") + && !theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", "resolved") + && !theCandidate + .getVerificationStatus() + .hasCoding( + "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification", + "entered-in-error"); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java new file mode 100644 index 00000000000..8425d7cdbe3 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport.java @@ -0,0 +1,42 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.DiagnosticReport; + +public class DiagnosticResultsJpaSectionSearchStrategyDiagnosticReport + extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull DiagnosticReport theCandidate) { + if (theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.CANCELLED + || theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.ENTEREDINERROR + || theCandidate.getStatus() == DiagnosticReport.DiagnosticReportStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java new file mode 100644 index 00000000000..bb77621cef1 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/DiagnosticResultsJpaSectionSearchStrategyObservation.java @@ -0,0 +1,56 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +public class DiagnosticResultsJpaSectionSearchStrategyObservation extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CATEGORY, + new TokenOrListParam() + .addOr(new TokenParam( + "http://terminology.hl7.org/CodeSystem/observation-category", "laboratory"))); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + if (theCandidate.getStatus() == Observation.ObservationStatus.CANCELLED + || theCandidate.getStatus() == Observation.ObservationStatus.ENTEREDINERROR + || theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..6a5631dda0a --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/FunctionalStatusJpaSectionSearchStrategy.java @@ -0,0 +1,40 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.ClinicalImpression; + +public class FunctionalStatusJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull ClinicalImpression theCandidate) { + if (theCandidate.getStatus() == ClinicalImpression.ClinicalImpressionStatus.INPROGRESS + || theCandidate.getStatus() == ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR) { + return false; + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..feebd978b7d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/IllnessHistoryJpaSectionSearchStrategy.java @@ -0,0 +1,53 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Condition; + +public class IllnessHistoryJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Condition theCandidate) { + if (theCandidate + .getVerificationStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) { + return false; + } + + if (theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") + || theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") + || theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "remission")) { + return true; + } else { + return false; + } + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..7329bd038f0 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ImmunizationsJpaSectionSearchStrategy.java @@ -0,0 +1,50 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.SortOrderEnum; +import ca.uhn.fhir.rest.api.SortSpec; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Immunization; + +public class ImmunizationsJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC)); + theSearchParameterMap.addInclude(Immunization.INCLUDE_MANUFACTURER); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Immunization theCandidate) { + if (theCandidate.getStatus() == Immunization.ImmunizationStatus.ENTEREDINERROR) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..90c279a7414 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicalDevicesJpaSectionSearchStrategy.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.DeviceUseStatement; + +public class MedicalDevicesJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(DeviceUseStatement.INCLUDE_DEVICE); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull DeviceUseStatement theCandidate) { + if (theCandidate.getStatus() == DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR) { + return false; + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java new file mode 100644 index 00000000000..3cec15b99e1 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationAdministration.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationAdministration; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationAdministration + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationAdministration.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationAdministration.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationAdministration.MedicationAdministrationStatus.INPROGRESS.getSystem(), + MedicationAdministration.MedicationAdministrationStatus.INPROGRESS.toCode())) + .addOr(new TokenParam( + MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.getSystem(), + MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationAdministration.MedicationAdministrationStatus.ONHOLD.getSystem(), + MedicationAdministration.MedicationAdministrationStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java new file mode 100644 index 00000000000..fc476cf3b3f --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationDispense.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationDispense; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationDispense + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationDispense.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationDispense.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationDispense.MedicationDispenseStatus.INPROGRESS.getSystem(), + MedicationDispense.MedicationDispenseStatus.INPROGRESS.toCode())) + .addOr(new TokenParam( + MedicationDispense.MedicationDispenseStatus.UNKNOWN.getSystem(), + MedicationDispense.MedicationDispenseStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationDispense.MedicationDispenseStatus.ONHOLD.getSystem(), + MedicationDispense.MedicationDispenseStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java new file mode 100644 index 00000000000..3d51ed17d6b --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationRequest.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationRequest; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationRequest + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationRequest.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationRequest.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationRequest.MedicationRequestStatus.ACTIVE.getSystem(), + MedicationRequest.MedicationRequestStatus.ACTIVE.toCode())) + .addOr(new TokenParam( + MedicationRequest.MedicationRequestStatus.UNKNOWN.getSystem(), + MedicationRequest.MedicationRequestStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationRequest.MedicationRequestStatus.ONHOLD.getSystem(), + MedicationRequest.MedicationRequestStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java new file mode 100644 index 00000000000..8089b76d1aa --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/MedicationSummaryJpaSectionSearchStrategyMedicationStatement.java @@ -0,0 +1,54 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.MedicationStatement; + +public class MedicationSummaryJpaSectionSearchStrategyMedicationStatement + extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.addInclude(MedicationStatement.INCLUDE_MEDICATION); + theSearchParameterMap.add( + MedicationStatement.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.ACTIVE.getSystem(), + MedicationStatement.MedicationStatementStatus.ACTIVE.toCode())) + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.INTENDED.getSystem(), + MedicationStatement.MedicationStatementStatus.INTENDED.toCode())) + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.UNKNOWN.getSystem(), + MedicationStatement.MedicationStatementStatus.UNKNOWN.toCode())) + .addOr(new TokenParam( + MedicationStatement.MedicationStatementStatus.ONHOLD.getSystem(), + MedicationStatement.MedicationStatementStatus.ONHOLD.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..4656eae3136 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PlanOfCareJpaSectionSearchStrategy.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.CarePlan; + +public class PlanOfCareJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + CarePlan.SP_STATUS, + new TokenOrListParam() + .addOr(new TokenParam( + CarePlan.CarePlanStatus.ACTIVE.getSystem(), CarePlan.CarePlanStatus.ACTIVE.toCode())) + .addOr(new TokenParam( + CarePlan.CarePlanStatus.ONHOLD.getSystem(), CarePlan.CarePlanStatus.ONHOLD.toCode())) + .addOr(new TokenParam( + CarePlan.CarePlanStatus.UNKNOWN.getSystem(), + CarePlan.CarePlanStatus.UNKNOWN.toCode()))); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..20e6ee5d35f --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/PregnancyJpaSectionSearchStrategy.java @@ -0,0 +1,75 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; + +public class PregnancyJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + public static final String LOINC_CODE_PREGNANCY_STATUS = "82810-3"; + public static final String LOINC_CODE_NUMBER_BIRTHS_LIVE = "11636-8"; + public static final String LOINC_CODE_NUMBER_BIRTHS_PRETERM = "11637-6"; + public static final String LOINC_CODE_NUMBER_BIRTHS_STILL_LIVING = "11638-4"; + public static final String LOINC_CODE_NUMBER_BIRTHS_TERM = "11639-2"; + public static final String LOINC_CODE_NUMBER_BIRTHS_TOTAL = "11640-0"; + public static final String LOINC_CODE_NUMBER_ABORTIONS = "11612-9"; + public static final String LOINC_CODE_NUMBER_ABORTIONS_INDUCED = "11613-7"; + public static final String LOINC_CODE_NUMBER_ABORTIONS_SPONTANEOUS = "11614-5"; + public static final String LOINC_CODE_NUMBER_ECTOPIC_PREGNANCY = "33065-4"; + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CODE, + new TokenOrListParam() + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_PREGNANCY_STATUS)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_LIVE)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_PRETERM)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_STILL_LIVING)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_TERM)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_BIRTHS_TOTAL)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS_INDUCED)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ABORTIONS_SPONTANEOUS)) + .addOr(new TokenParam(LOINC_URI, LOINC_CODE_NUMBER_ECTOPIC_PREGNANCY))); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + if (theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..97e126049fc --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProblemListJpaSectionSearchStrategy.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Condition; + +public class ProblemListJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Condition theCandidate) { + if (theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") + || theCandidate + .getClinicalStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") + || theCandidate + .getVerificationStatus() + .hasCoding("http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..3a76d79e6a8 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/ProceduresJpaSectionSearchStrategy.java @@ -0,0 +1,40 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Procedure; + +public class ProceduresJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Procedure theCandidate) { + if (theCandidate.getStatus() == Procedure.ProcedureStatus.ENTEREDINERROR + || theCandidate.getStatus() == Procedure.ProcedureStatus.NOTDONE) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..dc1fc63f6e9 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/SocialHistoryJpaSectionSearchStrategy.java @@ -0,0 +1,54 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +public class SocialHistoryJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CATEGORY, + new TokenOrListParam() + .addOr(new TokenParam( + "http://terminology.hl7.org/CodeSystem/observation-category", "social-history"))); + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + if (theCandidate.getStatus() == Observation.ObservationStatus.PRELIMINARY) { + return false; + } + + return true; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java new file mode 100644 index 00000000000..cee888cda26 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/jpa/section/VitalSignsJpaSectionSearchStrategy.java @@ -0,0 +1,51 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.jpa.section; + +import ca.uhn.fhir.jpa.ips.api.IpsSectionContext; +import ca.uhn.fhir.jpa.ips.jpa.JpaSectionSearchStrategy; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Observation; + +public class VitalSignsJpaSectionSearchStrategy extends JpaSectionSearchStrategy { + + @Override + public void massageResourceSearch( + @Nonnull IpsSectionContext theIpsSectionContext, + @Nonnull SearchParameterMap theSearchParameterMap) { + theSearchParameterMap.add( + Observation.SP_CATEGORY, + new TokenOrListParam() + .addOr(new TokenParam( + "http://terminology.hl7.org/CodeSystem/observation-category", "vital-signs"))); + } + + @Override + public boolean shouldInclude( + @Nonnull IpsSectionContext theIpsSectionContext, @Nonnull Observation theCandidate) { + // code filtering not yet applied + return theCandidate.getStatus() != Observation.ObservationStatus.CANCELLED + && theCandidate.getStatus() != Observation.ObservationStatus.ENTEREDINERROR + && theCandidate.getStatus() != Observation.ObservationStatus.PRELIMINARY; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java index 7ee56683b49..b946a9e5222 100644 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java @@ -28,8 +28,14 @@ import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ValidateUtil; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.thymeleaf.util.Validate; public class IpsOperationProvider { @@ -38,7 +44,8 @@ public class IpsOperationProvider { /** * Constructor */ - public IpsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) { + public IpsOperationProvider(@Nonnull IIpsGeneratorSvc theIpsGeneratorSvc) { + Validate.notNull(theIpsGeneratorSvc, "theIpsGeneratorSvc must not be null"); myIpsGeneratorSvc = theIpsGeneratorSvc; } @@ -54,9 +61,12 @@ public class IpsOperationProvider { bundleType = BundleTypeEnum.DOCUMENT, typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL) - public IBaseBundle patientInstanceSummary(@IdParam IIdType thePatientId, RequestDetails theRequestDetails) { - - return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId); + public IBaseBundle patientInstanceSummary( + @IdParam IIdType thePatientId, + @OperationParam(name = "profile", min = 0, typeName = "uri") IPrimitiveType theProfile, + RequestDetails theRequestDetails) { + String profile = theProfile != null ? theProfile.getValueAsString() : null; + return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId, profile); } /** @@ -72,12 +82,20 @@ public class IpsOperationProvider { typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL) public IBaseBundle patientTypeSummary( + @OperationParam(name = "profile", min = 0, typeName = "uri") IPrimitiveType theProfile, @Description( shortDefinition = "When the logical id of the patient is not used, servers MAY choose to support patient selection based on provided identifier") - @OperationParam(name = "identifier", min = 0, max = 1) - TokenParam thePatientIdentifier, + @OperationParam(name = "identifier", min = 1, max = 1, typeName = "Identifier") + IBase thePatientIdentifier, RequestDetails theRequestDetails) { - return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientIdentifier); + String profile = theProfile != null ? theProfile.getValueAsString() : null; + + ValidateUtil.isTrueOrThrowInvalidRequest(thePatientIdentifier != null, "No ID or identifier supplied"); + + FhirTerser terser = theRequestDetails.getFhirContext().newTerser(); + String system = terser.getSinglePrimitiveValueOrNull(thePatientIdentifier, "system"); + String value = terser.getSinglePrimitiveValueOrNull(thePatientIdentifier, "value"); + return myIpsGeneratorSvc.generateIps(theRequestDetails, new TokenParam(system, value), profile); } } diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java new file mode 100644 index 00000000000..23d048e264c --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/AllergyIntoleranceNoInfoR4Generator.java @@ -0,0 +1,46 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Reference; + +public class AllergyIntoleranceNoInfoR4Generator implements INoInfoGenerator { + @Override + public IBaseResource generate(IIdType theSubjectId) { + AllergyIntolerance allergy = new AllergyIntolerance(); + allergy.setCode(new CodeableConcept() + .addCoding(new Coding() + .setCode("no-allergy-info") + .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") + .setDisplay("No information about allergies"))) + .setPatient(new Reference(theSubjectId)) + .setClinicalStatus(new CodeableConcept() + .addCoding(new Coding() + .setCode("active") + .setSystem("http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical"))); + return allergy; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java new file mode 100644 index 00000000000..12621839568 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/BaseIpsGenerationStrategy.java @@ -0,0 +1,130 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.ISectionResourceSupplier; +import ca.uhn.fhir.jpa.ips.api.IpsContext; +import ca.uhn.fhir.jpa.ips.api.Section; +import com.google.common.collect.Lists; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Organization; +import org.thymeleaf.util.Validate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings({"HttpUrlsUsage"}) +public abstract class BaseIpsGenerationStrategy implements IIpsGenerationStrategy { + + public static final String DEFAULT_IPS_NARRATIVES_PROPERTIES = + "classpath:ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties"; + private final List
mySections = new ArrayList<>(); + private final Map mySectionToResourceSupplier = new HashMap<>(); + + /** + * Constructor + */ + public BaseIpsGenerationStrategy() { + super(); + } + + @Override + public String getBundleProfile() { + return "http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips"; + } + + @Nonnull + @Override + public final List
getSections() { + return Collections.unmodifiableList(mySections); + } + + @Nonnull + @Override + public ISectionResourceSupplier getSectionResourceSupplier(@Nonnull Section theSection) { + return mySectionToResourceSupplier.get(theSection); + } + + /** + * This should be called once per section to add a section for inclusion in generated IPS documents. + * It should include a {@link Section} which contains static details about the section, and a {@link ISectionResourceSupplier} + * which is used to fetch resources for inclusion at runtime. + * + * @param theSection Contains static details about the section, such as the resource types it can contain, and a title. + * @param theSectionResourceSupplier The strategy object which will be used to supply content for this section at runtime. + */ + public void addSection(Section theSection, ISectionResourceSupplier theSectionResourceSupplier) { + Validate.notNull(theSection, "theSection must not be null"); + Validate.notNull(theSectionResourceSupplier, "theSectionResourceSupplier must not be null"); + Validate.isTrue( + !mySectionToResourceSupplier.containsKey(theSection), + "A section with the given profile already exists"); + + mySections.add(theSection); + mySectionToResourceSupplier.put(theSection, theSectionResourceSupplier); + } + + @Override + public List getNarrativePropertyFiles() { + return Lists.newArrayList(DEFAULT_IPS_NARRATIVES_PROPERTIES); + } + + @Override + public IBaseResource createAuthor() { + Organization organization = new Organization(); + organization + .setName("eHealthLab - University of Cyprus") + .addAddress(new Address() + .addLine("1 University Avenue") + .setCity("Nicosia") + .setPostalCode("2109") + .setCountry("CY")) + .setId(IdType.newRandomUuid()); + return organization; + } + + @Override + public String createTitle(IpsContext theContext) { + return "Patient Summary as of " + + DateTimeFormatter.ofPattern("MM/dd/yyyy").format(LocalDate.now()); + } + + @Override + public String createConfidentiality(IpsContext theIpsContext) { + return Composition.DocumentConfidentiality.N.toCode(); + } + + @Override + public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource) { + return null; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java deleted file mode 100644 index eccc26fd6f6..00000000000 --- a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java +++ /dev/null @@ -1,446 +0,0 @@ -/*- - * #%L - * HAPI FHIR JPA Server - International Patient Summary (IPS) - * %% - * Copyright (C) 2014 - 2024 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package ca.uhn.fhir.jpa.ips.strategy; - -import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; -import ca.uhn.fhir.jpa.ips.api.IpsContext; -import ca.uhn.fhir.jpa.ips.api.SectionRegistry; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.rest.api.SortOrderEnum; -import ca.uhn.fhir.rest.api.SortSpec; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; - -@SuppressWarnings({"EnhancedSwitchMigration", "HttpUrlsUsage"}) -public class DefaultIpsGenerationStrategy implements IIpsGenerationStrategy { - - public static final String DEFAULT_IPS_NARRATIVES_PROPERTIES = - "classpath:ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties"; - private SectionRegistry mySectionRegistry; - - /** - * Constructor - */ - public DefaultIpsGenerationStrategy() { - setSectionRegistry(new SectionRegistry()); - } - - @Override - public SectionRegistry getSectionRegistry() { - return mySectionRegistry; - } - - public void setSectionRegistry(SectionRegistry theSectionRegistry) { - if (!theSectionRegistry.isInitialized()) { - theSectionRegistry.initialize(); - } - mySectionRegistry = theSectionRegistry; - } - - @Override - public List getNarrativePropertyFiles() { - return Lists.newArrayList(DEFAULT_IPS_NARRATIVES_PROPERTIES); - } - - @Override - public IBaseResource createAuthor() { - Organization organization = new Organization(); - organization - .setName("eHealthLab - University of Cyprus") - .addAddress(new Address() - .addLine("1 University Avenue") - .setCity("Nicosia") - .setPostalCode("2109") - .setCountry("CY")) - .setId(IdType.newRandomUuid()); - return organization; - } - - @Override - public String createTitle(IpsContext theContext) { - return "Patient Summary as of " - + DateTimeFormatter.ofPattern("MM/dd/yyyy").format(LocalDate.now()); - } - - @Override - public String createConfidentiality(IpsContext theIpsContext) { - return Composition.DocumentConfidentiality.N.toCode(); - } - - @Override - public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @Nonnull IBaseResource theResource) { - return IdType.newRandomUuid(); - } - - @Override - public void massageResourceSearch( - IpsContext.IpsSectionContext theIpsSectionContext, SearchParameterMap theSearchParameterMap) { - switch (theIpsSectionContext.getSection()) { - case ALLERGY_INTOLERANCE: - case PROBLEM_LIST: - case PROCEDURES: - case MEDICAL_DEVICES: - case ILLNESS_HISTORY: - case FUNCTIONAL_STATUS: - return; - case IMMUNIZATIONS: - theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC)); - return; - case VITAL_SIGNS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CATEGORY, - new TokenOrListParam() - .addOr(new TokenParam( - "http://terminology.hl7.org/CodeSystem/observation-category", - "vital-signs"))); - return; - } - break; - case SOCIAL_HISTORY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CATEGORY, - new TokenOrListParam() - .addOr(new TokenParam( - "http://terminology.hl7.org/CodeSystem/observation-category", - "social-history"))); - return; - } - break; - case DIAGNOSTIC_RESULTS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) { - return; - } else if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CATEGORY, - new TokenOrListParam() - .addOr(new TokenParam( - "http://terminology.hl7.org/CodeSystem/observation-category", - "laboratory"))); - return; - } - break; - case PREGNANCY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - theSearchParameterMap.add( - Observation.SP_CODE, - new TokenOrListParam() - .addOr(new TokenParam(LOINC_URI, "82810-3")) - .addOr(new TokenParam(LOINC_URI, "11636-8")) - .addOr(new TokenParam(LOINC_URI, "11637-6")) - .addOr(new TokenParam(LOINC_URI, "11638-4")) - .addOr(new TokenParam(LOINC_URI, "11639-2")) - .addOr(new TokenParam(LOINC_URI, "11640-0")) - .addOr(new TokenParam(LOINC_URI, "11612-9")) - .addOr(new TokenParam(LOINC_URI, "11613-7")) - .addOr(new TokenParam(LOINC_URI, "11614-5")) - .addOr(new TokenParam(LOINC_URI, "33065-4"))); - return; - } - break; - case MEDICATION_SUMMARY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationStatement.name())) { - theSearchParameterMap.add( - MedicationStatement.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.ACTIVE.getSystem(), - MedicationStatement.MedicationStatementStatus.ACTIVE.toCode())) - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.INTENDED.getSystem(), - MedicationStatement.MedicationStatementStatus.INTENDED.toCode())) - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.UNKNOWN.getSystem(), - MedicationStatement.MedicationStatementStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationStatement.MedicationStatementStatus.ONHOLD.getSystem(), - MedicationStatement.MedicationStatementStatus.ONHOLD.toCode()))); - return; - } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationRequest.name())) { - theSearchParameterMap.add( - MedicationRequest.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationRequest.MedicationRequestStatus.ACTIVE.getSystem(), - MedicationRequest.MedicationRequestStatus.ACTIVE.toCode())) - .addOr(new TokenParam( - MedicationRequest.MedicationRequestStatus.UNKNOWN.getSystem(), - MedicationRequest.MedicationRequestStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationRequest.MedicationRequestStatus.ONHOLD.getSystem(), - MedicationRequest.MedicationRequestStatus.ONHOLD.toCode()))); - return; - } else if (theIpsSectionContext - .getResourceType() - .equals(ResourceType.MedicationAdministration.name())) { - theSearchParameterMap.add( - MedicationAdministration.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationAdministration.MedicationAdministrationStatus.INPROGRESS - .getSystem(), - MedicationAdministration.MedicationAdministrationStatus.INPROGRESS - .toCode())) - .addOr(new TokenParam( - MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.getSystem(), - MedicationAdministration.MedicationAdministrationStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationAdministration.MedicationAdministrationStatus.ONHOLD.getSystem(), - MedicationAdministration.MedicationAdministrationStatus.ONHOLD.toCode()))); - return; - } else if (theIpsSectionContext.getResourceType().equals(ResourceType.MedicationDispense.name())) { - theSearchParameterMap.add( - MedicationDispense.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - MedicationDispense.MedicationDispenseStatus.INPROGRESS.getSystem(), - MedicationDispense.MedicationDispenseStatus.INPROGRESS.toCode())) - .addOr(new TokenParam( - MedicationDispense.MedicationDispenseStatus.UNKNOWN.getSystem(), - MedicationDispense.MedicationDispenseStatus.UNKNOWN.toCode())) - .addOr(new TokenParam( - MedicationDispense.MedicationDispenseStatus.ONHOLD.getSystem(), - MedicationDispense.MedicationDispenseStatus.ONHOLD.toCode()))); - return; - } - break; - case PLAN_OF_CARE: - if (theIpsSectionContext.getResourceType().equals(ResourceType.CarePlan.name())) { - theSearchParameterMap.add( - CarePlan.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - CarePlan.CarePlanStatus.ACTIVE.getSystem(), - CarePlan.CarePlanStatus.ACTIVE.toCode())) - .addOr(new TokenParam( - CarePlan.CarePlanStatus.ONHOLD.getSystem(), - CarePlan.CarePlanStatus.ONHOLD.toCode())) - .addOr(new TokenParam( - CarePlan.CarePlanStatus.UNKNOWN.getSystem(), - CarePlan.CarePlanStatus.UNKNOWN.toCode()))); - return; - } - break; - case ADVANCE_DIRECTIVES: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Consent.name())) { - theSearchParameterMap.add( - Consent.SP_STATUS, - new TokenOrListParam() - .addOr(new TokenParam( - Consent.ConsentState.ACTIVE.getSystem(), - Consent.ConsentState.ACTIVE.toCode()))); - return; - } - break; - } - - // Shouldn't happen: This means none of the above switches handled the Section+resourceType combination - assert false - : "Don't know how to handle " + theIpsSectionContext.getSection() + "/" - + theIpsSectionContext.getResourceType(); - } - - @Nonnull - @Override - public Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext) { - switch (theIpsSectionContext.getSection()) { - case MEDICATION_SUMMARY: - if (ResourceType.MedicationStatement.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationStatement.INCLUDE_MEDICATION); - } - if (ResourceType.MedicationRequest.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationRequest.INCLUDE_MEDICATION); - } - if (ResourceType.MedicationAdministration.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationAdministration.INCLUDE_MEDICATION); - } - if (ResourceType.MedicationDispense.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(MedicationDispense.INCLUDE_MEDICATION); - } - break; - case MEDICAL_DEVICES: - if (ResourceType.DeviceUseStatement.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(DeviceUseStatement.INCLUDE_DEVICE); - } - break; - case IMMUNIZATIONS: - if (ResourceType.Immunization.name().equals(theIpsSectionContext.getResourceType())) { - return Sets.newHashSet(Immunization.INCLUDE_MANUFACTURER); - } - break; - case ALLERGY_INTOLERANCE: - case PROBLEM_LIST: - case PROCEDURES: - case DIAGNOSTIC_RESULTS: - case VITAL_SIGNS: - case ILLNESS_HISTORY: - case PREGNANCY: - case SOCIAL_HISTORY: - case FUNCTIONAL_STATUS: - case PLAN_OF_CARE: - case ADVANCE_DIRECTIVES: - break; - } - return Collections.emptySet(); - } - - @SuppressWarnings("EnhancedSwitchMigration") - @Override - public boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate) { - - switch (theIpsSectionContext.getSection()) { - case MEDICATION_SUMMARY: - case PLAN_OF_CARE: - case ADVANCE_DIRECTIVES: - return true; - case ALLERGY_INTOLERANCE: - if (theIpsSectionContext.getResourceType().equals(ResourceType.AllergyIntolerance.name())) { - AllergyIntolerance allergyIntolerance = (AllergyIntolerance) theCandidate; - return !allergyIntolerance - .getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "inactive") - && !allergyIntolerance - .getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical", - "resolved") - && !allergyIntolerance - .getVerificationStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/allergyintolerance-verification", - "entered-in-error"); - } - break; - case PROBLEM_LIST: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) { - Condition prob = (Condition) theCandidate; - return !prob.getClinicalStatus() - .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") - && !prob.getClinicalStatus() - .hasCoding("http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") - && !prob.getVerificationStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-ver-status", - "entered-in-error"); - } - break; - case IMMUNIZATIONS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Immunization.name())) { - Immunization immunization = (Immunization) theCandidate; - return immunization.getStatus() != Immunization.ImmunizationStatus.ENTEREDINERROR; - } - break; - case PROCEDURES: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Procedure.name())) { - Procedure proc = (Procedure) theCandidate; - return proc.getStatus() != Procedure.ProcedureStatus.ENTEREDINERROR - && proc.getStatus() != Procedure.ProcedureStatus.NOTDONE; - } - break; - case MEDICAL_DEVICES: - if (theIpsSectionContext.getResourceType().equals(ResourceType.DeviceUseStatement.name())) { - DeviceUseStatement deviceUseStatement = (DeviceUseStatement) theCandidate; - return deviceUseStatement.getStatus() != DeviceUseStatement.DeviceUseStatementStatus.ENTEREDINERROR; - } - return true; - case DIAGNOSTIC_RESULTS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.DiagnosticReport.name())) { - return true; - } - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case VITAL_SIGNS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case ILLNESS_HISTORY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Condition.name())) { - Condition prob = (Condition) theCandidate; - if (prob.getVerificationStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-ver-status", "entered-in-error")) { - return false; - } else { - return prob.getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-clinical", "inactive") - || prob.getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-clinical", "resolved") - || prob.getClinicalStatus() - .hasCoding( - "http://terminology.hl7.org/CodeSystem/condition-clinical", - "remission"); - } - } - break; - case PREGNANCY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case SOCIAL_HISTORY: - if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) { - // code filtering not yet applied - Observation observation = (Observation) theCandidate; - return (observation.getStatus() != Observation.ObservationStatus.PRELIMINARY); - } - break; - case FUNCTIONAL_STATUS: - if (theIpsSectionContext.getResourceType().equals(ResourceType.ClinicalImpression.name())) { - ClinicalImpression clinicalImpression = (ClinicalImpression) theCandidate; - return clinicalImpression.getStatus() != ClinicalImpression.ClinicalImpressionStatus.INPROGRESS - && clinicalImpression.getStatus() - != ClinicalImpression.ClinicalImpressionStatus.ENTEREDINERROR; - } - break; - } - - return true; - } -} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java new file mode 100644 index 00000000000..9d6861a160d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/MedicationNoInfoR4Generator.java @@ -0,0 +1,45 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.MedicationStatement; +import org.hl7.fhir.r4.model.Reference; + +public class MedicationNoInfoR4Generator implements INoInfoGenerator { + @Override + public IBaseResource generate(IIdType theSubjectId) { + MedicationStatement medication = new MedicationStatement(); + // setMedicationCodeableConcept is not available + medication + .setMedication(new CodeableConcept() + .addCoding(new Coding() + .setCode("no-medication-info") + .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") + .setDisplay("No information about medications"))) + .setSubject(new Reference(theSubjectId)) + .setStatus(MedicationStatement.MedicationStatementStatus.UNKNOWN); + return medication; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java new file mode 100644 index 00000000000..4d69eb1312a --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/ProblemNoInfoR4Generator.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.ips.strategy; + +import ca.uhn.fhir.jpa.ips.api.INoInfoGenerator; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Reference; + +public class ProblemNoInfoR4Generator implements INoInfoGenerator { + @Override + public IBaseResource generate(IIdType theSubjectId) { + Condition condition = new Condition(); + condition + .setCode(new CodeableConcept() + .addCoding(new Coding() + .setCode("no-problem-info") + .setSystem("http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips") + .setDisplay("No information about problems"))) + .setSubject(new Reference(theSubjectId)) + .setClinicalStatus(new CodeableConcept() + .addCoding(new Coding() + .setCode("active") + .setSystem("http://terminology.hl7.org/CodeSystem/condition-clinical"))); + return condition; + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html index c9bfed14888..1855612b60b 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html @@ -6,8 +6,8 @@ Action Controlled: Consent.provision.action[x].{ text || coding[x].display (sepa Date: Consent.dateTime */-->
+
Advance Directives
- @@ -21,9 +21,9 @@ Date: Consent.dateTime - + - + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html index 5036bfc713d..728afbbd7a0 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html @@ -8,8 +8,8 @@ Severity: AllergyIntolerance.reaction.severity[x].code (separated by
) Comments: AllergyIntolerance.note[x].text (separated by
) */-->
+
Allergies And Intolerances
Advance Directives
Scope
ScopeScope StatusAction ControlledAction Controlled Date
- @@ -27,10 +27,10 @@ Comments: AllergyIntolerance.note[x].text (separated by
) - - - - + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html index 79c6015430a..560007cd56d 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html @@ -14,61 +14,75 @@ Code: DiagnosticReport.code.text || DiagnosticReport.code.coding[x].display (sep Date: DiagnosticReport.effectiveDateTime || DiagnosticReport.effectivePeriod.start */-->
-
Allergies And Intolerances
Allergen
Allergen StatusCategoryReactionSeverityCommentsCategoryReactionSeverityComments Onset
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Diagnostic Results: Observations
CodeResultUnitInterpretationReference RangeCommentsDate
CodeResultUnitInterpretationReference RangeCommentsDate
+ +
Diagnostic Results: Observations
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeResultUnitInterpretationReference RangeCommentsDate
Code + ResultUnit + Interpretation + + Reference + Range + CommentsDate
+
- - - - - - - - - - - - - - - - - - - - - - -
Diagnostic Results: Diagnostic Reports
CodeDate
DeviceDate
+ +
Diagnostic Results: Diagnostic Reports
+ + + + + + + + + + + + + + + + + + + + + +
CodeDate
Device + Date
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html index 30addeade65..a569efadf7b 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html @@ -7,8 +7,8 @@ Comments: ClinicalImpression.note[x].text (separated by
) Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod.start */-->
+
Functional Status
- @@ -23,11 +23,11 @@ Date: ClinicalImpression.effectiveDateTime || ClinicalImpression.effectivePeriod - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html index 6e1aa367549..9e4bfcc6a96 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html @@ -5,8 +5,8 @@ Comments: Procedure.note[x].text(separated by
) Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“ && Procedure.performedPeriod.end || Procedure.performedAge || Procedure.performedRange.low && “-“ && Procedure.performedRange.high || Procedure.performedString */-->
+
History Of Procedures
Functional Status
Assessment
AssessmentAssessment Status FindingCommentsDateCommentsDate
- @@ -19,9 +19,9 @@ Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“ - - - + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html index 2386d815b38..e21ca2e1ae2 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html @@ -9,8 +9,8 @@ Comments: Immunization.note[x].text (separated by
) Date: Immunization.occurrenceDateTime || Immunization.occurrenceString */-->
+
Immunizations
History Of Procedures
Procedure
ProcedureCommentsDateProcedureCommentsDate
- @@ -27,13 +27,13 @@ Date: Immunization.occurrenceDateTime || Immunization.occurrenceString - + - - + + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html index fe23a21d5fd..73902b88bab 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html @@ -6,8 +6,8 @@ Comments: DeviceUseStatement.note[x].text (separated by
) Date Recorded: DeviceUseStatement.recordedDateTime */-->
+
Medical Devices
Immunizations
Immunization
ImmunizationImmunization StatusCommentsManufacturerCommentsManufacturer Lot NumberCommentsDateCommentsDate
- @@ -21,10 +21,10 @@ Date Recorded: DeviceUseStatement.recordedDateTime - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html index ade250ed8ee..ed4a8f128d7 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html @@ -16,63 +16,78 @@ Sig: MedicationStatement.dosage[x].text (display all sigs separated by
) Date: MedicationStatement.effectiveDateTime || MedicationStatement.effectivePeriod.start */-->
-
Medical Devices
Device
DeviceDevice StatusCommentsDate RecordedCommentsDate Recorded
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Medication Summary: Medication Requests
MedicationStatusRouteSigCommentsAuthored Date
MedicationStatusRouteSigCommentsAuthored Date
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Medication Summary: Medication Statements
MedicationStatusRouteSigDate
MedicationStatusRouteSigDate
+ +
Medication Summary: Medication Requests
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MedicationStatusRouteSigCommentsAuthored Date
+ Medication + Status + Route + + Sig + CommentsAuthored Date
+
+ + +
Medication Summary: Medication Statements
+ + + + + + + + + + + + + + + + + + + + + + + + + +
MedicationStatusRouteSigDate
+ Medication + StatusRouteSigDate
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html index 0f3223999f7..11f06d54bf5 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html @@ -6,8 +6,8 @@ Comments: Condition.note[x].text (separated by
) Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString */-->
+
Past History of Illnesses
- @@ -21,10 +21,10 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html index 3b43317f4d3..d87170fac24 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html @@ -7,8 +7,8 @@ Planned Start: CarePlan.period.start Planned End: CarePlan.period.end */-->
+
Plan of Care
Past History of Illnesses
Medical Problems
Medical ProblemMedical Problem StatusCommentsOnset DateCommentsOnset Date
- @@ -25,7 +25,7 @@ Planned End: CarePlan.period.end - + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html index b4fd5a3a3ae..d939053c3eb 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html @@ -6,8 +6,8 @@ Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */-->
+
Pregnancy
Plan of Care
Activity
Activity IntentCommentsComments Planned Start Planned End
- @@ -21,10 +21,10 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start - - - - + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html index 02a6ab4ed76..5c0e796bd5c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html @@ -6,8 +6,8 @@ Comments: Condition.note[x].text (separated by
) Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && Condition.onsetPeriod.end || Condition.onsetAge || Condition.onsetRange.low && “-“ && Condition.onsetRange.high || Condition.onsetString */-->
+
Problem List
Pregnancy
Code
CodeResultCommentsDateCodeResultCommentsDate
- @@ -21,10 +21,10 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ && - + - - + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html index 9472f17f9b4..de7ba86c8b8 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html @@ -7,8 +7,8 @@ Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */-->
+
Social History
Problem List
Medical Problems
Medical ProblemsMedical Problems StatusCommentsOnset DateCommentsOnset Date
- @@ -23,11 +23,11 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start - - - - - + + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html index 89c01826383..f890ad6c26e 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html @@ -8,8 +8,8 @@ Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */-->
+
Vital Signs
Social History
Code
CodeResultUnitCommentsDateCodeResultUnitCommentsDate
- @@ -25,12 +25,12 @@ Date: Observation.effectiveDateTime || Observation.effectivePeriod.start - - - - - - + + + + + + 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 fc9711c1331..bb9bf90b283 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 @@ -7,14 +7,17 @@ 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; -import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.util.ClasspathUtil; -import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; +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.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -38,19 +41,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.Matchers.stringContainsInOrder; 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; -import static org.junit.jupiter.api.Assertions.fail; /** * This test uses a complete R4 JPA server as a backend and wires the @@ -98,8 +99,8 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { // Verify validateDocument(output); assertEquals(117, output.getEntry().size()); - String patientId = findFirstEntryResource(output, Patient.class, 1).getId(); - assertThat(patientId, matchesPattern("urn:uuid:.*")); + String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue(); + assertEquals("Patient/f15d2419-fbff-464a-826d-0afe8f095771", patientId); MedicationStatement medicationStatement = findFirstEntryResource(output, MedicationStatement.class, 2); assertEquals(patientId, medicationStatement.getSubject().getReference()); assertNull(medicationStatement.getInformationSource().getReference()); @@ -185,8 +186,8 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { // Verify validateDocument(output); assertEquals(7, output.getEntry().size()); - String patientId = findFirstEntryResource(output, Patient.class, 1).getId(); - assertThat(patientId, matchesPattern("urn:uuid:.*")); + String patientId = findFirstEntryResource(output, Patient.class, 1).getIdElement().toUnqualifiedVersionless().getValue(); + assertEquals("Patient/5342998", patientId); assertEquals(patientId, findEntryResource(output, Condition.class, 0, 2).getSubject().getReference()); assertEquals(patientId, findEntryResource(output, Condition.class, 1, 2).getSubject().getReference()); @@ -279,18 +280,9 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { instanceValidator.setValidationSupport(new ValidationSupportChain(new IpsTerminologySvc(), myFhirContext.getValidationSupport())); validator.registerValidatorModule(instanceValidator); ValidationResult validation = validator.validateWithResult(theOutcome); - assertTrue(validation.isSuccessful(), () -> myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(validation.toOperationOutcome())); - // Make sure that all refs have been replaced with UUIDs - List references = myFhirContext.newTerser().getAllResourceReferences(theOutcome); - for (IBaseResource next : myFhirContext.newTerser().getAllEmbeddedResources(theOutcome, true)) { - references.addAll(myFhirContext.newTerser().getAllResourceReferences(next)); - } - for (ResourceReferenceInfo next : references) { - if (!next.getResourceReference().getReferenceElement().getValue().startsWith("urn:uuid:")) { - fail(next.getName()); - } - } + Optional failure = validation.getMessages().stream().filter(t -> t.getSeverity().ordinal() >= ResultSeverityEnum.ERROR.ordinal()).findFirst(); + assertFalse(failure.isPresent(), () -> failure.get().toString()); } @Configuration @@ -298,12 +290,12 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { @Bean public IIpsGenerationStrategy ipsGenerationStrategy() { - return new DefaultIpsGenerationStrategy(); + return new DefaultJpaIpsGenerationStrategy(); } @Bean public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { - return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry); + return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy); } @Bean @@ -314,7 +306,6 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test { } - @SuppressWarnings("unchecked") private static T findFirstEntryResource(Bundle theBundle, Class theType, int theExpectedCount) { return findEntryResource(theBundle, theType, 0, theExpectedCount); } diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java index fbdaf9fa0d4..a7979e6b00d 100644 --- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java @@ -3,9 +3,10 @@ package ca.uhn.fhir.jpa.ips.generator; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.ips.api.IpsSectionEnum; -import ca.uhn.fhir.jpa.ips.api.SectionRegistry; -import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; +import ca.uhn.fhir.jpa.ips.api.IpsContext; +import ca.uhn.fhir.jpa.ips.api.Section; +import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -15,13 +16,11 @@ import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.test.utilities.HtmlUtil; import ca.uhn.fhir.util.ClasspathUtil; -import org.htmlunit.html.DomElement; -import org.htmlunit.html.DomNodeList; -import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlTable; -import org.htmlunit.html.HtmlTableRow; import com.google.common.collect.Lists; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.AllergyIntolerance; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CarePlan; @@ -61,11 +60,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.annotation.Nonnull; import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.ips.generator.IpsGenerationR4Test.findEntryResource; @@ -75,6 +74,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -114,23 +114,44 @@ public class IpsGeneratorSvcImplTest { private final FhirContext myFhirContext = FhirContext.forR4Cached(); private final DaoRegistry myDaoRegistry = new DaoRegistry(myFhirContext); private IIpsGeneratorSvc mySvc; - private DefaultIpsGenerationStrategy myStrategy; + private DefaultJpaIpsGenerationStrategy myStrategy; @BeforeEach public void beforeEach() { myDaoRegistry.setResourceDaos(Collections.emptyList()); + } - myStrategy = new DefaultIpsGenerationStrategy(); - mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy, myDaoRegistry); + private void initializeGenerationStrategy() { + initializeGenerationStrategy(List.of()); + } + + private void initializeGenerationStrategy(List> theGlobalSectionCustomizers) { + myStrategy = new DefaultJpaIpsGenerationStrategy() { + @Override + public IIdType massageResourceId(@Nullable IpsContext theIpsContext, @javax.annotation.Nonnull IBaseResource theResource) { + return IdType.newRandomUuid(); + } + }; + + myStrategy.setFhirContext(myFhirContext); + myStrategy.setDaoRegistry(myDaoRegistry); + + if (theGlobalSectionCustomizers != null) { + for (var next : theGlobalSectionCustomizers) { + myStrategy.addGlobalSectionCustomizer(next); + } + } + mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy); } @Test public void testGenerateIps() { // Setup + initializeGenerationStrategy(); registerResourceDaosForSmallPatientSet(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar")); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar"), null); // Verify ourLog.info("Generated IPS:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); @@ -165,6 +186,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testAllergyIntolerance_OnsetTypes() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); AllergyIntolerance allergy1 = new AllergyIntolerance(); @@ -191,11 +213,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ALLERGY_INTOLERANCE); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -213,6 +235,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testAllergyIntolerance_MissingElements() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); AllergyIntolerance allergy = new AllergyIntolerance(); @@ -226,11 +249,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ALLERGY_INTOLERANCE); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -242,6 +265,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testMedicationSummary_MedicationStatementWithMedicationReference() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -253,7 +277,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Bundle Contents List contentResourceTypes = toEntryResourceTypeStrings(outcome); @@ -266,14 +290,14 @@ public class IpsGeneratorSvcImplTest { // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); DomNodeList tables = narrativeHtml.getElementsByTagName("table"); - assertEquals(2, tables.size()); - HtmlTable table = (HtmlTable) tables.get(1); + assertEquals(1, tables.size()); + HtmlTable table = (HtmlTable) tables.get(0); HtmlTableRow row = table.getBodies().get(0).getRows().get(0); assertEquals("Tylenol", row.getCell(0).asNormalizedText()); assertEquals("Active", row.getCell(1).asNormalizedText()); @@ -285,6 +309,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testMedicationSummary_MedicationRequestWithNoMedication() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -298,17 +323,17 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); DomNodeList tables = narrativeHtml.getElementsByTagName("table"); - assertEquals(2, tables.size()); + assertEquals(1, tables.size()); HtmlTable table = (HtmlTable) tables.get(0); HtmlTableRow row = table.getBodies().get(0).getRows().get(0); assertEquals("", row.getCell(0).asNormalizedText()); @@ -317,22 +342,12 @@ public class IpsGeneratorSvcImplTest { assertEquals("", row.getCell(3).asNormalizedText()); } - @Nonnull - private Composition.SectionComponent findSection(Composition compositions, IpsSectionEnum sectionEnum) { - Composition.SectionComponent section = compositions - .getSection() - .stream() - .filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(sectionEnum).getTitle())) - .findFirst() - .orElseThrow(); - return section; - } - @Test public void testMedicationSummary_DuplicateSecondaryResources() { - myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null))); - // Setup Patient + initializeGenerationStrategy( + List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build()) + ); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -346,7 +361,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Bundle Contents List contentResourceTypes = toEntryResourceTypeStrings(outcome); @@ -367,9 +382,10 @@ public class IpsGeneratorSvcImplTest { */ @Test public void testMedicationSummary_ResourceAppearsAsSecondaryThenPrimary() throws IOException { - myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null))); - // Setup Patient + initializeGenerationStrategy( + List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build()) + ); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -385,7 +401,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Bundle Contents List contentResourceTypes = toEntryResourceTypeStrings(outcome); @@ -400,20 +416,61 @@ public class IpsGeneratorSvcImplTest { // Verify narrative - should have 2 rows (one for each primary MedicationStatement) Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); DomNodeList tables = narrativeHtml.getElementsByTagName("table"); - assertEquals(2, tables.size()); - HtmlTable table = (HtmlTable) tables.get(1); + assertEquals(1, tables.size()); + HtmlTable table = (HtmlTable) tables.get(0); + assertEquals(2, table.getBodies().get(0).getRows().size()); + } + + /** + * If there is no contents in one of the 2 medication summary tables it should be + * omitted + */ + @Test + public void testMedicationSummary_OmitMedicationRequestTable() throws IOException { + // Setup Patient + initializeGenerationStrategy( + List.of(t->Section.newBuilder(t).withNoInfoGenerator(null).build()) + ); + registerPatientDaoWithRead(); + + // Setup Medication + MedicationStatement + Medication medication = createSecondaryMedication(MEDICATION_ID); + MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID); + medicationStatement.addDerivedFrom().setReference(MEDICATION_STATEMENT_ID2); + MedicationStatement medicationStatement2 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement2, BundleEntrySearchModeEnum.INCLUDE); + MedicationStatement medicationStatement3 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2); + IFhirResourceDao medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class); + when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication, medicationStatement2, medicationStatement3))); + + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); + + // Verify narrative - should have 2 rows (one for each primary MedicationStatement) + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICATION_SUMMARY); + + HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); + ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); + + DomNodeList tables = narrativeHtml.getElementsByTagName("table"); + assertEquals(1, tables.size()); + HtmlTable table = (HtmlTable) tables.get(0); assertEquals(2, table.getBodies().get(0).getRows().size()); } @Test public void testMedicalDevices_DeviceUseStatementWithDevice() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -436,11 +493,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICAL_DEVICES); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_MEDICAL_DEVICES); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -457,6 +514,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testImmunizations() throws IOException { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -483,11 +541,11 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); // Verify Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.IMMUNIZATIONS); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_IMMUNIZATIONS); HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); @@ -508,6 +566,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testReferencesUpdatedInSecondaryInclusions() { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Medication + MedicationStatement @@ -545,7 +604,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); // Verify cross-references @@ -569,10 +628,10 @@ public class IpsGeneratorSvcImplTest { ourLog.info("Resource: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); verify(conditionDao, times(2)).search(any(), any()); Composition composition = (Composition) outcome.getEntry().get(0).getResource(); - Composition.SectionComponent problemListSection = findSection(composition, IpsSectionEnum.PROBLEM_LIST); + Composition.SectionComponent problemListSection = findSection(composition, DefaultJpaIpsGenerationStrategy.SECTION_CODE_PROBLEM_LIST); assertEquals(addedCondition.getId(), problemListSection.getEntry().get(0).getReference()); assertEquals(1, problemListSection.getEntry().size()); - Composition.SectionComponent illnessHistorySection = findSection(composition, IpsSectionEnum.ILLNESS_HISTORY); + Composition.SectionComponent illnessHistorySection = findSection(composition, DefaultJpaIpsGenerationStrategy.SECTION_CODE_ILLNESS_HISTORY); assertEquals(addedCondition2.getId(), illnessHistorySection.getEntry().get(0).getReference()); assertEquals(1, illnessHistorySection.getEntry().size()); } @@ -580,6 +639,7 @@ public class IpsGeneratorSvcImplTest { @Test public void testPatientIsReturnedAsAnIncludeResource() { // Setup Patient + initializeGenerationStrategy(); registerPatientDaoWithRead(); // Setup Condition @@ -605,7 +665,7 @@ public class IpsGeneratorSvcImplTest { registerRemainingResourceDaos(); // Test - Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID)); + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); List resources = outcome .getEntry() @@ -617,6 +677,30 @@ public class IpsGeneratorSvcImplTest { )); } + @Test + public void testSelectGenerator() { + IIpsGenerationStrategy strategy1 = mock(IIpsGenerationStrategy.class); + when(strategy1.getBundleProfile()).thenReturn("http://1"); + IIpsGenerationStrategy strategy2 = mock(IIpsGenerationStrategy.class); + when(strategy2.getBundleProfile()).thenReturn("http://2"); + IpsGeneratorSvcImpl svc = new IpsGeneratorSvcImpl(myFhirContext, List.of(strategy1, strategy2)); + + assertSame(strategy1, svc.selectGenerationStrategy("http://1")); + assertSame(strategy1, svc.selectGenerationStrategy(null)); + assertSame(strategy1, svc.selectGenerationStrategy("http://foo")); + assertSame(strategy2, svc.selectGenerationStrategy("http://2")); + } + + @Nonnull + private Composition.SectionComponent findSection(Composition compositions, String theSectionCode) { + return compositions + .getSection() + .stream() + .filter(t -> t.getCode().getCodingFirstRep().getCode().equals(theSectionCode)) + .findFirst() + .orElseThrow(); + } + private void registerPatientDaoWithRead() { IFhirResourceDao patientDao = registerResourceDaoWithNoData(Patient.class); Patient patient = new Patient(); @@ -674,19 +758,19 @@ public class IpsGeneratorSvcImplTest { } @Nonnull - private static Medication createSecondaryMedication(String medicationId) { + private static Medication createSecondaryMedication(String theMedicationId) { Medication medication = new Medication(); - medication.setId(new IdType(medicationId)); + medication.setId(new IdType(theMedicationId)); medication.getCode().addCoding().setDisplay("Tylenol"); ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE); return medication; } @Nonnull - private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) { + private static MedicationStatement createPrimaryMedicationStatement(String theMedicationId, String medicationStatementId) { MedicationStatement medicationStatement = new MedicationStatement(); medicationStatement.setId(medicationStatementId); - medicationStatement.setMedication(new Reference(medicationId)); + medicationStatement.setMedication(new Reference(theMedicationId)); medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE); medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral"); medicationStatement.getDosageFirstRep().setText("DAW"); diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java new file mode 100644 index 00000000000..96e6bd1c542 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProviderTest.java @@ -0,0 +1,152 @@ +package ca.uhn.fhir.jpa.ips.provider; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.UriType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class IpsOperationProviderTest { + + @Mock + private IIpsGeneratorSvc myIpsGeneratorSvc; + + @RegisterExtension + private RestfulServerExtension myServer = new RestfulServerExtension(FhirContext.forR4Cached()) + .withServer(t -> t.registerProviders(new IpsOperationProvider(myIpsGeneratorSvc))); + + @Captor + private ArgumentCaptor myProfileCaptor; + @Captor + private ArgumentCaptor myIdTypeCaptor; + @Captor + private ArgumentCaptor myTokenCaptor; + + @Test + public void testGenerateById() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(IIdType.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onInstance(new IdType("Patient/123")) + .named("$summary") + .withNoParameters(Parameters.class) + .returnResourceType(Bundle.class) + .execute(); + + // verify + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myIdTypeCaptor.capture(), myProfileCaptor.capture()); + assertEquals("Patient/123", myIdTypeCaptor.getValue().getValue()); + assertEquals(null, myProfileCaptor.getValue()); + } + + @Test + public void testGenerateById_WithProfile() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(IIdType.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onInstance(new IdType("Patient/123")) + .named("$summary") + .withParameter(Parameters.class, "profile", new UriType("http://foo")) + .returnResourceType(Bundle.class) + .execute(); + + // verify + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myIdTypeCaptor.capture(), myProfileCaptor.capture()); + assertEquals("Patient/123", myIdTypeCaptor.getValue().getValue()); + assertEquals("http://foo", myProfileCaptor.getValue()); + } + + @Test + public void testGenerateByIdentifier() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(TokenParam.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onType("Patient") + .named("$summary") + .withParameter(Parameters.class, "identifier", new Identifier().setSystem("http://system").setValue("value")) + .returnResourceType(Bundle.class) + .execute(); + + // verify + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myTokenCaptor.capture(), myProfileCaptor.capture()); + assertEquals("http://system", myTokenCaptor.getValue().getSystem()); + assertEquals("value", myTokenCaptor.getValue().getValue()); + assertEquals(null, myProfileCaptor.getValue()); + } + + @Test + public void testGenerateByIdentifier_WithProfile() { + // setup + + Bundle expected = new Bundle(); + expected.setType(Bundle.BundleType.DOCUMENT); + when(myIpsGeneratorSvc.generateIps(any(), any(TokenParam.class), any())).thenReturn(expected); + + // test + + Bundle actual = myServer + .getFhirClient() + .operation() + .onType("Patient") + .named("$summary") + .withParameter(Parameters.class, "identifier", new Identifier().setSystem("http://system").setValue("value")) + .andParameter("profile", new UriType("http://foo")) + .returnResourceType(Bundle.class) + .execute(); + + // verify + + assertEquals(Bundle.BundleType.DOCUMENT, actual.getType()); + verify(myIpsGeneratorSvc, times(1)).generateIps(any(), myTokenCaptor.capture(), myProfileCaptor.capture()); + assertEquals("http://system", myTokenCaptor.getValue().getSystem()); + assertEquals("value", myTokenCaptor.getValue().getValue()); + assertEquals("http://foo", myProfileCaptor.getValue()); + } + +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index ba0a5db50af..72f27397b49 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -2,15 +2,14 @@ package ca.uhn.fhirtest.config; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.r4.JpaR4Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; import ca.uhn.fhir.jpa.ips.api.IIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc; import ca.uhn.fhir.jpa.ips.generator.IpsGeneratorSvcImpl; +import ca.uhn.fhir.jpa.ips.jpa.DefaultJpaIpsGenerationStrategy; import ca.uhn.fhir.jpa.ips.provider.IpsOperationProvider; -import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; import ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgres94Dialect; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; @@ -200,13 +199,12 @@ public class TestR4Config { @Bean public IIpsGenerationStrategy ipsGenerationStrategy() { - return new DefaultIpsGenerationStrategy(); + return new DefaultJpaIpsGenerationStrategy(); } @Bean - public IIpsGeneratorSvc ipsGeneratorSvc( - FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { - return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry); + public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy) { + return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy); } @Bean diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java new file mode 100644 index 00000000000..ad71176a1f3 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtilsTest.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.narrative2; + +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Medication; +import org.hl7.fhir.dstu3.model.Patient; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NarrativeGeneratorTemplateUtilsTest { + + @Test + public void testBundleHasEntriesWithResourceType_True() { + Bundle bundle = new Bundle(); + bundle.addEntry().setResource(new Patient().setActive(true)); + bundle.addEntry().setResource(new Medication().setIsBrand(true)); + assertTrue(NarrativeGeneratorTemplateUtils.INSTANCE.bundleHasEntriesWithResourceType(bundle, "Patient")); + } + + @Test + public void testBundleHasEntriesWithResourceType_False() { + Bundle bundle = new Bundle(); + bundle.addEntry().setResource(new Medication().setIsBrand(true)); + assertFalse(NarrativeGeneratorTemplateUtils.INSTANCE.bundleHasEntriesWithResourceType(bundle, "Patient")); + } + + +}
Vital Signs
Code
CodeResultUnitInterpretationCommentsDateCodeResultUnitInterpretationCommentsDate