From d584e14048c743dafeeab3d8176b5ea714a37af0 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 24 Jan 2023 12:07:36 -0500 Subject: [PATCH] Add IPS Generator (#4438) * Begin IPS refactor * Commit work so far * Fix test bug * Fix typo * Fix typo * Narrative generator cleanup * Narrative generator working * Add test for bad reference in narrative * Tests passing * Start docs * Tests passing * Cleanup * Update cyangelog * Doc tweaks * Version bump * Address review comments * Address review comments * Build fix * Cleanup * Compile fix * Test fix * Test fix * Version bump * Build update * Test fix * Test fix * Add one utility method * Add doc --- azure-pipelines.yml | 2 + hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- .../java/ca/uhn/fhir/fhirpath/IFhirPath.java | 12 + .../fhirpath/IFhirPathEvaluationContext.java | 41 ++ .../java/ca/uhn/fhir/model/api/Include.java | 2 +- .../BaseThymeleafNarrativeGenerator.java | 303 +++++++++- .../CustomThymeleafNarrativeGenerator.java | 84 ++- .../DefaultThymeleafNarrativeGenerator.java | 22 +- .../fhir/narrative/INarrativeGenerator.java | 4 + .../narrative2/BaseNarrativeGenerator.java | 55 +- .../INarrativeTemplateManifest.java | 10 +- .../fhir/narrative2/NarrativeTemplate.java | 36 +- .../narrative2/NarrativeTemplateManifest.java | 197 +++--- .../narrative2/NullNarrativeGenerator.java | 5 + .../ThymeleafNarrativeGenerator.java | 209 ------- .../java/ca/uhn/fhir/util/BundleBuilder.java | 47 +- .../java/ca/uhn/fhir/util/ClasspathUtil.java | 15 + .../ca/uhn/fhir/util/CompositionBuilder.java | 174 ++++++ .../src/main/java/ca/uhn/fhir/util}/Logs.java | 9 +- hapi-fhir-bom/pom.xml | 9 +- hapi-fhir-checkstyle/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-api/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-app/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml | 2 +- hapi-fhir-cli/pom.xml | 2 +- hapi-fhir-client-okhttp/pom.xml | 2 +- hapi-fhir-client/pom.xml | 2 +- hapi-fhir-converter/pom.xml | 2 +- hapi-fhir-dist/pom.xml | 2 +- hapi-fhir-docs/pom.xml | 7 +- .../6_4_0/4438-add-composition-builder.yaml | 6 + .../4438-add-fhirpath-evaluation-conetxt.yaml | 5 + .../6_4_0/4438-add-ips-generator.yaml | 9 + .../4438-add-thymeleaf-fragment-support.yaml | 5 + .../ca/uhn/hapi/fhir/docs/files.properties | 1 + .../fhir/docs/model/narrative_generation.md | 35 ++ .../uhn/hapi/fhir/docs/server_jpa/elastic.md | 11 + .../ca/uhn/hapi/fhir/docs/server_jpa/ips.md | 63 ++ .../fhir/docs/server_jpa/ips/overview.svg | 1 + hapi-fhir-jacoco/pom.xml | 7 +- hapi-fhir-jaxrsserver-base/pom.xml | 2 +- hapi-fhir-jpa/pom.xml | 2 +- hapi-fhir-jpaserver-base/pom.xml | 8 +- .../jpa/batch2/JpaJobPersistenceImpl.java | 3 +- .../bulk/imprt/svc/BulkDataImportSvcImpl.java | 2 +- .../search/PersistedJpaBundleProvider.java | 3 - .../pom.xml | 2 +- hapi-fhir-jpaserver-ips/pom.xml | 48 ++ .../jpa/ips/api/IIpsGenerationStrategy.java | 136 +++++ .../ca/uhn/fhir/jpa/ips/api/IpsContext.java | 86 +++ .../uhn/fhir/jpa/ips/api/IpsSectionEnum.java | 38 ++ .../uhn/fhir/jpa/ips/api/SectionRegistry.java | 422 +++++++++++++ .../jpa/ips/generator/IIpsGeneratorSvc.java | 41 ++ .../ips/generator/IpsGeneratorSvcImpl.java | 562 ++++++++++++++++++ .../ips/provider/IpsOperationProvider.java | 75 +++ .../DefaultIpsGenerationStrategy.java | 365 ++++++++++++ .../jpa/ips/narrative/advancedirectives.html | 37 ++ .../jpa/ips/narrative/allergyintolerance.html | 43 ++ .../fhir/jpa/ips/narrative/composition.html | 11 + .../jpa/ips/narrative/diagnosticresults.html | 74 +++ .../jpa/ips/narrative/functionalstatus.html | 37 ++ .../ips/narrative/historyofprocedures.html | 31 + .../fhir/jpa/ips/narrative/immunizations.html | 43 ++ .../ips/narrative/ips-narratives.properties | 73 +++ .../jpa/ips/narrative/medicaldevices.html | 34 ++ .../jpa/ips/narrative/medicationsummary.html | 78 +++ .../ips/narrative/pasthistoryofillness.html | 34 ++ .../fhir/jpa/ips/narrative/planofcare.html | 37 ++ .../uhn/fhir/jpa/ips/narrative/pregnancy.html | 34 ++ .../fhir/jpa/ips/narrative/problemlist.html | 34 ++ .../fhir/jpa/ips/narrative/socialhistory.html | 37 ++ .../jpa/ips/narrative/utility-fragments.html | 243 ++++++++ .../fhir/jpa/ips/narrative/vitalsigns.html | 40 ++ .../jpa/ips/generator/IpsGenerationTest.java | 87 +++ .../generator/IpsGeneratorSvcImplTest.java | 465 +++++++++++++++ .../large-patient-everything.json.gz | Bin 0 -> 15551 bytes .../small-patient-everything.json.gz | Bin 0 -> 1756 bytes hapi-fhir-jpaserver-mdm/pom.xml | 2 +- hapi-fhir-jpaserver-model/pom.xml | 2 +- .../uhn/fhir/jpa/model/util/JpaConstants.java | 8 + hapi-fhir-jpaserver-searchparam/pom.xml | 2 +- hapi-fhir-jpaserver-subscription/pom.xml | 2 +- hapi-fhir-jpaserver-test-dstu2/pom.xml | 2 +- hapi-fhir-jpaserver-test-dstu3/pom.xml | 2 +- hapi-fhir-jpaserver-test-r4/pom.xml | 2 +- ...ourceDaoR4SearchCustomSearchParamTest.java | 4 + .../TerminologyLoaderSvcLoincJpaTest.java | 16 +- hapi-fhir-jpaserver-test-r4b/pom.xml | 2 +- hapi-fhir-jpaserver-test-r5/pom.xml | 2 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- .../ca/uhn/fhir/jpa/test/BaseJpaTest.java | 2 - .../ca/uhn/fhir/jpa/test/Batch2JobHelper.java | 6 + hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 2 +- hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server-openapi/pom.xml | 10 +- hapi-fhir-server/pom.xml | 2 +- .../rest/server/SimpleBundleProvider.java | 2 +- .../hapi-fhir-caching-api/pom.xml | 2 +- .../hapi-fhir-caching-caffeine/pom.xml | 4 +- .../hapi-fhir-caching-guava/pom.xml | 2 +- .../hapi-fhir-caching-testing/pom.xml | 2 +- hapi-fhir-serviceloaders/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../hapi-fhir-spring-boot-samples/pom.xml | 2 +- .../hapi-fhir-spring-boot-starter/pom.xml | 2 +- hapi-fhir-spring-boot/pom.xml | 2 +- hapi-fhir-sql-migrate/pom.xml | 2 +- hapi-fhir-storage-batch2-jobs/pom.xml | 2 +- hapi-fhir-storage-batch2/pom.xml | 2 +- .../batch2/config/Batch2JobRegisterer.java | 2 +- .../fhir/batch2/coordinator/BaseDataSink.java | 2 +- .../batch2/coordinator/FinalStepDataSink.java | 2 +- .../coordinator/JobCoordinatorImpl.java | 2 +- .../fhir/batch2/coordinator/JobDataSink.java | 2 +- .../coordinator/JobDefinitionRegistry.java | 2 +- .../batch2/coordinator/JobStepExecutor.java | 2 +- .../coordinator/ReductionStepDataSink.java | 2 +- .../coordinator/ReductionStepExecutor.java | 2 +- .../fhir/batch2/coordinator/StepExecutor.java | 2 +- .../WorkChannelMessageHandler.java | 2 +- .../coordinator/WorkChunkProcessor.java | 2 +- .../jobs/step/GenerateRangeChunksStep.java | 2 +- .../PartitionedUrlListIdChunkProducer.java | 2 +- .../batch2/jobs/step/ResourceIdListStep.java | 2 +- .../JobChunkProgressAccumulator.java | 2 +- .../maintenance/JobInstanceProcessor.java | 2 +- .../JobMaintenanceServiceImpl.java | 2 +- .../uhn/fhir/batch2/model/JobDefinition.java | 2 +- .../uhn/fhir/batch2/model/JobWorkCursor.java | 2 +- .../ca/uhn/fhir/batch2/model/StatusEnum.java | 3 +- .../batch2/progress/InstanceProgress.java | 2 +- .../JobInstanceProgressCalculator.java | 2 +- .../progress/JobInstanceStatusUpdater.java | 2 +- .../ReductionStepDataSinkTest.java | 2 +- hapi-fhir-storage-cr/pom.xml | 2 +- hapi-fhir-storage-mdm/pom.xml | 2 +- .../MdmInflateAndSubmitResourcesStep.java | 2 +- hapi-fhir-storage-test-utilities/pom.xml | 2 +- hapi-fhir-storage/pom.xml | 2 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- .../dstu3/hapi/fluentpath/FhirPathDstu3.java | 47 ++ .../NarrativeTemplateManifestTest.java | 91 +++ .../ThymeleafNarrativeGeneratorTest.java | 85 ++- .../manifest/fragment-test.properties | 9 + .../manifest/manifest-template1.html | 3 + .../manifest/manifest-template2.html | 3 + .../manifest/manifest-template3.html | 3 + .../manifest/manifest-test.properties | 14 + .../manifest/manifest2-template1.html | 3 + .../manifest/manifest2-test.properties | 6 + hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- hapi-fhir-structures-r4/pom.xml | 2 +- .../fhir/r4/hapi/fluentpath/FhirPathR4.java | 57 ++ .../uhn/fhir/util/CompositionBuilderTest.java | 99 +++ hapi-fhir-structures-r4b/pom.xml | 7 +- .../fhir/r4b/hapi/fhirpath/FhirPathR4B.java | 55 ++ hapi-fhir-structures-r5/pom.xml | 2 +- .../hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java | 107 +++- hapi-fhir-test-utilities/pom.xml | 2 +- .../ca/uhn/fhir/test/utilities/HtmlUtil.java | 8 +- .../narrative-with-fragment-child.html | 13 + .../narrative-with-fragment-parent.html | 7 + .../narrative-with-fragment.properties | 9 + ...atives-with-fhirpath-evaluate-resolve.html | 3 + ...th-fhirpath-evaluate-single-primitive.html | 3 + .../narratives-with-fhirpath.properties | 16 + hapi-fhir-testpage-overlay/pom.xml | 2 +- .../pom.xml | 2 +- hapi-fhir-validation-resources-dstu2/pom.xml | 2 +- hapi-fhir-validation-resources-dstu3/pom.xml | 2 +- hapi-fhir-validation-resources-r4/pom.xml | 2 +- hapi-fhir-validation-resources-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 2 +- hapi-tinder-plugin/pom.xml | 2 +- hapi-tinder-test/pom.xml | 2 +- pom.xml | 10 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- 186 files changed, 4984 insertions(+), 556 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java rename {hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log => hapi-fhir-base/src/main/java/ca/uhn/fhir/util}/Logs.java (74%) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg create mode 100644 hapi-fhir-jpaserver-ips/pom.xml create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java create 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/generator/IIpsGeneratorSvc.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java create mode 100644 hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java create 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/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/composition.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html create mode 100644 hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html create mode 100644 hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationTest.java create mode 100644 hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java create mode 100644 hapi-fhir-jpaserver-ips/src/test/resources/large-patient-everything.json.gz create mode 100644 hapi-fhir-jpaserver-ips/src/test/resources/small-patient-everything.json.gz create mode 100644 hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/fragment-test.properties create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template1.html create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template2.html create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template3.html create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-template1.html create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-test.properties create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/CompositionBuilderTest.java create mode 100644 hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-child.html create mode 100644 hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-parent.html create mode 100644 hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment.properties create mode 100644 hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-resolve.html create mode 100644 hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html create mode 100644 hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath.properties diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bdc13ab4116..13987de4279 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,6 +59,8 @@ stages: # module: hapi-fhir-jpaserver-base - name: hapi_fhir_jpaserver_elastic_test_utilities module: hapi-fhir-jpaserver-elastic-test-utilities + - name: hapi_fhir_jpaserver_ips + module: hapi-fhir-jpaserver-ips - name: hapi_fhir_jpaserver_mdm module: hapi-fhir-jpaserver-mdm - name: hapi_fhir_jpaserver_model diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index b13edcc35da..6be19192e0c 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 1e2050bce79..b426255dc0c 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 746e6e664c1..77391678ee0 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java index 38d7600cdfa..66ed7fbf455 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPath.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.fhirpath; import org.hl7.fhir.instance.model.api.IBase; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; @@ -52,4 +53,15 @@ public interface IFhirPath { * Parses the expression and throws an exception if it can not parse correctly */ void parse(String theExpression) throws Exception; + + + /** + * This method can be used optionally to supply an evaluation context for the + * FHIRPath evaluator instance. The context can be used to supply data needed by + * specific functions, e.g. allowing the resolve() function to + * fetch referenced resources. + * + * @since 6.4.0 + */ + void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java new file mode 100644 index 00000000000..b7feaf6d3bd --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/fhirpath/IFhirPathEvaluationContext.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.fhirpath; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IIdType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface IFhirPathEvaluationContext { + + /** + * Evaluates the resolve() function and returns the target + * of the resolution. + * + * @param theReference The reference + * @param theContext The entity containing the reference. Note that this will be null for FHIR versions R4 and below. + */ + default IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { + return null; + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java index a39edf7b2ba..2a9e81ceaac 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Include.java @@ -37,7 +37,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * upgrading servers. *

*

- * Note on thrwead safety: This class is not thread safe. + * Note on thread safety: This class is not thread safe. *

*/ public class Include implements Serializable { 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 20a8fb87bff..b1f08cd6f04 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 @@ -21,18 +21,55 @@ package ca.uhn.fhir.narrative; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fhirpath.IFhirPath; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.narrative2.NarrativeTemplateManifest; -import ca.uhn.fhir.narrative2.ThymeleafNarrativeGenerator; +import ca.uhn.fhir.narrative2.BaseNarrativeGenerator; +import ca.uhn.fhir.narrative2.INarrativeTemplate; +import ca.uhn.fhir.narrative2.TemplateTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hl7.fhir.instance.model.api.IBaseResource; +import com.google.common.collect.Sets; +import org.hl7.fhir.instance.model.api.IBase; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.cache.AlwaysValidCacheEntryValidity; +import org.thymeleaf.cache.ICacheEntryValidity; +import org.thymeleaf.context.Context; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.dialect.IDialect; +import org.thymeleaf.dialect.IExpressionObjectDialect; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.expression.IExpressionObjectFactory; +import org.thymeleaf.messageresolver.IMessageResolver; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.IProcessor; +import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; +import org.thymeleaf.processor.element.AbstractElementTagProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.standard.StandardDialect; +import org.thymeleaf.standard.expression.IStandardExpression; +import org.thymeleaf.standard.expression.IStandardExpressionParser; +import org.thymeleaf.standard.expression.StandardExpressions; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.DefaultTemplateResolver; +import org.thymeleaf.templateresolver.ITemplateResolver; +import org.thymeleaf.templateresource.ITemplateResource; +import org.thymeleaf.templateresource.StringTemplateResource; -import java.io.IOException; +import java.util.EnumSet; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; -public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrativeGenerator { +import static org.apache.commons.lang3.StringUtils.isNotBlank; - private boolean myInitialized; +public abstract class BaseThymeleafNarrativeGenerator extends BaseNarrativeGenerator { + + public static final String FHIRPATH = "fhirpath"; + private IMessageResolver myMessageResolver; + private IFhirPathEvaluationContext myFhirPathEvaluationContext; /** * Constructor @@ -41,32 +78,244 @@ public abstract class BaseThymeleafNarrativeGenerator extends ThymeleafNarrative super(); } + public void setFhirPathEvaluationContext(IFhirPathEvaluationContext theFhirPathEvaluationContext) { + myFhirPathEvaluationContext = theFhirPathEvaluationContext; + } + + private TemplateEngine getTemplateEngine(FhirContext theFhirContext) { + TemplateEngine engine = new TemplateEngine(); + ITemplateResolver resolver = new NarrativeTemplateResolver(theFhirContext); + engine.setTemplateResolver(resolver); + if (myMessageResolver != null) { + engine.setMessageResolver(myMessageResolver); + } + StandardDialect dialect = new StandardDialect() { + @Override + public Set getProcessors(String theDialectPrefix) { + Set retVal = super.getProcessors(theDialectPrefix); + retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix)); + retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext)); + return retVal; + } + + }; + engine.setDialect(dialect); + + engine.addDialect(new NarrativeGeneratorDialect(theFhirContext)); + return engine; + } + @Override - public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { - if (!myInitialized) { - initialize(); - } - super.populateResourceNarrative(theFhirContext, theResource); - return false; + protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) { + + Context context = new Context(); + context.setVariable("resource", theTargetContext); + context.setVariable("context", theTargetContext); + context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name()); + + return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context); } - protected abstract List getPropertyFile(); - private synchronized void initialize() { - if (myInitialized) { - return; - } - - List propFileName = getPropertyFile(); - try { - NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(propFileName); - setManifest(manifest); - } catch (IOException e) { - throw new InternalErrorException(Msg.code(1808) + e); - } - - myInitialized = true; + @Override + protected EnumSet getStyle() { + return EnumSet.of(TemplateTypeEnum.THYMELEAF); } + private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) { + IEngineConfiguration configuration = theTemplateContext.getConfiguration(); + IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration); + final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement); + Object elementValueObj = expression.execute(theTemplateContext); + final IBase elementValue = (IBase) elementValueObj; + if (elementValue == null) { + return ""; + } + + List templateOpt; + if (isNotBlank(theName)) { + templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName); + if (templateOpt.isEmpty()) { + throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName); + } + } else { + templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue); + if (templateOpt.isEmpty()) { + throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass()); + } + } + + return applyTemplate(theFhirContext, templateOpt.get(0), elementValue); + } + + public void setMessageResolver(IMessageResolver theMessageResolver) { + myMessageResolver = theMessageResolver; + } + + + private class NarrativeTemplateResolver extends DefaultTemplateResolver { + private final FhirContext myFhirContext; + + private NarrativeTemplateResolver(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + @Override + protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { + if (theOwnerTemplate == null) { + return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0; + } else { + return getManifest().getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate).size() > 0; + } + } + + @Override + protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { + return TemplateMode.XML; + } + + @Override + protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { + if (theOwnerTemplate == null) { + return getManifest() + .getTemplateByName(myFhirContext, getStyle(), theTemplate) + .stream() + .findFirst() + .map(t -> new StringTemplateResource(t.getTemplateText())) + .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate)); + } else { + return getManifest() + .getTemplateByFragmentName(myFhirContext, getStyle(), theTemplate) + .stream() + .findFirst() + .map(t -> new StringTemplateResource(t.getTemplateText())) + .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate)); + } + } + + @Override + protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { + return AlwaysValidCacheEntryValidity.INSTANCE; + } + } + + private class NarrativeTagProcessor extends AbstractElementTagProcessor { + + private final FhirContext myFhirContext; + + NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) { + super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0); + myFhirContext = theFhirContext; + } + + @Override + protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) { + String name = theTag.getAttributeValue("th:name"); + String element = theTag.getAttributeValue("th:element"); + + String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element); + theStructureHandler.replaceWith(appliedTemplate, false); + } + } + + /** + * This is a thymeleaf extension that allows people to do things like + * + */ + private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor { + + private final FhirContext myFhirContext; + + NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) { + super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true); + myFhirContext = theFhirContext; + } + + @Override + protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) { + String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue); + theStructureHandler.setBody(text, false); + } + + } + + + private class NarrativeGeneratorDialect implements IDialect, IExpressionObjectDialect { + + private final FhirContext myFhirContext; + + public NarrativeGeneratorDialect(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + @Override + public String getName() { + return "NarrativeGeneratorDialect"; + } + + + @Override + public IExpressionObjectFactory getExpressionObjectFactory() { + return new NarrativeGeneratorExpressionObjectFactory(myFhirContext); + } + } + + private class NarrativeGeneratorExpressionObjectFactory implements IExpressionObjectFactory { + + private final FhirContext myFhirContext; + + public NarrativeGeneratorExpressionObjectFactory(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + @Override + public Set getAllExpressionObjectNames() { + return Sets.newHashSet(FHIRPATH); + } + + @Override + public Object buildObject(IExpressionContext context, String expressionObjectName) { + if (FHIRPATH.equals(expressionObjectName)) { + return new NarrativeGeneratorFhirPathExpressionObject(myFhirContext); + } + return null; + } + + @Override + public boolean isCacheable(String expressionObjectName) { + return false; + } + } + + + private class NarrativeGeneratorFhirPathExpressionObject { + + private final FhirContext myFhirContext; + + public NarrativeGeneratorFhirPathExpressionObject(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + public IBase evaluateFirst(IBase theInput, String theExpression) { + IFhirPath fhirPath = newFhirPath(); + Optional output = fhirPath.evaluateFirst(theInput, theExpression, IBase.class); + return output.orElse(null); + } + + public List evaluate(IBase theInput, String theExpression) { + IFhirPath fhirPath = newFhirPath(); + return fhirPath.evaluate(theInput, theExpression, IBase.class); + } + + private IFhirPath newFhirPath() { + IFhirPath fhirPath = myFhirContext.newFhirPath(); + if (myFhirPathEvaluationContext != null) { + fhirPath.setEvaluationContext(myFhirPathEvaluationContext); + } + return fhirPath; + } + + + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java index 25ddc3835ca..7d88cdf65b2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java @@ -20,48 +20,80 @@ package ca.uhn.fhir.narrative; * #L% */ +import ca.uhn.fhir.narrative2.NarrativeTemplateManifest; +import org.apache.commons.lang3.Validate; + import java.util.Arrays; import java.util.List; -import org.apache.commons.lang3.Validate; - public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator { - private List myPropertyFile; + private volatile List myPropertyFile; + private volatile NarrativeTemplateManifest myManifest; + + /** + * Constructor. If this constructor is used you must explicitly call + * {@link #setManifest(NarrativeTemplateManifest)} to provide a template + * manifest before using the generator. + */ + public CustomThymeleafNarrativeGenerator() { + super(); + } + + /** + * Create a new narrative generator + * + * @param theNarrativePropertyFiles The name of the property file, in one of the following formats: + *
    + *
  • file:/path/to/file/file.properties
  • + *
  • classpath:/com/package/file.properties
  • + *
+ */ + public CustomThymeleafNarrativeGenerator(String... theNarrativePropertyFiles) { + this(); + setPropertyFile(theNarrativePropertyFiles); + } /** * Create a new narrative generator - * - * @param thePropertyFile - * The name of the property file, in one of the following formats: - *
    - *
  • file:/path/to/file/file.properties
  • - *
  • classpath:/com/package/file.properties
  • - *
+ * + * @param theNarrativePropertyFiles The name of the property file, in one of the following formats: + *
    + *
  • file:/path/to/file/file.properties
  • + *
  • classpath:/com/package/file.properties
  • + *
*/ - public CustomThymeleafNarrativeGenerator(String... thePropertyFile) { - super(); - setPropertyFile(thePropertyFile); + public CustomThymeleafNarrativeGenerator(List theNarrativePropertyFiles) { + this(theNarrativePropertyFiles.toArray(new String[0])); + } + + @Override + public NarrativeTemplateManifest getManifest() { + NarrativeTemplateManifest retVal = myManifest; + if (myManifest == null) { + Validate.isTrue(myPropertyFile != null, "Neither a property file or a manifest has been provided"); + retVal = NarrativeTemplateManifest.forManifestFileLocation(myPropertyFile); + setManifest(retVal); + } + return retVal; + } + + public void setManifest(NarrativeTemplateManifest theManifest) { + myManifest = theManifest; } /** * Set the property file to use - * - * @param thePropertyFile - * The name of the property file, in one of the following formats: - *
    - *
  • file:/path/to/file/file.properties
  • - *
  • classpath:/com/package/file.properties
  • - *
+ * + * @param thePropertyFile The name of the property file, in one of the following formats: + *
    + *
  • file:/path/to/file/file.properties
  • + *
  • classpath:/com/package/file.properties
  • + *
*/ public void setPropertyFile(String... thePropertyFile) { Validate.notNull(thePropertyFile, "Property file can not be null"); myPropertyFile = Arrays.asList(thePropertyFile); + myManifest = null; } - - @Override - public List getPropertyFile() { - return myPropertyFile; - } - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java index c921ace1d3c..0aff3ad66b0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.narrative; * #L% */ +import ca.uhn.fhir.narrative2.NarrativeTemplateManifest; + import java.util.ArrayList; import java.util.List; @@ -29,23 +31,29 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe static final String HAPISERVER_NARRATIVES_PROPERTIES = "classpath:ca/uhn/fhir/narrative/narratives-hapiserver.properties"; private boolean myUseHapiServerConformanceNarrative; + private volatile NarrativeTemplateManifest myManifest; public DefaultThymeleafNarrativeGenerator() { super(); } @Override - protected List getPropertyFile() { - List retVal = new ArrayList(); - retVal.add(NARRATIVES_PROPERTIES); - if (myUseHapiServerConformanceNarrative) { - retVal.add(HAPISERVER_NARRATIVES_PROPERTIES); + protected NarrativeTemplateManifest getManifest() { + NarrativeTemplateManifest retVal = myManifest; + if (retVal == null) { + List propertyFiles = new ArrayList<>(); + propertyFiles.add(NARRATIVES_PROPERTIES); + if (myUseHapiServerConformanceNarrative) { + propertyFiles.add(HAPISERVER_NARRATIVES_PROPERTIES); + } + retVal = NarrativeTemplateManifest.forManifestFileLocation(propertyFiles); + myManifest = retVal; } return retVal; } /** - * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI {@link RestfulServer} + * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI FHIR Server * instances. This narrative provides a friendly search page which can assist users of the service. */ public void setUseHapiServerConformanceNarrative(boolean theValue) { @@ -53,7 +61,7 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe } /** - * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI {@link RestfulServer} + * If set to true (default is false) a special custom narrative for the Conformance resource will be provided, which is designed to be used with HAPI FHIR Server * instances. This narrative provides a friendly search page which can assist users of the service. */ public boolean isUseHapiServerConformanceNarrative() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java index 618512635c1..754adf3c6e9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java @@ -35,4 +35,8 @@ public interface INarrativeGenerator { */ boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource); + /** + * Generates the narrative for the given resource and returns it as a string + */ + String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java index 003591e08e8..6319731ea26 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java @@ -28,10 +28,15 @@ import ca.uhn.fhir.fhirpath.IFhirPath; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.narrative.INarrativeGenerator; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.Logs; +import ch.qos.logback.classic.spi.LogbackServiceProvider; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.INarrative; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import java.util.Collections; import java.util.EnumSet; import java.util.List; @@ -43,29 +48,46 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseNarrativeGenerator implements INarrativeGenerator { - private INarrativeTemplateManifest myManifest; - - public INarrativeTemplateManifest getManifest() { - return myManifest; - } - - public void setManifest(INarrativeTemplateManifest theManifest) { - myManifest = theManifest; - } - @Override public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { - List templateOpt = getTemplateForElement(theFhirContext, theResource); - if (templateOpt.size() > 0) { - applyTemplate(theFhirContext, templateOpt.get(0), theResource); + INarrativeTemplate template = selectTemplate(theFhirContext, theResource); + if (template != null) { + applyTemplate(theFhirContext, template, theResource); return true; } return false; } - private List getTemplateForElement(FhirContext theFhirContext, IBase theElement) { - return myManifest.getTemplateByElement(theFhirContext, getStyle(), theElement); + @Nullable + private INarrativeTemplate selectTemplate(FhirContext theFhirContext, IBaseResource theResource) { + List templates = getTemplateForElement(theFhirContext, theResource); + INarrativeTemplate template = null; + if (templates.isEmpty()) { + Logs.getNarrativeGenerationTroubleshootingLog().debug("No templates match for resource of type {}", theResource.getClass()); + } else { + if (templates.size() > 1) { + Logs.getNarrativeGenerationTroubleshootingLog().debug("Multiple templates match for resource of type {} - Picking first from: {}", theResource.getClass(), templates); + } + template = templates.get(0); + Logs.getNarrativeGenerationTroubleshootingLog().debug("Selected template: {}", template); + } + return template; + } + + @Override + public String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { + INarrativeTemplate template = selectTemplate(theFhirContext, theResource); + if (template != null) { + String narrative = applyTemplate(theFhirContext, template, (IBase)theResource); + return cleanWhitespace(narrative); + } + + return null; + } + + protected List getTemplateForElement(FhirContext theFhirContext, IBase theElement) { + return getManifest().getTemplateByElement(theFhirContext, getStyle(), theElement); } private boolean applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBaseResource theResource) { @@ -208,4 +230,7 @@ public abstract class BaseNarrativeGenerator implements INarrativeGenerator { } return b.toString(); } + + protected abstract NarrativeTemplateManifest getManifest(); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java index 9b2530bd831..5f7b2a9d582 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java @@ -23,13 +23,17 @@ package ca.uhn.fhir.narrative2; import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.instance.model.api.IBase; +import javax.annotation.Nonnull; +import java.util.Collection; import java.util.EnumSet; import java.util.List; public interface INarrativeTemplateManifest { - List getTemplateByResourceName(FhirContext theFhirContext, EnumSet theStyles, String theResourceName); + List getTemplateByResourceName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theResourceName, @Nonnull Collection theProfiles); - List getTemplateByName(FhirContext theFhirContext, EnumSet theStyles, String theName); + List getTemplateByName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theName); - List getTemplateByElement(FhirContext theFhirContext, EnumSet theStyles, IBase theElementValue); + List getTemplateByElement(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull IBase theElementValue); + + List getTemplateByFragmentName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theFragmentName); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java index c03c3cd75b1..688606d9f4d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java @@ -20,30 +20,46 @@ package ca.uhn.fhir.narrative2; * #L% */ -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBase; -import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; public class NarrativeTemplate implements INarrativeTemplate { + private final Set myAppliesToProfiles = new HashSet<>(); + private final Set myAppliesToResourceTypes = new HashSet<>(); + private final Set myAppliesToDataTypes = new HashSet<>(); + private final Set> myAppliesToClasses = new HashSet<>(); + private final Set myAppliesToFragmentNames = new HashSet<>(); private String myTemplateFileName; - private Set myAppliesToProfiles = new HashSet<>(); - private Set myAppliesToResourceTypes = new HashSet<>(); - private Set myAppliesToDataTypes = new HashSet<>(); - private Set> myAppliesToClasses = new HashSet<>(); private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF; private String myContextPath; private String myTemplateName; + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SIMPLE_STYLE) + .append("name", myTemplateName) + .append("fileName", myTemplateFileName) + .toString(); + } + public Set getAppliesToDataTypes() { return Collections.unmodifiableSet(myAppliesToDataTypes); } + public Set getAppliesToFragmentNames() { + return Collections.unmodifiableSet(myAppliesToFragmentNames); + } + + void addAppliesToFragmentName(String theAppliesToFragmentName) { + myAppliesToFragmentNames.add(theAppliesToFragmentName); + } + @Override public String getContextPath() { return myContextPath; @@ -109,11 +125,7 @@ public class NarrativeTemplate implements INarrativeTemplate { @Override public String getTemplateText() { - try { - return NarrativeTemplateManifest.loadResource(getTemplateFileName()); - } catch (IOException e) { - throw new InternalErrorException(Msg.code(1866) + e); - } + return NarrativeTemplateManifest.loadResource(getTemplateFileName()); } void addAppliesToDatatype(String theDataType) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java index 7201b0d6fb4..e6b0cd80972 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java @@ -23,31 +23,28 @@ package ca.uhn.fhir.narrative2; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ClasspathUtil; import com.google.common.base.Charsets; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.StringReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; +import java.util.function.Consumer; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -55,36 +52,42 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class NarrativeTemplateManifest implements INarrativeTemplateManifest { private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class); - private final Map> myResourceTypeToTemplate; - private final Map> myDatatypeToTemplate; - private final Map> myNameToTemplate; - private final Map> myClassToTemplate; + private final ListMultimap myResourceTypeToTemplate; + private final ListMultimap myDatatypeToTemplate; + private final ListMultimap myNameToTemplate; + private final ListMultimap myFragmentNameToTemplate; + private final ListMultimap myClassToTemplate; private final int myTemplateCount; private NarrativeTemplateManifest(Collection theTemplates) { - Map> resourceTypeToTemplate = new HashMap<>(); - Map> datatypeToTemplate = new HashMap<>(); - Map> nameToTemplate = new HashMap<>(); - Map> classToTemplate = new HashMap<>(); + ListMultimap resourceTypeToTemplate = ArrayListMultimap.create(); + ListMultimap datatypeToTemplate = ArrayListMultimap.create(); + ListMultimap nameToTemplate = ArrayListMultimap.create(); + ListMultimap classToTemplate = ArrayListMultimap.create(); + ListMultimap fragmentNameToTemplate = ArrayListMultimap.create(); for (NarrativeTemplate nextTemplate : theTemplates) { - nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate); + nameToTemplate.put(nextTemplate.getTemplateName(), nextTemplate); for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) { - resourceTypeToTemplate.computeIfAbsent(nextResourceType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate); + resourceTypeToTemplate.put(nextResourceType.toUpperCase(), nextTemplate); } for (String nextDataType : nextTemplate.getAppliesToDataTypes()) { - datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate); + datatypeToTemplate.put(nextDataType.toUpperCase(), nextTemplate); } for (Class nextAppliesToClass : nextTemplate.getAppliesToClasses()) { - classToTemplate.computeIfAbsent(nextAppliesToClass.getName(), t -> new ArrayList<>()).add(nextTemplate); + classToTemplate.put(nextAppliesToClass.getName(), nextTemplate); + } + for (String nextFragmentName : nextTemplate.getAppliesToFragmentNames()) { + fragmentNameToTemplate.put(nextFragmentName, nextTemplate); } } myTemplateCount = theTemplates.size(); - myClassToTemplate = makeImmutable(classToTemplate); - myNameToTemplate = makeImmutable(nameToTemplate); - myResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate); - myDatatypeToTemplate = makeImmutable(datatypeToTemplate); + myClassToTemplate = Multimaps.unmodifiableListMultimap(classToTemplate); + myNameToTemplate = Multimaps.unmodifiableListMultimap(nameToTemplate); + myResourceTypeToTemplate = Multimaps.unmodifiableListMultimap(resourceTypeToTemplate); + myDatatypeToTemplate = Multimaps.unmodifiableListMultimap(datatypeToTemplate); + myFragmentNameToTemplate = Multimaps.unmodifiableListMultimap(fragmentNameToTemplate); } public int getNamedTemplateCount() { @@ -92,35 +95,56 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { } @Override - public List getTemplateByResourceName(FhirContext theFhirContext, EnumSet theStyles, String theResourceName) { - return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate); + public List getTemplateByResourceName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theResourceName, @Nonnull Collection theProfiles) { + return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles); } @Override - public List getTemplateByName(FhirContext theFhirContext, EnumSet theStyles, String theName) { - return getFromMap(theStyles, theName, myNameToTemplate); + public List getTemplateByName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theName) { + return getFromMap(theStyles, theName, myNameToTemplate, Collections.emptyList()); } @Override - public List getTemplateByElement(FhirContext theFhirContext, EnumSet theStyles, IBase theElement) { - List retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate); + public List getTemplateByFragmentName(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull String theFragmentName) { + return getFromMap(theStyles, theFragmentName, myFragmentNameToTemplate, Collections.emptyList()); + } + + @SuppressWarnings("PatternVariableCanBeUsed") + @Override + public List getTemplateByElement(@Nonnull FhirContext theFhirContext, @Nonnull EnumSet theStyles, @Nonnull IBase theElement) { + List retVal = Collections.emptyList(); + + if (theElement instanceof IBaseResource) { + IBaseResource resource = (IBaseResource) theElement; + String resourceName = theFhirContext.getResourceDefinition(resource).getName(); + List profiles = resource + .getMeta() + .getProfile() + .stream() + .filter(Objects::nonNull) + .map(IPrimitiveType::getValueAsString) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName, profiles); + } + if (retVal.isEmpty()) { - if (theElement instanceof IBaseResource) { - String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName(); - retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName); - } else { - String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName(); - retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate); - } + retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate, Collections.emptyList()); + } + + if (retVal.isEmpty()) { + String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName(); + retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate, Collections.emptyList()); } return retVal; } - public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException { + + public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) { return forManifestFileLocation(Arrays.asList(thePropertyFilePaths)); } - public static NarrativeTemplateManifest forManifestFileLocation(Collection thePropertyFilePaths) throws IOException { + public static NarrativeTemplateManifest forManifestFileLocation(Collection thePropertyFilePaths) { ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths); List manifestFileContents = new ArrayList<>(thePropertyFilePaths.size()); @@ -132,18 +156,23 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { return forManifestFileContents(manifestFileContents); } - public static NarrativeTemplateManifest forManifestFileContents(String... theResources) throws IOException { + public static NarrativeTemplateManifest forManifestFileContents(String... theResources) { return forManifestFileContents(Arrays.asList(theResources)); } - public static NarrativeTemplateManifest forManifestFileContents(Collection theResources) throws IOException { - List templates = new ArrayList<>(); - for (String next : theResources) { - templates.addAll(loadProperties(next)); + public static NarrativeTemplateManifest forManifestFileContents(Collection theResources) { + try { + List templates = new ArrayList<>(); + for (String next : theResources) { + templates.addAll(loadProperties(next)); + } + return new NarrativeTemplateManifest(templates); + } catch (IOException e) { + throw new InternalErrorException(Msg.code(1808) + e); } - return new NarrativeTemplateManifest(templates); } + @SuppressWarnings("unchecked") private static Collection loadProperties(String theManifestText) throws IOException { Map nameToTemplate = new HashMap<>(); @@ -164,7 +193,7 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { try { nextTemplate.addAppliesToClass((Class) Class.forName(className)); } catch (ClassNotFoundException theE) { - throw new InternalErrorException(Msg.code(1867) + "Could not find class " + className + " declared in narative manifest"); + throw new InternalErrorException(Msg.code(1867) + "Could not find class " + className + " declared in narrative manifest"); } } } else if (nextKey.endsWith(".profile")) { @@ -174,18 +203,13 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { } } else if (nextKey.endsWith(".resourceType")) { String resourceType = file.getProperty(nextKey); - Arrays - .stream(resourceType.split(",")) - .map(t -> t.trim()) - .filter(t -> isNotBlank(t)) - .forEach(t -> nextTemplate.addAppliesToResourceType(t)); + parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToResourceType); + } else if (nextKey.endsWith(".fragmentName")) { + String resourceType = file.getProperty(nextKey); + parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToFragmentName); } else if (nextKey.endsWith(".dataType")) { String dataType = file.getProperty(nextKey); - Arrays - .stream(dataType.split(",")) - .map(t -> t.trim()) - .filter(t -> isNotBlank(t)) - .forEach(t -> nextTemplate.addAppliesToDatatype(t)); + parseValuesAndAddToMap(dataType, nextTemplate::addAppliesToDatatype); } else if (nextKey.endsWith(".style")) { String templateTypeName = file.getProperty(nextKey).toUpperCase(); TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName); @@ -203,8 +227,8 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey); } else { throw new ConfigurationException(Msg.code(1868) + "Invalid property name: " + nextKey - + " - the key must end in one of the expected extensions " - + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'"); + + " - the key must end in one of the expected extensions " + + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'"); } } @@ -212,44 +236,39 @@ public class NarrativeTemplateManifest implements INarrativeTemplateManifest { return nameToTemplate.values(); } - static String loadResource(String name) throws IOException { - if (name.startsWith("classpath:")) { - String cpName = name.substring("classpath:".length()); - try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) { - if (resource == null) { - try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) { - if (resource2 == null) { - throw new IOException(Msg.code(1869) + "Can not find '" + cpName + "' on classpath"); - } - return IOUtils.toString(resource2, Charsets.UTF_8); - } - } - return IOUtils.toString(resource, Charsets.UTF_8); - } - } else if (name.startsWith("file:")) { - File file = new File(name.substring("file:".length())); + private static void parseValuesAndAddToMap(String resourceType, Consumer addAppliesToResourceType) { + Arrays + .stream(resourceType.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .forEach(addAppliesToResourceType); + } + + static String loadResource(String theName) { + if (theName.startsWith("classpath:")) { + return ClasspathUtil.loadResource(theName); + } else if (theName.startsWith("file:")) { + File file = new File(theName.substring("file:".length())); if (file.exists() == false) { - throw new IOException(Msg.code(1870) + "File not found: " + file.getAbsolutePath()); + throw new InternalErrorException(Msg.code(1870) + "File not found: " + file.getAbsolutePath()); } try (FileInputStream inputStream = new FileInputStream(file)) { return IOUtils.toString(inputStream, Charsets.UTF_8); + } catch (IOException e) { + throw new InternalErrorException(Msg.code(1869) + e.getMessage(), e); } } else { - throw new IOException(Msg.code(1871) + "Invalid resource name: '" + name + "' (must start with classpath: or file: )"); + throw new InternalErrorException(Msg.code(1871) + "Invalid resource name: '" + theName + "' (must start with classpath: or file: )"); } } - private static List getFromMap(EnumSet theStyles, T theKey, Map> theMap) { + private static List getFromMap(EnumSet theStyles, T theKey, ListMultimap theMap, Collection theProfiles) { return theMap - .getOrDefault(theKey, Collections.emptyList()) - .stream() - .filter(t -> theStyles.contains(t.getTemplateType())) - .collect(Collectors.toList()); - } - - private static Map> makeImmutable(Map> theStyleToResourceTypeToTemplate) { - theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value)); - return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate); + .get(theKey) + .stream() + .filter(t -> theStyles.contains(t.getTemplateType())) + .filter(t -> theProfiles.isEmpty() || t.getAppliesToProfiles().stream().anyMatch(theProfiles::contains)) + .collect(Collectors.toList()); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java index 5b37cf3b60a..3ff366724ce 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java @@ -29,4 +29,9 @@ public class NullNarrativeGenerator implements INarrativeGenerator { public boolean populateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { return false; } + + @Override + public String generateResourceNarrative(FhirContext theFhirContext, IBaseResource theResource) { + return null; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java deleted file mode 100644 index 7fc96d179cf..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java +++ /dev/null @@ -1,209 +0,0 @@ -package ca.uhn.fhir.narrative2; - -/*- - * #%L - * HAPI FHIR - Core Library - * %% - * Copyright (C) 2014 - 2023 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% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hl7.fhir.instance.model.api.IBase; -import org.thymeleaf.IEngineConfiguration; -import org.thymeleaf.TemplateEngine; -import org.thymeleaf.cache.AlwaysValidCacheEntryValidity; -import org.thymeleaf.cache.ICacheEntryValidity; -import org.thymeleaf.context.Context; -import org.thymeleaf.context.ITemplateContext; -import org.thymeleaf.engine.AttributeName; -import org.thymeleaf.messageresolver.IMessageResolver; -import org.thymeleaf.model.IProcessableElementTag; -import org.thymeleaf.processor.IProcessor; -import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; -import org.thymeleaf.processor.element.AbstractElementTagProcessor; -import org.thymeleaf.processor.element.IElementTagStructureHandler; -import org.thymeleaf.standard.StandardDialect; -import org.thymeleaf.standard.expression.IStandardExpression; -import org.thymeleaf.standard.expression.IStandardExpressionParser; -import org.thymeleaf.standard.expression.StandardExpressions; -import org.thymeleaf.templatemode.TemplateMode; -import org.thymeleaf.templateresolver.DefaultTemplateResolver; -import org.thymeleaf.templateresource.ITemplateResource; -import org.thymeleaf.templateresource.StringTemplateResource; - -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator { - - private IMessageResolver myMessageResolver; - - /** - * Constructor - */ - public ThymeleafNarrativeGenerator() { - super(); - } - - private TemplateEngine getTemplateEngine(FhirContext theFhirContext) { - TemplateEngine engine = new TemplateEngine(); - ProfileResourceResolver resolver = new ProfileResourceResolver(theFhirContext); - engine.setTemplateResolver(resolver); - if (myMessageResolver != null) { - engine.setMessageResolver(myMessageResolver); - } - StandardDialect dialect = new StandardDialect() { - @Override - public Set getProcessors(String theDialectPrefix) { - Set retVal = super.getProcessors(theDialectPrefix); - retVal.add(new NarrativeTagProcessor(theFhirContext, theDialectPrefix)); - retVal.add(new NarrativeAttributeProcessor(theDialectPrefix, theFhirContext)); - return retVal; - } - - }; - - engine.setDialect(dialect); - return engine; - } - - @Override - protected String applyTemplate(FhirContext theFhirContext, INarrativeTemplate theTemplate, IBase theTargetContext) { - - Context context = new Context(); - context.setVariable("resource", theTargetContext); - context.setVariable("context", theTargetContext); - context.setVariable("fhirVersion", theFhirContext.getVersion().getVersion().name()); - - return getTemplateEngine(theFhirContext).process(theTemplate.getTemplateName(), context); - } - - - @Override - protected EnumSet getStyle() { - return EnumSet.of(TemplateTypeEnum.THYMELEAF); - } - - private String applyTemplateWithinTag(FhirContext theFhirContext, ITemplateContext theTemplateContext, String theName, String theElement) { - IEngineConfiguration configuration = theTemplateContext.getConfiguration(); - IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration); - final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement); - Object elementValueObj = expression.execute(theTemplateContext); - final IBase elementValue = (IBase) elementValueObj; - if (elementValue == null) { - return ""; - } - - List templateOpt; - if (isNotBlank(theName)) { - templateOpt = getManifest().getTemplateByName(theFhirContext, getStyle(), theName); - if (templateOpt.isEmpty()) { - throw new InternalErrorException(Msg.code(1863) + "Unknown template name: " + theName); - } - } else { - templateOpt = getManifest().getTemplateByElement(theFhirContext, getStyle(), elementValue); - if (templateOpt.isEmpty()) { - throw new InternalErrorException(Msg.code(1864) + "No template for type: " + elementValue.getClass()); - } - } - - return applyTemplate(theFhirContext, templateOpt.get(0), elementValue); - } - - public void setMessageResolver(IMessageResolver theMessageResolver) { - myMessageResolver = theMessageResolver; - } - - - private class ProfileResourceResolver extends DefaultTemplateResolver { - private final FhirContext myFhirContext; - - private ProfileResourceResolver(FhirContext theFhirContext) { - myFhirContext = theFhirContext; - } - - @Override - protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { - return getManifest().getTemplateByName(myFhirContext, getStyle(), theTemplate).size() > 0; - } - - @Override - protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { - return TemplateMode.XML; - } - - @Override - protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { - return getManifest() - .getTemplateByName(myFhirContext, getStyle(), theTemplate) - .stream() - .findFirst() - .map(t -> new StringTemplateResource(t.getTemplateText())) - .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate)); - } - - @Override - protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) { - return AlwaysValidCacheEntryValidity.INSTANCE; - } - } - - private class NarrativeTagProcessor extends AbstractElementTagProcessor { - - private final FhirContext myFhirContext; - - NarrativeTagProcessor(FhirContext theFhirContext, String dialectPrefix) { - super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0); - myFhirContext = theFhirContext; - } - - @Override - protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) { - String name = theTag.getAttributeValue("th:name"); - String element = theTag.getAttributeValue("th:element"); - - String appliedTemplate = applyTemplateWithinTag(myFhirContext, theTemplateContext, name, element); - theStructureHandler.replaceWith(appliedTemplate, false); - } - } - - /** - * This is a thymeleaf extension that allows people to do things like - * - */ - private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor { - - private final FhirContext myFhirContext; - - NarrativeAttributeProcessor(String theDialectPrefix, FhirContext theFhirContext) { - super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true); - myFhirContext = theFhirContext; - } - - @Override - protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) { - String text = applyTemplateWithinTag(myFhirContext, theContext, null, theAttributeValue); - theStructureHandler.setBody(text, false); - } - - } -} 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 3521803c23e..4fbf6261fb3 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 @@ -35,6 +35,8 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Date; import java.util.Objects; /** @@ -144,8 +146,9 @@ public class BundleBuilder { /** * Adds a FHIRPatch patch bundle to the transaction + * * @param theTarget The target resource ID to patch - * @param thePatch The FHIRPath Parameters resource + * @param thePatch The FHIRPath Parameters resource * @since 6.3.0 */ public PatchBuilder addTransactionFhirPatchEntry(IIdType theTarget, IBaseParameters thePatch) { @@ -162,10 +165,10 @@ public class BundleBuilder { * Adds a FHIRPatch patch bundle to the transaction. This method is intended for conditional PATCH operations. If you * know the ID of the resource you wish to patch, use {@link #addTransactionFhirPatchEntry(IIdType, IBaseParameters)} * instead. - * + * * @param thePatch The FHIRPath Parameters resource - * @since 6.3.0 * @see #addTransactionFhirPatchEntry(IIdType, IBaseParameters) + * @since 6.3.0 */ public PatchBuilder addTransactionFhirPatchEntry(IBaseParameters thePatch) { IPrimitiveType url = addAndPopulateTransactionBundleEntryRequest(thePatch, null, null, "PATCH"); @@ -324,6 +327,14 @@ public class BundleBuilder { addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); } + /** + * Adds an entry for a Document bundle type + */ + public void addDocumentEntry(IBaseResource theResource) { + setType("document"); + addEntryAndReturnRequest(theResource, theResource.getIdElement().getValue()); + } + /** * Creates new entry and adds it to the bundle * @@ -460,6 +471,30 @@ public class BundleBuilder { setBundleField("type", theType); } + /** + * Adds an identifier to Bundle.identifier + * + * @param theSystem The system + * @param theValue The value + * @since 6.4.0 + */ + public void setIdentifier(@Nullable String theSystem, @Nullable String theValue) { + FhirTerser terser = myContext.newTerser(); + IBase identifier = terser.addElement(myBundle, "identifier"); + terser.setElement(identifier, "system", theSystem); + terser.setElement(identifier, "value", theValue); + } + + /** + * Sets the timestamp in Bundle.timestamp + * + * @since 6.4.0 + */ + public void setTimestamp(@Nonnull IPrimitiveType theTimestamp) { + FhirTerser terser = myContext.newTerser(); + terser.setElement(myBundle, "Bundle.timestamp", theTimestamp.getValueAsString()); + } + public class DeleteBuilder extends BaseOperationBuilder { @@ -486,7 +521,7 @@ public class BundleBuilder { public class CreateBuilder extends BaseOperationBuilder { private final IBase myRequest; - CreateBuilder(IBase theRequest) { + CreateBuilder(IBase theRequest) { myRequest = theRequest; } @@ -509,7 +544,7 @@ public class BundleBuilder { /** * Returns a reference to the BundleBuilder instance. - * + *

* Calling this method has no effect at all, it is only * provided for easy method chaning if you want to build * your bundle as a single fluent call. @@ -527,7 +562,7 @@ public class BundleBuilder { private final IPrimitiveType myUrl; - BaseOperationBuilderWithConditionalUrl(IPrimitiveType theUrl) { + BaseOperationBuilderWithConditionalUrl(IPrimitiveType theUrl) { myUrl = theUrl; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java index 12d7ef2043e..1df46e78737 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ClasspathUtil.java @@ -118,9 +118,24 @@ public class ClasspathUtil { return loadResource(theClasspath, streamTransform); } + /** + * Load a classpath resource, throw an {@link InternalErrorException} if not found + * + * @since 6.4.0 + */ + @Nonnull + public static T loadCompressedResource(FhirContext theCtx, Class theType, String theClasspath) { + String resource = loadCompressedResource(theClasspath); + return parseResource(theCtx, theType, resource); + } + @Nonnull public static T loadResource(FhirContext theCtx, Class theType, String theClasspath) { String raw = loadResource(theClasspath); + return parseResource(theCtx, theType, raw); + } + + private static T parseResource(FhirContext theCtx, Class theType, String raw) { return EncodingEnum.detectEncodingNoDefault(raw).newParser(theCtx).parseResource(theType, raw); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java new file mode 100644 index 00000000000..49589864e55 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/CompositionBuilder.java @@ -0,0 +1,174 @@ +package ca.uhn.fhir.util; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import javax.annotation.Nonnull; +import java.util.Date; + +/** + * This class can be used to generate Composition resources in + * a version independent way. + * + * @since 6.4.0 + */ +public class CompositionBuilder { + + private final FhirContext myCtx; + private final IBaseResource myComposition; + private final RuntimeResourceDefinition myCompositionDef; + private final FhirTerser myTerser; + + public CompositionBuilder(@Nonnull FhirContext theFhirContext) { + myCtx = theFhirContext; + myCompositionDef = myCtx.getResourceDefinition("Composition"); + myTerser = myCtx.newTerser(); + myComposition = myCompositionDef.newInstance(); + } + + @SuppressWarnings("unchecked") + public T getComposition() { + return (T) myComposition; + } + + /** + * Add a value to Composition.author + */ + public void addAuthor(IIdType theAuthorId) { + IBaseReference reference = myTerser.addElement(myComposition, "Composition.author"); + reference.setReference(theAuthorId.getValue()); + } + + /** + * Set a value in Composition.status + */ + public void setStatus(String theStatusCode) { + myTerser.setElement(myComposition, "Composition.status", theStatusCode); + } + + + /** + * Set a value in Composition.subject + */ + public void setSubject(IIdType theSubject) { + myTerser.setElement(myComposition, "Composition.subject.reference", theSubject.getValue()); + } + + /** + * Add a Coding to Composition.type.coding + */ + public void addTypeCoding(String theSystem, String theCode, String theDisplay) { + IBaseCoding coding = myTerser.addElement(myComposition, "Composition.type.coding"); + coding.setCode(theCode); + coding.setSystem(theSystem); + coding.setDisplay(theDisplay); + } + + /** + * Set a value in Composition.date + */ + public void setDate(IPrimitiveType theDate) { + myTerser.setElement(myComposition, "Composition.date", theDate.getValueAsString()); + } + + /** + * Set a value in Composition.title + */ + public void setTitle(String theTitle) { + myTerser.setElement(myComposition, "Composition.title", theTitle); + } + + /** + * Set a value in Composition.confidentiality + */ + public void setConfidentiality(String theConfidentiality) { + myTerser.setElement(myComposition, "Composition.confidentiality", theConfidentiality); + } + + /** + * Set a value in Composition.id + */ + public void setId(IIdType theId) { + myComposition.setId(theId.getValue()); + } + + public SectionBuilder addSection() { + IBase section = myTerser.addElement(myComposition, "Composition.section"); + return new SectionBuilder(section); + } + + public class SectionBuilder { + + private final IBase mySection; + + /** + * Constructor + */ + private SectionBuilder(IBase theSection) { + mySection = theSection; + } + + /** + * Sets the section title + */ + public void setTitle(String theTitle) { + myTerser.setElement(mySection, "title", theTitle); + } + + /** + * Add a coding to section.code + */ + public void addCodeCoding(String theSystem, String theCode, String theDisplay) { + IBaseCoding coding = myTerser.addElement(mySection, "code.coding"); + coding.setCode(theCode); + coding.setSystem(theSystem); + coding.setDisplay(theDisplay); + } + + /** + * Adds a reference to entry.reference + */ + public void addEntry(IIdType theReference) { + IBaseReference entry = myTerser.addElement(mySection, "entry"); + entry.setReference(theReference.getValue()); + } + + /** + * Adds narrative text to the section + */ + public void setText(String theStatus, String theDivHtml) { + IBase text = myTerser.addElement(mySection, "text"); + myTerser.setElement(text, "status", theStatus); + myTerser.setElement(text, "div", theDivHtml); + } + } + + +} + diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log/Logs.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/Logs.java similarity index 74% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log/Logs.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/util/Logs.java index 6432ae69de7..4da6fa01edb 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/batch/log/Logs.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/Logs.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.jpa.batch.log; +package ca.uhn.fhir.util; /*- * #%L - * HAPI FHIR Storage api + * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2023 Smile CDR, Inc. * %% @@ -25,8 +25,13 @@ import org.slf4j.LoggerFactory; public class Logs { private static final Logger ourBatchTroubleshootingLog = LoggerFactory.getLogger("ca.uhn.fhir.log.batch_troubleshooting"); + private static final Logger ourNarrativeGenerationTroubleshootingLog = LoggerFactory.getLogger("ca.uhn.fhir.log.narrative_generation_troubleshooting"); public static Logger getBatchTroubleshootingLog() { return ourBatchTroubleshootingLog; } + + public static Logger getNarrativeGenerationTroubleshootingLog() { + return ourBatchTroubleshootingLog; + } } diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 8a8554894a7..6ee77e7c3af 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,14 +4,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -77,6 +77,11 @@ hapi-fhir-jpaserver-elastic-test-utilities ${project.version} + + ${project.groupId} + hapi-fhir-jpaserver-ips + ${project.version} + ${project.groupId} hapi-fhir-jpaserver-mdm diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index b08a7edf751..b85d38797be 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 31c4fa52bb1..945799435ba 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index dd76d6eef33..15fd8f03efe 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 1f15b94df7e..af41b578c7e 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 0b11c25bc7f..520224f7cd4 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 097affebf5d..8befbdd4228 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index 55b19a9789f..a80ba2c725d 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 3d448d1c47d..988f1031258 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index fea8a625d05..3a31155caa9 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 0d5907f97b4..90acbc2a826 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -60,6 +60,11 @@ hapi-fhir-server-openapi ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-test-utilities + ${project.version} + com.fasterxml.jackson.dataformat diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml new file mode 100644 index 00000000000..872de97dbd8 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-composition-builder.yaml @@ -0,0 +1,6 @@ +--- +type: add +issue: 4438 +title: "A new utility called `CompositionBuilder` has been added. This class can be used to + generate Composition resources in a version-independent way, similar to the function + of `BundleUtil` for Bundle resources." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml new file mode 100644 index 00000000000..ce4c10f7d67 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-fhirpath-evaluation-conetxt.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 4438 +title: "The FhirPath evaluator framework now allows for an optional evaluation context object + which can be used to supply external data such as responses to the `resolve()` function." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml new file mode 100644 index 00000000000..f4d6109f3d9 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-ips-generator.yaml @@ -0,0 +1,9 @@ +--- +type: add +issue: 4438 +title: "A new experimental API has been added for automated generation of + International Patient Summary (IPS) documents in the JPA server. This module + is based on the excellent work of Rio Bennin and Panayiotis Savva of the + University of Cyprus and was completed at FHIR Connectathon 34 in + Henderson Nevada. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml new file mode 100644 index 00000000000..e1eed4fbe66 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4438-add-thymeleaf-fragment-support.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 4438 +title: "The Thymeleaf narrative generator can now declare template fragments in separate files + so that they can be reused across multiple templates." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 5ff1884ce0d..0f0e29c3c5a 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -65,6 +65,7 @@ page.server_jpa.diff=Diff Operation page.server_jpa.lastn=LastN Operation page.server_jpa.elastic=Lucene/Elasticsearch Indexing page.server_jpa.terminology=Terminology +page.server_jpa.ips=International Patient Summary (IPS) section.server_jpa_mdm.title=JPA Server: MDM page.server_jpa_mdm.mdm=MDM Getting Started diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md index 36fa258c477..68fccf51f7e 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/model/narrative_generation.md @@ -97,3 +97,38 @@ Finally, use the [CustomThymeleafNarrativeGenerator](/hapi-fhir/apidocs/hapi-fhi {{snippet:classpath:/ca/uhn/hapi/fhir/docs/NarrativeGenerator.java|gen}} ``` +# Fragments Expressions in Thyemleaf Templates + +Thymeleaf has a concept called Fragments, which allow reusable template portions that can be imported anywhere you need them. It can be helpful to put these fragment definitions in their own file. For example, the following property file declares a template and a fragment: + +```properties +{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment.properties}} +``` + +The following template declares a fragment (this is `narrative-with-fragment-child.html` in the example above): + +```html +{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment-child.html}} +``` + +And the following template uses it (this is `narrative-with-fragment-child.html` in the example above): + +```html +{{snippet:classpath:ca/uhn/fhir/narrative/narrative-with-fragment-parent.html}} +``` + + +# FHIRPath Expressions in Thyemleaf Templates + +Thymeleaf templates can incorporate FHIRPath expressions using the `#fhirpath` expression object. + +This object has the following methods: + +* evaluateFirst(input, pathExpression) – This method returns the first element matched on `input` by the path expression, or _null_ if nothing matches. +* evaluate(input, pathExpression) – This method returns a Java List of elements matched on `input` by the path expression, or an empty list if nothing matches. + +For example: + +```html +{{snippet:classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html}} +``` diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md index fd1a24eb0bd..9c8211067d5 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/elastic.md @@ -104,3 +104,14 @@ Note: This does not support the $meta-add or $meta-delete operations. Full reind when this option is enabled after resources have been indexed. This **experimental** feature is enabled via the `setStoreResourceInHSearchIndex()` option of DaoConfig. + +# Synchronous Writes + +ElasticSearch writes are asynchronous by default. This means that when writing to an ElasticSearch instance (independent of HAPI FHIR), the data you write will not be available to subsequent reads for a short period of time while the indexes synchronize. + +ElasticSearch states that this behaviour leads to better overall performance and throughput on the system. + +This can cause issues, particularly in unit tests where data is being examined shortly after it is written. + +You can force synchronous writing to them in HAPI FHIR JPA by setting the Hibernate Search [synchronization strategy](https://docs.jboss.org/hibernate/stable/search/reference/en-US/html_single/#mapper-orm-indexing-automatic-synchronization). This setting is internally setting the ElasticSearch [refresh=wait_for](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-refresh.html) option. Be warned that this will have a negative impact on overall performance. THE HAPI FHIR TEAM has not tried to quantify this impact but the ElasticSearch docs seem to make a fairly big deal about it. + 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 new file mode 100644 index 00000000000..67cfe65c68b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips.md @@ -0,0 +1,63 @@ +# International Patient Summary (IPS) Generator + +The International Patient Summary (IPS) is an international collaborative effort to develop a specification for a health record summary extract. It is specified in the standards EN 17269 and ISO 27269, and supported in FHIR through the [International Patient Summary Implementation Guide](http://hl7.org/fhir/uv/ips/). + +In FHIR, an IPS is expressed as a [FHIR Document](https://www.hl7.org/fhir/documents.html). The HAPI FHIR JPA server supports the automated generation of IPS documents through an extensible and customizable engine which implements the [`$summary`](http://hl7.org/fhir/uv/ips/OperationDefinition-summary.html) operation. + +# Overview + +IPS Overview + +The IPS Generator uses FHIR resources stored in your repository as its input. The algorithm for determining which resources to include and how to construct the mandatory narrative is customizable and extensible, with a default algorithm included. + + + +# 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. + +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. + +* 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) + + + + +# 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) + + + + +# Narrative Templates + +The IPS Document includes a [Composition](http://hl7.org/fhir/composition.html) resource, and this composition must include a populated narrative for each section containing the relevant clinical details for the section. + +The IPS generator uses HAPI FHIR [Narrative Generation](/hapi-fhir/docs/model/narrative_generation.html) to achieve this. + +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: + +```properties +ips-allergyintolerance.resourceType=Bundle +ips-allergyintolerance.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips +ips-allergyintolerance.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html +``` + +Built-in Narrative Templates: +* Source Code: [ca.uhn.fhir.jpa.ips.narrative](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/). Note the following: + * Default properties file: [ips-narratives.properties](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties) + * Example template for Allergies section: [allergyintolerance.html](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html) + * Fragments file containing common fragments used in multiple templates: [utility-fragments.html](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html) + +# Credits + +This module is based on the excellent work of Rio Bennin and Panayiotis Savva of the University of Cyprus. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg new file mode 100644 index 00000000000..2a3acf91c94 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/ips/overview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index c4bb66238c1..9fd26de5ce9 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -181,6 +181,11 @@ hapi-fhir-jpaserver-model ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-ips + ${project.version} + ca.uhn.hapi.fhir hapi-fhir-jpaserver-mdm diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 9e6f22375db..d5ded8302c0 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index a2137f38b88..aace7e6de97 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 9f32dc19aef..58f104cd3ed 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -376,6 +376,12 @@ ${project.version} test + + ca.uhn.hapi.fhir + hapi-fhir-test-utilities + ${project.version} + test + org.jetbrains diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java index c53b37d045f..f6ecea647be 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImpl.java @@ -29,7 +29,7 @@ import ca.uhn.fhir.batch2.model.MarkWorkChunkAsErrorRequest; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.batch2.models.JobInstanceFetchRequest; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.jpa.dao.data.IBatch2JobInstanceRepository; import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository; import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity; @@ -39,7 +39,6 @@ import ca.uhn.fhir.model.api.PagingIterator; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java index 0bebb4f459f..8c4467b295f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java @@ -24,7 +24,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.importpull.models.Batch2BulkImportPullJobParameters; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; import ca.uhn.fhir.jpa.bulk.imprt.model.ActivateJobResult; import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobFileJson; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java index c36928583f1..52ac51e2cad 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/PersistedJpaBundleProvider.java @@ -105,7 +105,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider { private MemoryCacheService myMemoryCacheService; @Autowired private IJpaStorageResourceParser myJpaStorageResourceParser; - /* * Non autowired fields (will be different for every instance * of this class, since it's a prototype @@ -114,7 +113,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider { private String myUuid; private SearchCacheStatusEnum myCacheStatus; private RequestPartitionId myRequestPartitionId; - /** * Constructor */ @@ -223,7 +221,6 @@ public class PersistedJpaBundleProvider implements IBundleProvider { return myTxService.withRequest(myRequest).execute(() -> toResourceList(sb, pidsSubList)); } - /** * Returns false if the entity can't be found */ diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 7b769a6a173..2d2952de95e 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml new file mode 100644 index 00000000000..3283bee3887 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + ca.uhn.hapi.fhir + hapi-deployable-pom + 6.3.13-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + hapi-fhir-jpaserver-ips + jar + HAPI FHIR JPA Server - International Patient Summary (IPS) + + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-base + ${project.version} + + + + + javax.servlet + javax.servlet-api + provided + + + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-test-utilities + ${project.version} + test + + + ca.uhn.hapi.fhir + hapi-fhir-test-utilities + ${project.version} + test + + + net.sourceforge.htmlunit + htmlunit + test + + + + 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 new file mode 100644 index 00000000000..196aceb3ed1 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IIpsGenerationStrategy.java @@ -0,0 +1,136 @@ +package ca.uhn.fhir.jpa.ips.api; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.Include; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Set; + +/** + * This interface is the primary configuration and strategy provider for the + * HAPI FHIR International Patient Summary (IPS) generator. + *

+ * Note that this API will almost certainly change as more real-world experience is + * gained with the IPS generator. + */ +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. + */ + SectionRegistry getSectionRegistry(); + + /** + * Provides a list of configuration property files for the IPS narrative generator. + *

+ * Entries should be of the format classpath:path/to/file.properties + *

+ *

+ * 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} + * in order to fall back to the default templates for any sections you have not + * provided an explicit template for. + *

+ */ + List getNarrativePropertyFiles(); + + /** + * Create and return a new Organization resource representing. + * the author of the IPS document. This method will be called once per IPS + * in order to + */ + IBaseResource createAuthor(); + + /** + * Create and return a title for the composition document. + * + * @param theContext The associated context for the specific IPS document being generated. + */ + String createTitle(IpsContext theContext); + + /** + * Create and return a confidentiality code for the composition document. Must be a valid + * code for the element Composition.confidentiality + * + * @param theIpsContext The associated context for the specific IPS document being generated. + */ + String createConfidentiality(IpsContext theIpsContext); + + /** + * 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. + * + * @param theIpsContext The associated context for the specific IPS document being + * generated. Note that this will be null when + * massaging the ID of the subject (Patient) resource, but will + * 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 + */ + 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. + * + * @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. + */ + @Nonnull + Set provideResourceSearchIncludes(IpsContext.IpsSectionContext theIpsSectionContext); + + /** + * 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. + */ + boolean shouldInclude(IpsContext.IpsSectionContext theIpsSectionContext, IBaseResource theCandidate); + +} 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 new file mode 100644 index 00000000000..118a9722726 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsContext.java @@ -0,0 +1,86 @@ +package ca.uhn.fhir.jpa.ips.api; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +public class IpsContext { + + private final IBaseResource mySubject; + private final IIdType mySubjectId; + + /** + * Constructor + * + * @param theSubject The subject Patient resource for the IPS being generated + * @param theSubjectId The original ID for {@literal theSubject}, which may not match the current ID if {@link IIpsGenerationStrategy#massageResourceId(IpsContext, IBaseResource)} has modified it + */ + public IpsContext(IBaseResource theSubject, IIdType theSubjectId) { + mySubject = theSubject; + mySubjectId = theSubjectId; + } + + /** + * Returns the subject Patient resource for the IPS being generated. Note that + * the {@literal Resource.id} value may not match the ID of the resource stored in the + * repository if {@link IIpsGenerationStrategy#massageResourceId(IpsContext, IBaseResource)} has + * returned a different ID. Use {@link #getSubjectId()} if you want the originally stored ID. + * + * @see #getSubjectId() for the originally stored ID. + */ + public IBaseResource getSubject() { + return mySubject; + } + + /** + * Returns the ID of the subject for the given IPS. This value should match the + * ID which was originally fetched from the repository. + */ + public IIdType getSubjectId() { + 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; + } + } + +} 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/api/IpsSectionEnum.java new file mode 100644 index 00000000000..31b0fc02c9e --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/IpsSectionEnum.java @@ -0,0 +1,38 @@ +package ca.uhn.fhir.jpa.ips.api; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +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/api/SectionRegistry.java b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java new file mode 100644 index 00000000000..1b4fc7f3544 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/api/SectionRegistry.java @@ -0,0 +1,422 @@ +package ca.uhn.fhir.jpa.ips.api; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +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 javax.annotation.Nullable; +import javax.annotation.PostConstruct; +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") + .withResourceTypes(ResourceType.AllergyIntolerance.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips") + .withNoInfoGenerator(new AllergyIntoleranceNoInfoR4Generator()) + .build(); + } + + protected void addSectionMedicationSummary() { + addSection(IpsSectionEnum.MEDICATION_SUMMARY) + .withTitle("Medication List") + .withSectionCode("10160-0") + .withSectionDisplay("Medication List") + .withResourceTypes( + ResourceType.MedicationStatement.name(), + ResourceType.MedicationRequest.name(), + ResourceType.MedicationAdministration.name(), + ResourceType.MedicationDispense.name() + ) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/MedicationSummary-uv-ips") + .withNoInfoGenerator(new MedicationNoInfoR4Generator()) + .build(); + } + + protected void addSectionProblemList() { + addSection(IpsSectionEnum.PROBLEM_LIST) + .withTitle("Problem List") + .withSectionCode("11450-4") + .withSectionDisplay("Problem List") + .withResourceTypes(ResourceType.Condition.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/ProblemList-uv-ips") + .withNoInfoGenerator(new ProblemNoInfoR4Generator()) + .build(); + } + + protected void addSectionImmunizations() { + addSection(IpsSectionEnum.IMMUNIZATIONS) + .withTitle("History of Immunizations") + .withSectionCode("11369-6") + .withSectionDisplay("History of Immunizations") + .withResourceTypes(ResourceType.Immunization.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Immunizations-uv-ips") + .build(); + } + + protected void addSectionProcedures() { + addSection(IpsSectionEnum.PROCEDURES) + .withTitle("History of Procedures") + .withSectionCode("47519-4") + .withSectionDisplay("History of Procedures") + .withResourceTypes(ResourceType.Procedure.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/HistoryOfProcedures-uv-ips") + .build(); + } + + protected void addSectionMedicalDevices() { + addSection(IpsSectionEnum.MEDICAL_DEVICES) + .withTitle("Medical Devices") + .withSectionCode("46240-8") + .withSectionDisplay("Medical Devices") + .withResourceTypes(ResourceType.DeviceUseStatement.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/MedicalDevices-uv-ips") + .build(); + } + + protected void addSectionDiagnosticResults() { + addSection(IpsSectionEnum.DIAGNOSTIC_RESULTS) + .withTitle("Diagnostic Results") + .withSectionCode("30954-2") + .withSectionDisplay("Diagnostic Results") + .withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/DiagnosticResults-uv-ips") + .build(); + } + + protected void addSectionVitalSigns() { + addSection(IpsSectionEnum.VITAL_SIGNS) + .withTitle("Vital Signs") + .withSectionCode("8716-3") + .withSectionDisplay("Vital Signs") + .withResourceTypes(ResourceType.Observation.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/VitalSigns-uv-ips") + .build(); + } + + protected void addSectionPregnancy() { + addSection(IpsSectionEnum.PREGNANCY) + .withTitle("Pregnancy Information") + .withSectionCode("10162-6") + .withSectionDisplay("Pregnancy Information") + .withResourceTypes(ResourceType.Observation.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/Pregnancy-uv-ips") + .build(); + } + + protected void addSectionSocialHistory() { + addSection(IpsSectionEnum.SOCIAL_HISTORY) + .withTitle("Social History") + .withSectionCode("29762-2") + .withSectionDisplay("Social History") + .withResourceTypes(ResourceType.Observation.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/SocialHistory-uv-ips") + .build(); + } + + protected void addSectionIllnessHistory() { + addSection(IpsSectionEnum.ILLNESS_HISTORY) + .withTitle("History of Past Illness") + .withSectionCode("11348-0") + .withSectionDisplay("History of Past Illness") + .withResourceTypes(ResourceType.Condition.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/PastHistoryOfIllnesses-uv-ips") + .build(); + } + + protected void addSectionFunctionalStatus() { + addSection(IpsSectionEnum.FUNCTIONAL_STATUS) + .withTitle("Functional Status") + .withSectionCode("47420-5") + .withSectionDisplay("Functional Status") + .withResourceTypes(ResourceType.ClinicalImpression.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/FunctionalStatus-uv-ips") + .build(); + } + + protected void addSectionPlanOfCare() { + addSection(IpsSectionEnum.PLAN_OF_CARE) + .withTitle("Plan of Care") + .withSectionCode("18776-5") + .withSectionDisplay("Plan of Care") + .withResourceTypes(ResourceType.CarePlan.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/PlanOfCare-uv-ips") + .build(); + } + + protected void addSectionAdvanceDirectives() { + addSection(IpsSectionEnum.ADVANCE_DIRECTIVES) + .withTitle("Advance Directives") + .withSectionCode("42349-0") + .withSectionDisplay("Advance Directives") + .withResourceTypes(ResourceType.Consent.name()) + .withProfile("http://hl7.org/fhir/uv/ips/StructureDefinition/AdvanceDirectives-uv-ips") + .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 new file mode 100644 index 00000000000..42a6ab9a395 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IIpsGeneratorSvc.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.jpa.ips.generator; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenParam; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IIdType; + +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); + + /** + * Generates an IPS document and returns the complete document bundle + * for the given patient by identifier + */ + IBaseBundle generateIps(RequestDetails theRequestDetails, TokenParam thePatientIdentifier); +} 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 new file mode 100644 index 00000000000..a3afa800395 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImpl.java @@ -0,0 +1,562 @@ +package ca.uhn.fhir.jpa.ips.generator; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +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.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.valueset.BundleEntrySearchModeEnum; +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 org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseExtension; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +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 static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI; +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; + private final FhirContext myFhirContext; + + /** + * Constructor + */ + public IpsGeneratorSvcImpl(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { + myGenerationStrategy = theGenerationStrategy; + myDaoRegistry = theDaoRegistry; + myFhirContext = theFhirContext; + } + + @Override + public IBaseBundle generateIps(RequestDetails theRequestDetails, IIdType thePatientId) { + IBaseResource patient = myDaoRegistry + .getResourceDao("Patient") + .read(thePatientId, theRequestDetails); + + return generateIpsForPatient(theRequestDetails, patient); + } + + @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); + } + + private IBaseBundle generateIpsForPatient(RequestDetails theRequestDetails, IBaseResource thePatient) { + IIdType originalSubjectId = myFhirContext.getVersion().newIdType().setValue(thePatient.getIdElement().getValue()); + massageResourceId(null, thePatient); + IpsContext context = new IpsContext(thePatient, originalSubjectId); + + IBaseResource author = myGenerationStrategy.createAuthor(); + massageResourceId(context, author); + + CompositionBuilder compositionBuilder = createComposition(thePatient, context, author); + + ResourceInclusionCollection globalResourcesToInclude = determineInclusions(theRequestDetails, originalSubjectId, context, compositionBuilder); + + IBaseResource composition = compositionBuilder.getComposition(); + + // Create the narrative for the Composition itself + CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(globalResourcesToInclude); + generator.populateResourceNarrative(myFhirContext, composition); + + return createCompositionDocument(thePatient, author, composition, globalResourcesToInclude); + } + + private IBaseBundle createCompositionDocument(IBaseResource thePatient, 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()); + bundleBuilder.setTimestamp(InstantType.now()); + + // Add composition to document + bundleBuilder.addDocumentEntry(composition); + + // Add subject to document + bundleBuilder.addDocumentEntry(thePatient); + + // Add inclusion candidates + for (IBaseResource next : theResourcesToInclude.getResources()) { + bundleBuilder.addDocumentEntry(next); + } + + // Add author to document + bundleBuilder.addDocumentEntry(author); + + return bundleBuilder.getBundle(); + } + + @Nonnull + private ResourceInclusionCollection determineInclusions(RequestDetails theRequestDetails, IIdType originalSubjectId, IpsContext context, CompositionBuilder theCompositionBuilder) { + ResourceInclusionCollection globalResourcesToInclude = new ResourceInclusionCollection(); + SectionRegistry sectionRegistry = myGenerationStrategy.getSectionRegistry(); + for (SectionRegistry.Section nextSection : sectionRegistry.getSections()) { + determineInclusionsForSection(theRequestDetails, originalSubjectId, context, theCompositionBuilder, globalResourcesToInclude, nextSection); + } + return globalResourcesToInclude; + } + + private void determineInclusionsForSection(RequestDetails theRequestDetails, IIdType theOriginalSubjectId, IpsContext theIpsContext, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theGlobalResourcesToInclude, SectionRegistry.Section theSection) { + ResourceInclusionCollection sectionResourcesToInclude = new ResourceInclusionCollection(); + for (String nextResourceType : theSection.getResourceTypes()) { + + SearchParameterMap searchParameterMap = new SearchParameterMap(); + String subjectSp = determinePatientCompartmentSearchParameterName(nextResourceType); + searchParameterMap.add(subjectSp, new ReferenceParam(theOriginalSubjectId)); + + 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 include; + + if (ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextCandidate) == BundleEntrySearchModeEnum.INCLUDE) { + include = true; + } else { + include = myGenerationStrategy.shouldInclude(ipsSectionContext, nextCandidate); + } + + if (include) { + + 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 { + IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, nextCandidate); + nextCandidate.setId(id); + theGlobalResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); + sectionResourcesToInclude.addResourceIfNotAlreadyPresent(nextCandidate, originalResourceId); + } + } + + } + + } + + /* + * Update any references within the added candidates - This is important + * because we might be replacing resource IDs before including them in + * the summary, so we need to also update the references to those + * resources. + */ + for (IBaseResource nextResource : sectionResourcesToInclude.getResources()) { + List references = myFhirContext.newTerser().getAllResourceReferences(nextResource); + for (ResourceReferenceInfo nextReference : references) { + String existingReference = nextReference.getResourceReference().getReferenceElement().getValue(); + if (isNotBlank(existingReference)) { + existingReference = new IdType(existingReference).toUnqualifiedVersionless().getValue(); + String replacement = theGlobalResourcesToInclude.getIdSubstitution(existingReference); + if (isNotBlank(replacement) && !replacement.equals(existingReference)) { + nextReference.getResourceReference().setReference(replacement); + } + } + } + } + + } + + 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); + } + + addSection(theSection, theCompositionBuilder, sectionResourcesToInclude, theGlobalResourcesToInclude); + } + + @SuppressWarnings("unchecked") + private void addSection(SectionRegistry.Section theSection, CompositionBuilder theCompositionBuilder, ResourceInclusionCollection theResourcesToInclude, ResourceInclusionCollection theGlobalResourcesToInclude) { + + CompositionBuilder.SectionBuilder sectionBuilder = theCompositionBuilder.addSection(); + + sectionBuilder.setTitle(theSection.getTitle()); + sectionBuilder.addCodeCoding(LOINC_URI, theSection.getSectionCode(), theSection.getSectionDisplay()); + + for (IBaseResource next : theResourcesToInclude.getResources()) { + + 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("uri").newInstance(); + narrativeLinkUri.setValueAsString(narrativeLinkValue); + narrativeLink.setValue(narrativeLinkUri); + + sectionBuilder.addEntry(next.getIdElement()); + } + + String narrative = createSectionNarrative(theSection, theResourcesToInclude, theGlobalResourcesToInclude); + sectionBuilder.setText("generated", narrative); + } + + private CompositionBuilder createComposition(IBaseResource thePatient, IpsContext context, IBaseResource author) { + CompositionBuilder compositionBuilder = new CompositionBuilder(myFhirContext); + compositionBuilder.setId(IdType.newRandomUuid()); + + compositionBuilder.setStatus(Composition.CompositionStatus.FINAL.toCode()); + 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.addAuthor(author.getIdElement()); + + return compositionBuilder; + } + + private String determinePatientCompartmentSearchParameterName(String theResourceType) { + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceType); + return resourceDef.getSearchParamsForCompartmentName("Patient").get(0).getName(); + } + + private void massageResourceId(IpsContext theIpsContext, IBaseResource theResource) { + IIdType id = myGenerationStrategy.massageResourceId(theIpsContext, theResource); + theResource.setId(id); + } + + private String createSectionNarrative(SectionRegistry.Section theSection, ResourceInclusionCollection theResources, ResourceInclusionCollection theGlobalResourceCollection) { + CustomThymeleafNarrativeGenerator generator = newNarrativeGenerator(theGlobalResourceCollection); + + Bundle bundle = new Bundle(); + for (IBaseResource resource : theResources.getResources()) { + BundleEntrySearchModeEnum searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(resource); + if (searchMode == BundleEntrySearchModeEnum.MATCH) { + bundle.addEntry().setResource((Resource) resource); + } + } + String profile = theSection.getProfile(); + bundle.getMeta().addProfile(profile); + + // Generate the narrative + return generator.generateResourceNarrative(myFhirContext, bundle); + } + + @Nonnull + private CustomThymeleafNarrativeGenerator newNarrativeGenerator(ResourceInclusionCollection theGlobalResourceCollection) { + List narrativePropertyFiles = myGenerationStrategy.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 generator; + } + + + + + + + + + + + + + + + + +/* + + + + + + + private static HashMap> hashPrimaries(List resourceList) { + HashMap> iPSResourceMap = new HashMap>(); + + for (Resource resource : resourceList) { + for (PatientSummary.IPSSection iPSSection : PatientSummary.IPSSection.values()) { + if ( SectionTypes.get(iPSSection).contains(resource.getResourceType()) ) { + if ( !(resource.getResourceType() == ResourceType.Observation) || isObservationinSection(iPSSection, (Observation) resource)) { + if (iPSResourceMap.get(iPSSection) == null) { + iPSResourceMap.put(iPSSection, new ArrayList()); + } + iPSResourceMap.get(iPSSection).add(resource); + } + } + } + } + + return iPSResourceMap; + } + + + + private static HashMap> filterPrimaries(HashMap> sectionPrimaries) { + HashMap> filteredPrimaries = new HashMap>(); + for ( PatientSummary.IPSSection section : sectionPrimaries.keySet() ) { + List filteredList = new ArrayList(); + for (Resource resource : sectionPrimaries.get(section)) { + if (passesFilter(section, resource)) { + filteredList.add(resource); + } + } + if (filteredList.size() > 0) { + filteredPrimaries.put(section, filteredList); + } + } + return filteredPrimaries; + } + + private static List pruneResources(Patient patient, List resources, HashMap> sectionPrimaries, FhirContext ctx) { + List resourceIds = new ArrayList(); + List followedIds = new ArrayList(); + + HashMap resourcesById = new HashMap(); + for (Resource resource : resources) { + resourcesById.put(resource.getIdElement().getIdPart(), resource); + } + String patientId = patient.getIdElement().getIdPart(); + resourcesById.put(patientId, patient); + + recursivePrune(patientId, resourceIds, followedIds, resourcesById, ctx); + + for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) { + for (Resource resource : sectionPrimaries.get(section)) { + String resourceId = resource.getIdElement().getIdPart(); + recursivePrune(resourceId, resourceIds, followedIds, resourcesById, ctx); + } + } + + List prunedResources = new ArrayList(); + + for (Resource resource : resources) { + if (resourceIds.contains(resource.getIdElement().getIdPart())) { + prunedResources.add(resource); + } + } + + return prunedResources; + } + + private static Void recursivePrune(String resourceId, List resourceIds, List followedIds, HashMap resourcesById, FhirContext ctx) { + if (!resourceIds.contains(resourceId)) { + resourceIds.add(resourceId); + } + + Resource resource = resourcesById.get(resourceId); + if (resource != null) { + ctx.newTerser().getAllResourceReferences(resource).stream() + .map( r -> r.getResourceReference().getReferenceElement().getIdPart() ) + .forEach( id -> { + if (!followedIds.contains(id)) { + followedIds.add(id); + recursivePrune(id, resourceIds, followedIds, resourcesById, ctx); + } + }); + } + + return null; + } + + private static List addLinkToResources(List resources, HashMap> sectionPrimaries, Composition composition) { + List linkedResources = new ArrayList(); + HashMap valueUrls = new HashMap(); + + String url = "http://hl7.org/fhir/StructureDefinition/NarrativeLink"; + String valueUrlBase = composition.getId() + "#"; + + for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) { + String profile = SectionProfiles.get(section); + String[] arr = profile.split("/"); + String profileName = arr[arr.length - 1]; + String sectionValueUrlBase = valueUrlBase + profileName.split("-uv-")[0]; + + for (Resource resource : sectionPrimaries.get(section)) { + String valueUrl = sectionValueUrlBase + "-" + resource.getIdElement().getIdPart(); + valueUrls.put(resource.getIdElement().getIdPart(), valueUrl); + } + } + + for (Resource resource : resources) { + if (valueUrls.containsKey(resource.getIdElement().getIdPart())) { + String valueUrl = valueUrls.get(resource.getIdElement().getIdPart()); + Extension extension = new Extension(); + extension.setUrl(url); + extension.setValue(new UriType(valueUrl)); + DomainResource domainResource = (DomainResource) resource; + domainResource.addExtension(extension); + resource = (Resource) domainResource; + } + linkedResources.add(resource); + } + + return linkedResources; + } + + private static HashMap createNarratives(HashMap> sectionPrimaries, List resources, FhirContext ctx) { + HashMap hashedNarratives = new HashMap(); + + for (PatientSummary.IPSSection section : sectionPrimaries.keySet()) { + String narrative = createSectionNarrative(section, resources, ctx); + hashedNarratives.put(section, narrative); + } + + return hashedNarratives; + } + + + + +*/ + + + private static class ResourceInclusionCollection { + + private final List myResources = new ArrayList<>(); + private final Map myIdToResource = new HashMap<>(); + private final Map myOriginalIdToNewId = new HashMap<>(); + + public List getResources() { + return myResources; + } + + /** + * @param theOriginalResourceId Must be an unqualified versionless ID + */ + public void addResourceIfNotAlreadyPresent(IBaseResource theResource, String theOriginalResourceId) { + assert theOriginalResourceId.matches("([A-Z][a-z]([A-Za-z]+)/[a-zA-Z0-9._-]+)|(urn:uuid:[0-9a-z-]+)") : "Not an unqualified versionless ID: " + theOriginalResourceId; + + String resourceId = theResource.getIdElement().toUnqualifiedVersionless().getValue(); + if (myIdToResource.containsKey(resourceId)) { + return; + } + + myResources.add(theResource); + myIdToResource.put(resourceId, theResource); + myOriginalIdToNewId.put(theOriginalResourceId, resourceId); + } + + public String getIdSubstitution(String theExistingReference) { + return myOriginalIdToNewId.get(theExistingReference); + } + + public IBaseResource getResourceById(IIdType theReference) { + return myIdToResource.get(theReference.toUnqualifiedVersionless().getValue()); + } + + @Nullable + public IBaseResource getResourceByOriginalId(String theOriginalResourceId) { + String newResourceId = myOriginalIdToNewId.get(theOriginalResourceId); + if (newResourceId != null) { + return myIdToResource.get(newResourceId); + } + return null; + } + + public boolean isEmpty() { + return myResources.isEmpty(); + } + } + + +} 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 new file mode 100644 index 00000000000..8371430dfeb --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/provider/IpsOperationProvider.java @@ -0,0 +1,75 @@ +package ca.uhn.fhir.jpa.ips.provider; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +import ca.uhn.fhir.jpa.ips.generator.IIpsGeneratorSvc; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import ca.uhn.fhir.rest.annotation.IdParam; +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 org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IIdType; + +public class IpsOperationProvider { + + private final IIpsGeneratorSvc myIpsGeneratorSvc; + + /** + * Constructor + */ + public IpsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) { + myIpsGeneratorSvc = theIpsGeneratorSvc; + } + + + /** + * Patient/123/$summary + */ + @Operation(name = JpaConstants.OPERATION_SUMMARY, idempotent = true, bundleType = BundleTypeEnum.DOCUMENT, typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL) + public IBaseBundle patientInstanceSummary( + @IdParam + IIdType thePatientId, + + RequestDetails theRequestDetails + ) { + return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientId); + } + + /** + * /Patient/$summary?identifier=foo|bar + */ + @Operation(name = JpaConstants.OPERATION_SUMMARY, idempotent = true, bundleType = BundleTypeEnum.DOCUMENT, typeName = "Patient", canonicalUrl = JpaConstants.SUMMARY_OPERATION_URL) + public IBaseBundle patientTypeSummary( + + @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, + + RequestDetails theRequestDetails + ) { + return myIpsGeneratorSvc.generateIps(theRequestDetails, thePatientIdentifier); + } + +} 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 new file mode 100644 index 00000000000..a97df7705ab --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/java/ca/uhn/fhir/jpa/ips/strategy/DefaultIpsGenerationStrategy.java @@ -0,0 +1,365 @@ +package ca.uhn.fhir.jpa.ips.strategy; + +/*- + * #%L + * HAPI FHIR JPA Server - International Patient Summary (IPS) + * %% + * Copyright (C) 2014 - 2023 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% + */ + +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.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +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 IMMUNIZATIONS: + case PROCEDURES: + case MEDICAL_DEVICES: + case ILLNESS_HISTORY: + case FUNCTIONAL_STATUS: + 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 ALLERGY_INTOLERANCE: + case PROBLEM_LIST: + case IMMUNIZATIONS: + 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/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 new file mode 100644 index 00000000000..7ab1f745ad4 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html @@ -0,0 +1,37 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Advance Directives
ScopeStatusAction ControlledDate
ScopeStatusAction ControlledAction ControlledDate
+
+ + 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 new file mode 100644 index 00000000000..2588697bc43 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html @@ -0,0 +1,43 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Allergies And Intolerances
AllergenStatusCategoryReactionSeverityCommentsOnset
AllergenStatusCategoryReactionSeverityCommentsOnset
+
+ + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/composition.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/composition.html new file mode 100644 index 00000000000..505a799a597 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/composition.html @@ -0,0 +1,11 @@ + +
+
+

+
+
+
+
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 new file mode 100644 index 00000000000..76e91a30786 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html @@ -0,0 +1,74 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Diagnostic Results: Observations
CodeResultUnitInterpretationReference RangeCommentsDate
CodeResultUnitInterpretationReference RangeCommentsDate
+ + + + + + + + + + + + + + + + + + + + + + + +
Diagnostic Results: Diagnostic Reports
CodeDate
DeviceDate
+
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 new file mode 100644 index 00000000000..6eea575abcc --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html @@ -0,0 +1,37 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Functional Status
AssessmentStatusFindingCommentsDate
AssessmentStatusFindingCommentsDate
+
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 new file mode 100644 index 00000000000..d283cc3a8d8 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html @@ -0,0 +1,31 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
History Of Procedures
ProcedureCommentsDate
ProcedureCommentsDate
+
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 new file mode 100644 index 00000000000..c26b9cab63d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html @@ -0,0 +1,43 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Immunizations
ImmunizationStatusDose NumberManufacturerLot NumberCommentsDate
ImmunizationStatusCommentsManufacturerLot NumberCommentsDate
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties new file mode 100644 index 00000000000..3036388c3fe --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/ips-narratives.properties @@ -0,0 +1,73 @@ +################################################ +# IPS Sections +################################################ + +ips-allergyintolerance.resourceType=Bundle +ips-allergyintolerance.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AllergiesAndIntolerances-uv-ips +ips-allergyintolerance.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html + +ips-medicationsummary.resourceType=Bundle +ips-medicationsummary.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/MedicationSummary-uv-ips +ips-medicationsummary.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html + +ips-problemlist.resourceType=Bundle +ips-problemlist.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/ProblemList-uv-ips +ips-problemlist.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/problemlist.html + +ips-immunizations.resourceType=Bundle +ips-immunizations.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Immunizations-uv-ips +ips-immunizations.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/immunizations.html + +ips-historyofprocedures.resourceType=Bundle +ips-historyofprocedures.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/HistoryOfProcedures-uv-ips +ips-historyofprocedures.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html + +ips-medicaldevices.resourceType=Bundle +ips-medicaldevices.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/MedicalDevices-uv-ips +ips-medicaldevices.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html + +ips-diagnosticresults.resourceType=Bundle +ips-diagnosticresults.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/DiagnosticResults-uv-ips +ips-diagnosticresults.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/diagnosticresults.html + +ips-vitalsigns.resourceType=Bundle +ips-vitalsigns.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/VitalSigns-uv-ips +ips-vitalsigns.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html + +ips-pregnancy.resourceType=Bundle +ips-pregnancy.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Pregnancy-uv-ips +ips-pregnancy.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/pregnancy.html + +ips-socialhistory.resourceType=Bundle +ips-socialhistory.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/SocialHistory-uv-ips +ips-socialhistory.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/socialhistory.html + +ips-pasthistoryofillness.resourceType=Bundle +ips-pasthistoryofillness.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/PastHistoryOfIllness-uv-ips +ips-pasthistoryofillness.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html + +ips-functionalstatus.resourceType=Bundle +ips-functionalstatus.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/FunctionalStatus-uv-ips +ips-functionalstatus.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/functionalstatus.html + +ips-planofcare.resourceType=Bundle +ips-planofcare.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/PlanOfCare-uv-ips +ips-planofcare.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/planofcare.html + +ips-advancedirectives.resourceType=Bundle +ips-advancedirectives.profile=http://hl7.org/fhir/uv/ips/StructureDefinition/AdvanceDirectives-uv-ips +ips-advancedirectives.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/advancedirectives.html + +################################################ +# Utility Fragments +################################################ + +ips-utility-fragments.fragmentName=IpsUtilityFragments +ips-utility-fragments.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html + +################################################ +# IPS Global Composition Narrative (applied at the end) +################################################ + +ips-global.resourceType=Composition +ips-global.narrative=classpath:ca/uhn/fhir/jpa/ips/narrative/composition.html 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 new file mode 100644 index 00000000000..3e8dc390a66 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicaldevices.html @@ -0,0 +1,34 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Medical Devices
DeviceStatusCommentsDate Recorded
DeviceStatusCommentsDate Recorded
+
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 new file mode 100644 index 00000000000..e7bd2ae9994 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html @@ -0,0 +1,78 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Medication Summary: Medication Requests
MedicationStatusRouteSigCommentsAuthored Date
MedicationStatusRouteSigCommentsAuthored Date
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Medication Summary: Medication Statements
MedicationStatusRouteSigDate
MedicationStatusRouteSigDate
+
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 new file mode 100644 index 00000000000..f0024a2a7a9 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html @@ -0,0 +1,34 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Past History of Illnesses
Medical ProblemsStatusCommentsDate
Medical ProblemStatusCommentsOnset Date
+
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 new file mode 100644 index 00000000000..b052097136b --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/planofcare.html @@ -0,0 +1,37 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Plan of Care
ActivityIntentCommentsPlanned StartPlanned End
ActivityIntentCommentsPlanned StartPlanned 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 new file mode 100644 index 00000000000..579c83c2f53 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pregnancy.html @@ -0,0 +1,34 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Pregnancy
CodeResultCommentsDate
CodeResultCommentsDate
+
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 new file mode 100644 index 00000000000..2a97ed8b2ad --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html @@ -0,0 +1,34 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Problem List
Medical ProblemsStatusCommentsOnset Date
Medical ProblemsStatusCommentsOnset Date
+
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 new file mode 100644 index 00000000000..cbcc467de48 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/socialhistory.html @@ -0,0 +1,37 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Social History
CodeResultUnitCommentsDate
CodeResultUnitCommentsDate
+
diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html new file mode 100644 index 00000000000..5ef138d7ae2 --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html @@ -0,0 +1,243 @@ + + + + + + + + + Device + + + + + + + + + + Org Name + + + + + + + + + + Medication + + + Medication + + + + + + + + + Medication + + + + + + Medication + + + + + + + + Dose Number + Dose Number + + + + + + + + + + Result + Result + + Result + + Result + + + + + + + + Unit + + + + + + + + + + Date + Date + + + + + + + + Date + + Date + + Date + Date + + Date + + + + + + + + Date + + Date + + Date + Date + + Date + + + + + + + + Date + Date + + + + + + + + Date Recorded + + + + + + + + + + + + + + + + + + + + + Interpretation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + Reaction + + Reaction + + + + + + + + + + + + + + + + + + + + + + Dose Number + + + + + + + Reference Range + + Reference Range + + + + 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 new file mode 100644 index 00000000000..681aab9fa8e --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html @@ -0,0 +1,40 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Vital Signs
CodeResultUnitInterpretationCommentsDate
CodeResultUnitInterpretationCommentsDate
+
diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationTest.java new file mode 100644 index 00000000000..5de2d5517fd --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGenerationTest.java @@ -0,0 +1,87 @@ +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.ips.api.IIpsGenerationStrategy; +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 org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Parameters; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ContextConfiguration(classes = {IpsGenerationTest.IpsConfig.class}) +public class IpsGenerationTest extends BaseResourceProviderR4Test { + + @Autowired + private IpsOperationProvider myIpsOperationProvider; + + @BeforeEach + public void beforeEach() { + myServer.withServer(t->t.registerProvider(myIpsOperationProvider)); + } + + @AfterEach + public void afterEach() { + myServer.withServer(t->t.unregisterProvider(myIpsOperationProvider)); + } + + + @Test + public void testGenerateLargePatientSummary() { + Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything.json.gz"); + sourceData.setType(Bundle.BundleType.TRANSACTION); + for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) { + nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT); + nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + Bundle outcome = mySystemDao.transaction(mySrd, sourceData); + ourLog.info("Created {} resources", outcome.getEntry().size()); + + Bundle output = myClient + .operation() + .onInstance("Patient/f15d2419-fbff-464a-826d-0afe8f095771") + .named(JpaConstants.OPERATION_SUMMARY) + .withNoParameters(Parameters.class) + .returnResourceType(Bundle.class) + .execute(); + + ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output)); + + assertEquals(37, output.getEntry().size()); + } + + + @Configuration + public static class IpsConfig { + + @Bean + public IIpsGenerationStrategy ipsGenerationStrategy() { + return new DefaultIpsGenerationStrategy(); + } + + @Bean + public IIpsGeneratorSvc ipsGeneratorSvc(FhirContext theFhirContext, IIpsGenerationStrategy theGenerationStrategy, DaoRegistry theDaoRegistry) { + return new IpsGeneratorSvcImpl(theFhirContext, theGenerationStrategy, theDaoRegistry); + } + + @Bean + public IpsOperationProvider ipsOperationProvider(IIpsGeneratorSvc theIpsGeneratorSvc) { + return new IpsOperationProvider(theIpsGeneratorSvc); + } + + + } + + +} 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 new file mode 100644 index 00000000000..12ef04f993d --- /dev/null +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java @@ -0,0 +1,465 @@ +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.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +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 com.gargoylesoftware.htmlunit.html.DomElement; +import com.gargoylesoftware.htmlunit.html.DomNodeList; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlTable; +import com.gargoylesoftware.htmlunit.html.HtmlTableRow; +import com.google.common.collect.Lists; +import org.hamcrest.Matchers; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.AllergyIntolerance; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.ClinicalImpression; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.Condition; +import org.hl7.fhir.r4.model.Consent; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.Device; +import org.hl7.fhir.r4.model.DeviceUseStatement; +import org.hl7.fhir.r4.model.DiagnosticReport; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Immunization; +import org.hl7.fhir.r4.model.Medication; +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.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.PositiveIntType; +import org.hl7.fhir.r4.model.Procedure; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class IpsGeneratorSvcImplTest { + + public static final String MEDICATION_ID = "Medication/tyl"; + public static final String MEDICATION_STATEMENT_ID = "MedicationStatement/meds"; + public static final String MEDICATION_STATEMENT_ID2 = "MedicationStatement/meds2"; + private static final List> RESOURCE_TYPES = Lists.newArrayList( + AllergyIntolerance.class, + CarePlan.class, + Condition.class, + Consent.class, + ClinicalImpression.class, + DeviceUseStatement.class, + DiagnosticReport.class, + Immunization.class, + MedicationRequest.class, + MedicationStatement.class, + MedicationAdministration.class, + MedicationDispense.class, + Observation.class, + Patient.class, + Procedure.class + ); + private static final Logger ourLog = LoggerFactory.getLogger(IpsGeneratorSvcImplTest.class); + private final FhirContext myFhirContext = FhirContext.forR4Cached(); + private final DaoRegistry myDaoRegistry = new DaoRegistry(myFhirContext); + private IIpsGeneratorSvc mySvc; + private DefaultIpsGenerationStrategy myStrategy; + + @BeforeEach + public void beforeEach() { + myDaoRegistry.setResourceDaos(Collections.emptyList()); + + myStrategy = new DefaultIpsGenerationStrategy(); + mySvc = new IpsGeneratorSvcImpl(myFhirContext, myStrategy, myDaoRegistry); + } + + @Test + public void testGenerateIps() { + // Setup + registerResourceDaosForSmallPatientSet(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new TokenParam("http://foo", "bar")); + + // Verify + ourLog.info("Generated IPS:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); + + List contentResourceTypes = toEntryResourceTypeStrings(outcome); + assertThat(contentResourceTypes.toString(), contentResourceTypes, + Matchers.contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "MedicationStatement", "MedicationStatement", "Condition", "Condition", "Condition", "Organization")); + + Composition composition = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section; + + // Allergy and Intolerances has no content + section = composition.getSection().get(0); + assertEquals("Allergies and Intolerances", section.getTitle()); + assertThat(section.getText().getDivAsString(), + containsString("No information about allergies")); + + // Medication Summary has a resource + section = composition.getSection().get(1); + assertEquals("Medication List", section.getTitle()); + assertThat(section.getText().getDivAsString(), + containsString("Oral use")); + + // Composition itself should also have a narrative + String compositionNarrative = composition.getText().getDivAsString(); + ourLog.info("Composition narrative: {}", compositionNarrative); + assertThat(compositionNarrative, containsString("Allergies and Intolerances")); + assertThat(compositionNarrative, containsString("Pregnancy")); + + } + + @Test + public void testMedicationSummary_MedicationStatementWithMedicationReference() throws IOException { + // Setup Patient + registerPatientDaoWithRead(); + + // Setup Medication + MedicationStatement + Medication medication = createSecondaryMedication(MEDICATION_ID); + MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID); + IFhirResourceDao medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class); + when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication))); + + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123")); + + // Verify Bundle Contents + List contentResourceTypes = toEntryResourceTypeStrings(outcome); + assertThat(contentResourceTypes.toString(), contentResourceTypes, + Matchers.contains("Composition", "Patient", "AllergyIntolerance", "MedicationStatement", "Medication", "Condition", "Organization")); + MedicationStatement actualMedicationStatement = (MedicationStatement) outcome.getEntry().get(3).getResource(); + Medication actualMedication = (Medication) outcome.getEntry().get(4).getResource(); + assertThat(actualMedication.getId(), startsWith("urn:uuid:")); + assertEquals(actualMedication.getId(), actualMedicationStatement.getMedicationReference().getReference()); + + // Verify + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = compositions + .getSection() + .stream() + .filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.MEDICATION_SUMMARY).getTitle())) + .findFirst() + .orElseThrow(); + + 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); + HtmlTableRow row = table.getBodies().get(0).getRows().get(0); + assertEquals("Tylenol", row.getCell(0).asNormalizedText()); + assertEquals("Active", row.getCell(1).asNormalizedText()); + assertEquals("Oral", row.getCell(2).asNormalizedText()); + assertEquals("DAW", row.getCell(3).asNormalizedText()); + assertThat(row.getCell(4).asNormalizedText(), containsString("2023")); + } + + @Test + public void testMedicationSummary_DuplicateSecondaryResources() { + myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null))); + + // Setup Patient + registerPatientDaoWithRead(); + + // Setup Medication + MedicationStatement + Medication medication = createSecondaryMedication(MEDICATION_ID); + MedicationStatement medicationStatement = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID); + Medication medication2 = createSecondaryMedication(MEDICATION_ID); // same ID again (could happen if we span multiple pages of results) + MedicationStatement medicationStatement2 = createPrimaryMedicationStatement(MEDICATION_ID, MEDICATION_STATEMENT_ID2); + IFhirResourceDao medicationStatementDao = registerResourceDaoWithNoData(MedicationStatement.class); + when(medicationStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationStatement, medication, medicationStatement2, medication2))); + + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123")); + + // Verify Bundle Contents + List contentResourceTypes = toEntryResourceTypeStrings(outcome); + assertThat(contentResourceTypes.toString(), contentResourceTypes, + Matchers.contains( + "Composition", + "Patient", + "MedicationStatement", + "Medication", + "MedicationStatement", + "Organization")); + + } + + /** + * Make sure that if a resource is added as a secondary resource but then gets included as a + * primary resource, we include it. + */ + @Test + public void testMedicationSummary_ResourceAppearsAsSecondaryThenPrimary() throws IOException { + myStrategy.setSectionRegistry(new SectionRegistry().addGlobalCustomizer(t -> t.withNoInfoGenerator(null))); + + // Setup Patient + 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/123")); + + // Verify Bundle Contents + List contentResourceTypes = toEntryResourceTypeStrings(outcome); + assertThat(contentResourceTypes.toString(), contentResourceTypes, + Matchers.contains( + "Composition", + "Patient", + "MedicationStatement", + "Medication", + "MedicationStatement", + "Organization")); + + // Verify narrative - should have 2 rows (one for each primary MedicationStatement) + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = compositions + .getSection() + .stream() + .filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.MEDICATION_SUMMARY).getTitle())) + .findFirst() + .orElseThrow(); + + 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(2, table.getBodies().get(0).getRows().size()); + } + + @Test + public void testMedicalDevices_DeviceUseStatementWithDevice() throws IOException { + // Setup Patient + registerPatientDaoWithRead(); + + // Setup Medication + MedicationStatement + Device device = new Device(); + device.setId(new IdType("Device/pm")); + device.getType().addCoding().setDisplay("Pacemaker"); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(device, BundleEntrySearchModeEnum.INCLUDE); + + DeviceUseStatement deviceUseStatement = new DeviceUseStatement(); + deviceUseStatement.setId("DeviceUseStatement/dus"); + deviceUseStatement.setDevice(new Reference("Device/pm")); + deviceUseStatement.setStatus(DeviceUseStatement.DeviceUseStatementStatus.ACTIVE); + deviceUseStatement.addNote().setText("This is some note text"); + deviceUseStatement.setRecordedOnElement(new DateTimeType("2023-01-01T12:22:33Z")); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(deviceUseStatement, BundleEntrySearchModeEnum.MATCH); + + IFhirResourceDao deviceUseStatementDao = registerResourceDaoWithNoData(DeviceUseStatement.class); + when(deviceUseStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(deviceUseStatement, device))); + + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123")); + + // Verify + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = compositions + .getSection() + .stream() + .filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.MEDICAL_DEVICES).getTitle())) + .findFirst() + .orElseThrow(); + + 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); + HtmlTableRow row = table.getBodies().get(0).getRows().get(0); + assertEquals("Pacemaker", row.getCell(0).asNormalizedText()); + assertEquals("ACTIVE", row.getCell(1).asNormalizedText()); + assertEquals("This is some note text", row.getCell(2).asNormalizedText()); + } + + @Test + public void testImmunizations() throws IOException { + // Setup Patient + registerPatientDaoWithRead(); + + // Setup Medication + MedicationStatement + Organization org = new Organization(); + org.setId(new IdType("Organization/pfizer")); + org.setName("Pfizer"); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(org, BundleEntrySearchModeEnum.INCLUDE); + + Immunization immunization = new Immunization(); + immunization.setId("Immunization/imm"); + immunization.getVaccineCode().addCoding().setDisplay("SpikeVax"); + immunization.setStatus(Immunization.ImmunizationStatus.COMPLETED); + immunization.addProtocolApplied().setDoseNumber(new PositiveIntType(2)); + immunization.addProtocolApplied().setDoseNumber(new PositiveIntType(4)); + immunization.setManufacturer(new Reference("Organization/pfizer")); + immunization.setLotNumber("35"); + immunization.addNote().setText("Hello World"); + immunization.setOccurrence(new DateTimeType("2023-01-01T11:22:33Z")); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(immunization, BundleEntrySearchModeEnum.MATCH); + + IFhirResourceDao deviceUseStatementDao = registerResourceDaoWithNoData(Immunization.class); + when(deviceUseStatementDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(immunization, org))); + + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType("Patient/123")); + + // Verify + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = compositions + .getSection() + .stream() + .filter(t -> t.getTitle().equals(myStrategy.getSectionRegistry().getSection(IpsSectionEnum.IMMUNIZATIONS).getTitle())) + .findFirst() + .orElseThrow(); + + 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); + HtmlTableRow row = table.getBodies().get(0).getRows().get(0); + assertEquals("SpikeVax", row.getCell(0).asNormalizedText()); + assertEquals("COMPLETED", row.getCell(1).asNormalizedText()); + assertEquals("2 , 4", row.getCell(2).asNormalizedText()); + assertEquals("Pfizer", row.getCell(3).asNormalizedText()); + assertEquals("35", row.getCell(4).asNormalizedText()); + assertEquals("Hello World", row.getCell(5).asNormalizedText()); + assertThat(row.getCell(6).asNormalizedText(), containsString("2023")); + } + + private void registerPatientDaoWithRead() { + IFhirResourceDao patientDao = registerResourceDaoWithNoData(Patient.class); + Patient patient = new Patient(); + patient.setId("Patient/123"); + when(patientDao.read(any(), any())).thenReturn(patient); + } + + private void registerRemainingResourceDaos() { + for (var next : RESOURCE_TYPES) { + if (!myDaoRegistry.isResourceTypeSupported(myFhirContext.getResourceType(next))) { + IFhirResourceDao dao = registerResourceDaoWithNoData(next); + when(dao.search(any(), any())).thenReturn(new SimpleBundleProvider()); + } + } + } + + private IBundleProvider bundleProviderWithAllOfType(Bundle theSourceData, Class theType) { + List resources = theSourceData + .getEntry() + .stream() + .filter(t -> t.getResource() != null && theType.isAssignableFrom(t.getResource().getClass())) + .map(Bundle.BundleEntryComponent::getResource) + .collect(Collectors.toList()); + resources.forEach(t -> ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(t, BundleEntrySearchModeEnum.MATCH)); + return new SimpleBundleProvider(resources); + } + + @SuppressWarnings("unchecked") + @Nonnull + private IFhirResourceDao registerResourceDaoWithNoData(@Nonnull Class theType) { + IFhirResourceDao dao = mock(IFhirResourceDao.class); + when(dao.getResourceType()).thenReturn(theType); + myDaoRegistry.register(dao); + return dao; + } + + @SuppressWarnings("rawtypes") + private void registerResourceDaosForSmallPatientSet() { + Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/small-patient-everything.json.gz"); + + for (var nextType : IpsGeneratorSvcImplTest.RESOURCE_TYPES) { + IFhirResourceDao dao = registerResourceDaoWithNoData(nextType); + when(dao.search(any(), any())).thenReturn(bundleProviderWithAllOfType(sourceData, nextType)); + } + + } + + @Nonnull + private static List toEntryResourceTypeStrings(Bundle outcome) { + List contentResourceTypes = outcome + .getEntry() + .stream() + .map(t -> t.getResource().getResourceType().name()) + .collect(Collectors.toList()); + return contentResourceTypes; + } + + @Nonnull + private static Medication createSecondaryMedication(String medicationId) { + Medication medication = new Medication(); + medication.setId(new IdType(medicationId)); + medication.getCode().addCoding().setDisplay("Tylenol"); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE); + return medication; + } + + @Nonnull + private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) { + MedicationStatement medicationStatement = new MedicationStatement(); + medicationStatement.setId(medicationStatementId); + medicationStatement.setMedication(new Reference(medicationId)); + medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE); + medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral"); + medicationStatement.getDosageFirstRep().setText("DAW"); + medicationStatement.setEffective(new DateTimeType("2023-01-01T11:22:33Z")); + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement, BundleEntrySearchModeEnum.MATCH); + return medicationStatement; + } + +} diff --git a/hapi-fhir-jpaserver-ips/src/test/resources/large-patient-everything.json.gz b/hapi-fhir-jpaserver-ips/src/test/resources/large-patient-everything.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..dd2a283ecbde40ccc10d05012227f5e42a4fa411 GIT binary patch literal 15551 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 literal 0 HcmV?d00001 diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 222ec3408a7..63415f75f11 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 0f396428586..00c77a788cc 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index d6021fd0e2f..359a45a5e50 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -277,6 +277,14 @@ public class JpaConstants { public static final String HEADER_REWRITE_HISTORY = "X-Rewrite-History"; public static final String SKIP_REINDEX_ON_UPDATE = "SKIP-REINDEX-ON-UPDATE"; + /** + * IPS Generation operation name + */ + public static final String OPERATION_SUMMARY = "$summary"; + /** + * IPS Generation operation URL + */ + public static final String SUMMARY_OPERATION_URL = "http://hl7.org/fhir/uv/ips/OperationDefinition/summary"; /** * Non-instantiable diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 5fa0d9cf16a..4fe982a4c2b 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 33efee1e0c8..5ae8f625fc2 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index 14bdcb8b8e1..1f436a775f9 100644 --- a/hapi-fhir-jpaserver-test-dstu2/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/pom.xml b/hapi-fhir-jpaserver-test-dstu3/pom.xml index ea27c3f8cbf..bfdca6c9adf 100644 --- a/hapi-fhir-jpaserver-test-dstu3/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu3/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 281606b6519..5005faf7634 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index 2552808f2e2..4dd34aab214 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -1402,6 +1402,10 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test .setUrl("http://acme.org/bar") .setValue(new StringType("HELLOHELLO")); + ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); + ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(siblingSp)); + + IIdType p2id = myPatientDao.create(patient).getId().toUnqualifiedVersionless(); SearchParameterMap map; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java index f694281d35b..11d36a8fa6d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvcLoincJpaTest.java @@ -20,8 +20,10 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { private TermLoaderSvcImpl mySvc; private ZipCollectionBuilder myFiles; + @Override @BeforeEach - public void before() { + public void before() throws Exception { + super.before(); mySvc = new TermLoaderSvcImpl(myTerminologyDeferredStorageSvc, myTermCodeSystemStorageSvc); myFiles = new ZipCollectionBuilder(); @@ -29,7 +31,6 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { @Test public void testLoadLoincMultipleVersions() throws IOException { - // Load LOINC marked as version 2.67 TermTestUtil.addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v267_loincupload.properties"); @@ -61,7 +62,10 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { mySvc.loadLoinc(myFiles.getFiles(), mySrd); myTerminologyDeferredStorageSvc.saveAllDeferred(); - await().atMost(10, SECONDS).until(() -> myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true)); + await().atMost(10, SECONDS).until(() -> { + myBatch2JobHelper.awaitNoJobsRunning(); + return myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true); + }); logAllCodeSystemsAndVersionsCodeSystemsAndVersions(); @@ -89,8 +93,10 @@ public class TerminologyLoaderSvcLoincJpaTest extends BaseJpaR4Test { TermTestUtil.addLoincMandatoryFilesWithPropertiesFileToZip(myFiles, "v268_loincupload.properties"); mySvc.loadLoinc(myFiles.getFiles(), mySrd); myTerminologyDeferredStorageSvc.saveAllDeferred(); - await().atMost(10, SECONDS).until(() -> myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true)); - + await().atMost(10, SECONDS).until(() -> { + myBatch2JobHelper.awaitNoJobsRunning(); + return myTerminologyDeferredStorageSvc.isStorageQueueEmpty(true); + }); runInTransaction(() -> { assertEquals(1, myTermCodeSystemDao.count()); diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index fe9c57cd060..73089187c1d 100644 --- a/hapi-fhir-jpaserver-test-r4b/pom.xml +++ b/hapi-fhir-jpaserver-test-r4b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/pom.xml b/hapi-fhir-jpaserver-test-r5/pom.xml index 5718a857a03..1ab431e5507 100644 --- a/hapi-fhir-jpaserver-test-r5/pom.xml +++ b/hapi-fhir-jpaserver-test-r5/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index f05a501c6c4..71ff3bbabb0 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java index 4ff7212d2eb..4ac9eb7c552 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java @@ -268,8 +268,6 @@ public abstract class BaseJpaTest extends BaseTest { DaoConfig defaultConfig = new DaoConfig(); myDaoConfig.setAdvancedHSearchIndexing(defaultConfig.isAdvancedHSearchIndexing()); myDaoConfig.setAllowContainsSearches(defaultConfig.isAllowContainsSearches()); - - } @AfterEach diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/Batch2JobHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/Batch2JobHelper.java index a1ac9bc6fb2..0af74a7fa66 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/Batch2JobHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/Batch2JobHelper.java @@ -243,4 +243,10 @@ public class Batch2JobHelper { myJobMaintenanceService.runMaintenancePass(); } + public void cancelAllJobsAndAwaitCancellation() { + List instances = myJobPersistence.fetchInstances(1000, 0); + for (JobInstance next : instances) { + myJobPersistence.cancelInstance(next.getInstanceId()); + } + } } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 68079a1e7fa..05570a1ed4e 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 87ae31997c0..c657810ada3 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index 17a218f8ff2..f06e5ce7771 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -56,11 +56,19 @@ flexmark + + + javax.servlet + javax.servlet-api + provided + + ca.uhn.hapi.fhir hapi-fhir-test-utilities ${project.version} + test ch.qos.logback diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 6b5d5458e65..851c17947d6 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java index 993e95bde1f..109fe623a84 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/SimpleBundleProvider.java @@ -130,7 +130,7 @@ public class SimpleBundleProvider implements IBundleProvider { @Nonnull @Override public List getResources(int theFromIndex, int theToIndex) { - return (List) myList.subList(theFromIndex, Math.min(theToIndex, myList.size())); + return (List) myList.subList(Math.min(theFromIndex, myList.size()), Math.min(theToIndex, myList.size())); } @Override diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index 790e4451e21..02300bcde89 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml index 84bd29dd098..e72f40f4dd3 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml @@ -20,7 +20,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT com.github.ben-manes.caffeine diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index 62742ca5449..3b1e81ac5f7 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml index 77d5680b976..c7d92c42801 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml @@ -7,7 +7,7 @@ hapi-fhir ca.uhn.hapi.fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index 5113c4e1226..9c9cae70ed0 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-fhir ca.uhn.hapi.fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index bdeab65c74e..2396b7ac555 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 5b8d1998e69..6f34a0c26b9 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index f8853ec0a25..ca9dfd726ae 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index 68597d71563..afa13074307 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index b224d8e826e..a403a414a33 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT hapi-fhir-spring-boot-samples diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index 1c090a45924..d3a8ef9d189 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index d051b6e02cf..e1183efa6ff 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index e0143bd2d04..2cca18a9e71 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-jobs/pom.xml b/hapi-fhir-storage-batch2-jobs/pom.xml index 45617d0688e..be9bcd61988 100644 --- a/hapi-fhir-storage-batch2-jobs/pom.xml +++ b/hapi-fhir-storage-batch2-jobs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index bc3ec9ef3b4..4548753fd31 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/config/Batch2JobRegisterer.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/config/Batch2JobRegisterer.java index 67d0cba7009..009db34d98e 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/config/Batch2JobRegisterer.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/config/Batch2JobRegisterer.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.batch2.config; import ca.uhn.fhir.IHapiBootOrder; import ca.uhn.fhir.batch2.coordinator.JobDefinitionRegistry; import ca.uhn.fhir.batch2.model.JobDefinition; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/BaseDataSink.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/BaseDataSink.java index 6a5dd2a1d0b..58caedace2f 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/BaseDataSink.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/BaseDataSink.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.batch2.coordinator; import ca.uhn.fhir.batch2.api.IJobDataSink; import ca.uhn.fhir.batch2.model.JobDefinitionStep; import ca.uhn.fhir.batch2.model.JobWorkCursor; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/FinalStepDataSink.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/FinalStepDataSink.java index d7e3296e9ef..07fcb3d497c 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/FinalStepDataSink.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/FinalStepDataSink.java @@ -25,7 +25,7 @@ import ca.uhn.fhir.batch2.api.VoidModel; import ca.uhn.fhir.batch2.model.JobWorkCursor; import ca.uhn.fhir.batch2.model.WorkChunkData; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobCoordinatorImpl.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobCoordinatorImpl.java index 3a5302ec603..53d4888fd1e 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobCoordinatorImpl.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobCoordinatorImpl.java @@ -33,7 +33,7 @@ import ca.uhn.fhir.batch2.model.JobWorkNotification; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.models.JobInstanceFetchRequest; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDataSink.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDataSink.java index 4e8712971a8..43877e4613c 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDataSink.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDataSink.java @@ -28,7 +28,7 @@ import ca.uhn.fhir.batch2.model.JobWorkCursor; import ca.uhn.fhir.batch2.model.JobWorkNotification; import ca.uhn.fhir.batch2.model.WorkChunkData; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import ca.uhn.fhir.util.JsonUtil; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDefinitionRegistry.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDefinitionRegistry.java index 4b361372f08..32eeff03d0f 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDefinitionRegistry.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobDefinitionRegistry.java @@ -26,7 +26,7 @@ import ca.uhn.fhir.batch2.model.JobDefinitionStep; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import com.google.common.collect.ImmutableSortedMap; import org.apache.commons.lang3.Validate; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java index 1b13b210251..1a3b5c4b040 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/JobStepExecutor.java @@ -28,7 +28,7 @@ import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobWorkCursor; import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.batch2.progress.JobInstanceStatusUpdater; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSink.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSink.java index ff51755fa8e..0c8e790182e 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSink.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSink.java @@ -27,7 +27,7 @@ import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobWorkCursor; import ca.uhn.fhir.batch2.model.WorkChunkData; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import ca.uhn.fhir.util.JsonUtil; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutor.java index d297933a678..05f24de1449 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutor.java @@ -28,7 +28,7 @@ import ca.uhn.fhir.batch2.model.JobDefinitionStep; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/StepExecutor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/StepExecutor.java index e4b367ccfb6..433e2a256f3 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/StepExecutor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/StepExecutor.java @@ -29,7 +29,7 @@ import ca.uhn.fhir.batch2.api.StepExecutionDetails; import ca.uhn.fhir.batch2.model.MarkWorkChunkAsErrorRequest; import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChannelMessageHandler.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChannelMessageHandler.java index 30f0fc51281..ffaf4028e2b 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChannelMessageHandler.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChannelMessageHandler.java @@ -32,7 +32,7 @@ import ca.uhn.fhir.batch2.model.JobWorkNotificationJsonMessage; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.batch2.progress.JobInstanceStatusUpdater; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChunkProcessor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChunkProcessor.java index c06c19ca5e7..aaed2f8148d 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChunkProcessor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/WorkChunkProcessor.java @@ -32,7 +32,7 @@ import ca.uhn.fhir.batch2.model.JobDefinitionStep; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobWorkCursor; import ca.uhn.fhir.batch2.model.WorkChunk; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/GenerateRangeChunksStep.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/GenerateRangeChunksStep.java index fe3b7564785..03d6320d54e 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/GenerateRangeChunksStep.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/GenerateRangeChunksStep.java @@ -29,7 +29,7 @@ import ca.uhn.fhir.batch2.api.VoidModel; import ca.uhn.fhir.batch2.jobs.chunk.PartitionedUrlChunkRangeJson; import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrl; import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrlListJobParameters; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; import javax.annotation.Nonnull; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/PartitionedUrlListIdChunkProducer.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/PartitionedUrlListIdChunkProducer.java index 5d5fe20a5cd..588b6f760d7 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/PartitionedUrlListIdChunkProducer.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/PartitionedUrlListIdChunkProducer.java @@ -25,7 +25,7 @@ import ca.uhn.fhir.batch2.jobs.parameters.PartitionedUrl; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.pid.IResourcePidList; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; import javax.annotation.Nonnull; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java index dd37d9a166a..d0bd8bede12 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/step/ResourceIdListStep.java @@ -32,7 +32,7 @@ import ca.uhn.fhir.batch2.jobs.parameters.PartitionedJobParameters; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.pid.IResourcePidList; import ca.uhn.fhir.jpa.api.pid.TypedResourcePid; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; import javax.annotation.Nonnull; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobChunkProgressAccumulator.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobChunkProgressAccumulator.java index 8d8071489d9..f1671f3ad37 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobChunkProgressAccumulator.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobChunkProgressAccumulator.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.batch2.maintenance; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import org.apache.commons.lang3.ArrayUtils; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobInstanceProcessor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobInstanceProcessor.java index efacc0b4662..e6a2cd3e186 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobInstanceProcessor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobInstanceProcessor.java @@ -30,7 +30,7 @@ import ca.uhn.fhir.batch2.model.JobWorkNotification; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.progress.JobInstanceProgressCalculator; import ca.uhn.fhir.batch2.progress.JobInstanceStatusUpdater; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import org.apache.commons.lang3.time.DateUtils; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobMaintenanceServiceImpl.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobMaintenanceServiceImpl.java index 690ec3ca181..2c89769feef 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobMaintenanceServiceImpl.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/maintenance/JobMaintenanceServiceImpl.java @@ -28,11 +28,11 @@ import ca.uhn.fhir.batch2.coordinator.WorkChunkProcessor; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.DaoConfig; -import ca.uhn.fhir.jpa.batch.log.Logs; import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; +import ca.uhn.fhir.util.Logs; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java index 3bf894b3ad7..a5230b2ab31 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobDefinition.java @@ -27,7 +27,7 @@ import ca.uhn.fhir.batch2.api.IReductionStepWorker; import ca.uhn.fhir.batch2.api.VoidModel; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobWorkCursor.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobWorkCursor.java index 2f027c74af7..597e0d95375 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobWorkCursor.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/JobWorkCursor.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.batch2.model; import ca.uhn.fhir.batch2.api.VoidModel; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.apache.commons.lang3.Validate; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/StatusEnum.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/StatusEnum.java index 1614d910eb4..27708c6da07 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/StatusEnum.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/StatusEnum.java @@ -21,8 +21,7 @@ package ca.uhn.fhir.batch2.model; */ import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.batch.log.Logs; -import net.bytebuddy.dynamic.ClassFileLocator; +import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; import javax.annotation.Nonnull; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java index ebb9206dd23..dc4a65e0bad 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/InstanceProgress.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.batch2.progress; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.util.StopWatch; import org.apache.commons.lang3.builder.ToStringBuilder; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java index 52dc1390b34..79fdccac419 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceProgressCalculator.java @@ -25,7 +25,7 @@ import ca.uhn.fhir.batch2.maintenance.JobChunkProgressAccumulator; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import org.slf4j.Logger; import java.util.Iterator; diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceStatusUpdater.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceStatusUpdater.java index 0de8d8b47ad..b1765b048bf 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceStatusUpdater.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/progress/JobInstanceStatusUpdater.java @@ -26,7 +26,7 @@ import ca.uhn.fhir.batch2.api.JobCompletionDetails; import ca.uhn.fhir.batch2.model.JobDefinition; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.StatusEnum; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import org.slf4j.Logger; diff --git a/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java b/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java index cfebcd014cf..aee2acee622 100644 --- a/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java +++ b/hapi-fhir-storage-batch2/src/test/java/ca/uhn/fhir/batch2/coordinator/ReductionStepDataSinkTest.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.batch2.model.JobDefinition; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobWorkCursor; import ca.uhn.fhir.batch2.model.WorkChunkData; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.model.api.IModelJson; import ca.uhn.fhir.util.JsonUtil; import ch.qos.logback.classic.Level; diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index 1d4188b53a4..a68941b6eae 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index 038511d6199..2f5ab051b8c 100644 --- a/hapi-fhir-storage-mdm/pom.xml +++ b/hapi-fhir-storage-mdm/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-storage-mdm/src/main/java/ca/uhn/fhir/mdm/batch2/submit/MdmInflateAndSubmitResourcesStep.java b/hapi-fhir-storage-mdm/src/main/java/ca/uhn/fhir/mdm/batch2/submit/MdmInflateAndSubmitResourcesStep.java index 5d61ef51d77..b27c88151f3 100644 --- a/hapi-fhir-storage-mdm/src/main/java/ca/uhn/fhir/mdm/batch2/submit/MdmInflateAndSubmitResourcesStep.java +++ b/hapi-fhir-storage-mdm/src/main/java/ca/uhn/fhir/mdm/batch2/submit/MdmInflateAndSubmitResourcesStep.java @@ -30,7 +30,7 @@ import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; -import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.util.Logs; import ca.uhn.fhir.mdm.api.IMdmChannelSubmitterSvc; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index e3380df9dc6..d0d0f6064e0 100644 --- a/hapi-fhir-storage-test-utilities/pom.xml +++ b/hapi-fhir-storage-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index f15afa5c757..9fe6c7ba224 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index b2bdabe7622..f499cd35522 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 02ac70f962c..93c10d1600e 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 14fb7f0d813..1fddc0d00ef 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FhirPathDstu3.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FhirPathDstu3.java index b0c364cf1b5..78359fbf482 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FhirPathDstu3.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/fluentpath/FhirPathDstu3.java @@ -1,5 +1,6 @@ package org.hl7.fhir.dstu3.hapi.fluentpath; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; @@ -7,10 +8,14 @@ import ca.uhn.fhir.fhirpath.FhirPathExecutionException; import ca.uhn.fhir.fhirpath.IFhirPath; import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.dstu3.model.Base; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.TypeDetails; import org.hl7.fhir.dstu3.utils.FHIRPathEngine; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.instance.model.api.IBase; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; @@ -52,4 +57,46 @@ public class FhirPathDstu3 implements IFhirPath { myEngine.parse(theExpression); } + @Override + public void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext) { + myEngine.setHostServices(new FHIRPathEngine.IEvaluationContext(){ + + @Override + public Base resolveConstant(Object appContext, String name) throws PathEngineException { + return null; + } + + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + return null; + } + + @Override + public boolean log(String argument, List focus) { + return false; + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + return null; + } + + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + return null; + } + + @Override + public List executeFunction(Object appContext, String functionName, List> parameters) { + return null; + } + + @Override + public Base resolveReference(Object appContext, String theUrl) throws FHIRException { + return (Base)theEvaluationContext.resolveReference(new IdType(theUrl), null); + } + + }); + } + } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java new file mode 100644 index 00000000000..d928c863977 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifestTest.java @@ -0,0 +1,91 @@ +package ca.uhn.fhir.narrative2; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.ClasspathUtil; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.*; + +public class NarrativeTemplateManifestTest { + private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifestTest.class); + private static final FhirContext ourCtx = FhirContext.forDstu3Cached(); + + @Test + public void getTemplateByResourceName_NoProfile() throws IOException { + INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:manifest/manifest-test.properties"); + List template = manifest.getTemplateByResourceName( + ourCtx, + EnumSet.of(TemplateTypeEnum.THYMELEAF), + "Bundle", + Collections.emptyList()); + ourLog.info("Templates: {}", template); + assertEquals(3, template.size()); + assertThat(template.get(0).getTemplateText(), containsString("template3")); + assertThat(template.get(1).getTemplateText(), containsString("template2")); + assertThat(template.get(2).getTemplateText(), containsString("template1")); + } + + @Test + public void getTemplateByResourceName_ByProfile_ExactMatch() throws IOException { + INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:manifest/manifest-test.properties"); + List template = manifest.getTemplateByResourceName( + ourCtx, + EnumSet.of(TemplateTypeEnum.THYMELEAF), + "Bundle", + Lists.newArrayList("http://profile1")); + assertEquals(1, template.size()); + assertThat(template.get(0).getTemplateText(), containsString("template1")); + } + + @Test + public void getTemplateByResourceName_ByProfile_NoMatch() throws IOException { + INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:manifest/manifest-test.properties"); + List template = manifest.getTemplateByResourceName( + ourCtx, + EnumSet.of(TemplateTypeEnum.THYMELEAF), + "Bundle", + Lists.newArrayList("http://profile99")); + assertEquals(0, template.size()); + } + + @Test + public void getTemplateByResourceName_WithFallback_ByProfile_ExactMatch() throws IOException { + INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation( + "classpath:manifest/manifest2-test.properties", + "classpath:manifest/manifest-test.properties" + ); + List template = manifest.getTemplateByResourceName( + ourCtx, + EnumSet.of(TemplateTypeEnum.THYMELEAF), + "Bundle", + Lists.newArrayList("http://profile1")); + assertEquals(2, template.size()); + assertThat(template.get(0).getTemplateText(), containsString("template2-1")); + assertThat(template.get(1).getTemplateText(), containsString("template1")); + } + + + @Test + public void getTemplateByFragment() throws IOException { + INarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileContents( + ClasspathUtil.loadResource("classpath:manifest/fragment-test.properties") + ); + List template = manifest.getTemplateByFragmentName( + ourCtx, + EnumSet.of(TemplateTypeEnum.THYMELEAF), + "Foo"); + assertEquals(1, template.size()); + assertThat(template.get(0).getTemplateText(), containsString("template1")); + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGeneratorTest.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGeneratorTest.java index 21bdfa5e703..c19baf9d3c6 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGeneratorTest.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGeneratorTest.java @@ -1,25 +1,40 @@ package ca.uhn.fhir.narrative2; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; +import ca.uhn.fhir.narrative.CustomThymeleafNarrativeGenerator; import org.hamcrest.Matchers; -import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Composition; +import org.hl7.fhir.dstu3.model.DiagnosticReport; +import org.hl7.fhir.dstu3.model.InstantType; +import org.hl7.fhir.dstu3.model.Medication; +import org.hl7.fhir.dstu3.model.MedicationStatement; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Quantity; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IIdType; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ThymeleafNarrativeGeneratorTest { private static final Logger ourLog = LoggerFactory.getLogger(ThymeleafNarrativeGeneratorTest.class); - private FhirContext myCtx = FhirContext.forDstu3(); + private static final FhirContext ourCtx = FhirContext.forDstu3Cached(); @Test - public void testGenerateCompositionWithContextPath() throws IOException { + public void testGenerateCompositionWithContextPath() { DiagnosticReport dr1 = new DiagnosticReport(); dr1.setStatus(DiagnosticReport.DiagnosticReportStatus.FINAL); dr1.setIssuedElement(new InstantType("2019-01-01T12:12:12-05:00")); @@ -58,11 +73,9 @@ public class ThymeleafNarrativeGeneratorTest { ref.setReference("DiagnosticReport/1").setResource(dr1); sect.getEntry().add(ref); - NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation("classpath:narrative2/narratives.properties"); - ThymeleafNarrativeGenerator gen = new ThymeleafNarrativeGenerator(); - gen.setManifest(manifest); + CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative2/narratives.properties"); - gen.populateResourceNarrative(myCtx, composition); + gen.populateResourceNarrative(ourCtx, composition); // First narrative should be empty String narrative = composition.getSection().get(0).getText().getDiv().getValueAsString(); @@ -82,4 +95,60 @@ public class ThymeleafNarrativeGeneratorTest { assertEquals(4, manifest.getNamedTemplateCount()); } + + @Test + public void testFragment() { + CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:ca/uhn/fhir/narrative/narrative-with-fragment.properties"); + + String output = gen.generateResourceNarrative(ourCtx, new Bundle()); + ourLog.info("Output:\n{}", output); + + assertEquals(" This is some content
Fragment-1-content blah
", output); + } + + @Test + public void testFhirPathWithEvaluateSinglePrimitive() { + MedicationStatement ms = new MedicationStatement(); + ms.getMeta().addProfile("http://testFhirPathWithEvaluateSinglePrimitive"); + ms.setId("MedicationStatement/MS"); + ms.setMedication(new CodeableConcept().setText("Some Text")); + + CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath.properties"); + + String output = gen.generateResourceNarrative(ourCtx, ms); + ourLog.info("Output:\n{}", output); + + assertEquals("
Some Text
", output); + } + + @Test + public void testFhirPathWithResolve() { + Medication medication = new Medication(); + medication.setId("Medication/M"); + medication.getCode().setText("Other Med"); + + MedicationStatement ms = new MedicationStatement(); + ms.getMeta().addProfile("http://testFhirPathWithResolve"); + ms.setId("MedicationStatement/MS"); + ms.setMedication(new Reference("Medication/M")); + + CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath.properties"); + + gen.setFhirPathEvaluationContext(new IFhirPathEvaluationContext() { + @Override + public IBase resolveReference(@Nonnull IIdType theReference, @Nullable IBase theContext) { + if ("Medication/M".equals(theReference.getValue())) { + return medication; + } + throw new IllegalArgumentException(); + } + }); + + String output = gen.generateResourceNarrative(ourCtx, ms); + ourLog.info("Output:\n{}", output); + + assertEquals("
Other Med
", output); + } + + } diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/fragment-test.properties b/hapi-fhir-structures-dstu3/src/test/resources/manifest/fragment-test.properties new file mode 100644 index 00000000000..c8c5613a4fc --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/fragment-test.properties @@ -0,0 +1,9 @@ + +# With profile 1 +ips-profile1.fragmentName=Foo +ips-profile1.narrative=classpath:manifest/manifest-template1.html + +# With profile 2 +ips-profile2.fragmentName=Bar +ips-profile2.narrative=classpath:manifest/manifest-template2.html + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template1.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template1.html new file mode 100644 index 00000000000..51fb9e4f080 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template1.html @@ -0,0 +1,3 @@ + +template1 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template2.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template2.html new file mode 100644 index 00000000000..327df1a0d31 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template2.html @@ -0,0 +1,3 @@ + +template2 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template3.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template3.html new file mode 100644 index 00000000000..53d6347dcee --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-template3.html @@ -0,0 +1,3 @@ + +template3 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties new file mode 100644 index 00000000000..1188e074f61 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest-test.properties @@ -0,0 +1,14 @@ + +# With profile 1 +ips-profile1.resourceType=Bundle +ips-profile1.narrative=classpath:manifest/manifest-template1.html +ips-profile1.profile=http://profile1 + +# With profile 2 +ips-profile2.resourceType=Bundle +ips-profile2.narrative=classpath:manifest/manifest-template2.html +ips-profile2.profile=http://profile2 + +# No profile +ips-profile3.resourceType=Bundle +ips-profile3.narrative=classpath:manifest/manifest-template3.html diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-template1.html b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-template1.html new file mode 100644 index 00000000000..52a2fcb7a40 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-template1.html @@ -0,0 +1,3 @@ + +template2-1 + diff --git a/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-test.properties b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-test.properties new file mode 100644 index 00000000000..162b8f6c7ba --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/manifest/manifest2-test.properties @@ -0,0 +1,6 @@ + +# With profile 1 +ips-profile1.resourceType=Bundle +ips-profile1.narrative=classpath:manifest/manifest2-template1.html +ips-profile1.profile=http://profile1 + diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 94a22154c6a..fde9cdccbcc 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 1db0983de46..b4772cf0fe5 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FhirPathR4.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FhirPathR4.java index c44682921b6..5d45219a5a1 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FhirPathR4.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/hapi/fluentpath/FhirPathR4.java @@ -1,16 +1,22 @@ package org.hl7.fhir.r4.hapi.fluentpath; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.fhirpath.FhirPathExecutionException; import ca.uhn.fhir.fhirpath.IFhirPath; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4.model.Base; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.TypeDetails; +import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.utils.FHIRPathEngine; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; @@ -52,5 +58,56 @@ public class FhirPathR4 implements IFhirPath { myEngine.parse(theExpression); } + @Override + public void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext) { + myEngine.setHostServices(new FHIRPathEngine.IEvaluationContext(){ + + @Override + public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { + return null; + } + + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + return null; + } + + @Override + public boolean log(String argument, List focus) { + return false; + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + return null; + } + + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + return null; + } + + @Override + public List executeFunction(Object appContext, List focus, String functionName, List> parameters) { + return null; + } + + @Override + public Base resolveReference(Object appContext, String theUrl) throws FHIRException { + return (Base)theEvaluationContext.resolveReference(new IdType(theUrl), null); + } + + @Override + public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { + return false; + } + + @Override + public ValueSet resolveValueSet(Object appContext, String url) { + return null; + } + }); + } + } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/CompositionBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/CompositionBuilderTest.java new file mode 100644 index 00000000000..ffac8e20d98 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/CompositionBuilderTest.java @@ -0,0 +1,99 @@ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.InstantType; +import org.junit.jupiter.api.Test; + +import static com.helger.commons.mock.CommonsAssert.assertEquals; + +public class CompositionBuilderTest { + + private final FhirContext myCtx = FhirContext.forR4Cached(); + + @Test + public void testAddAuthor() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.addAuthor(new IdType("Organization/author0")); + builder.addAuthor(new IdType("Organization/author1")); + + Composition composition = builder.getComposition(); + assertEquals("Organization/author0", composition.getAuthor().get(0).getReference()); + assertEquals("Organization/author1", composition.getAuthor().get(1).getReference()); + } + + @Test + public void testSetStatus() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.setStatus("final"); + + Composition composition = builder.getComposition(); + assertEquals(Composition.CompositionStatus.FINAL, composition.getStatus()); + } + + @Test + public void testSetSubject() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.setSubject(new IdType("Patient/patient")); + + Composition composition = builder.getComposition(); + assertEquals("Patient/patient", composition.getSubject().getReference()); + } + + @Test + public void testAddTypeCoding() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.addTypeCoding("system0", "code0", "display0"); + builder.addTypeCoding("system1", "code1", "display1"); + + Composition composition = builder.getComposition(); + assertEquals("code0", composition.getType().getCoding().get(0).getCode()); + assertEquals("system0", composition.getType().getCoding().get(0).getSystem()); + assertEquals("display0", composition.getType().getCoding().get(0).getDisplay()); + assertEquals("code1", composition.getType().getCoding().get(1).getCode()); + assertEquals("system1", composition.getType().getCoding().get(1).getSystem()); + assertEquals("display1", composition.getType().getCoding().get(1).getDisplay()); + } + + @Test + public void testSetDate() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.setDate(new InstantType("2023-01-01T00:00:01Z")); + + Composition composition = builder.getComposition(); + assertEquals("2023-01-01T00:00:01Z", composition.getDateElement().getValueAsString()); + } + + @Test + public void testSetTitle() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.setTitle("title"); + + Composition composition = builder.getComposition(); + assertEquals("title", composition.getTitle()); + } + + @Test + public void testSetConfidentiality() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + builder.setConfidentiality(Composition.DocumentConfidentiality.L.toCode()); + + Composition composition = builder.getComposition(); + assertEquals(Composition.DocumentConfidentiality.L, composition.getConfidentiality()); + } + + + @Test + public void testAddSection() { + CompositionBuilder builder = new CompositionBuilder(myCtx); + CompositionBuilder.SectionBuilder sectionBuilder = builder.addSection(); + sectionBuilder.setTitle("title"); + + Composition composition = builder.getComposition(); + Composition.SectionComponent section = composition.getSection().get(0); + assertEquals("title", section.getTitle()); + + } + +} diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index c9c83a5c925..a1581f1624a 100644 --- a/hapi-fhir-structures-r4b/pom.xml +++ b/hapi-fhir-structures-r4b/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -53,11 +53,6 @@ We include these here to get the aggregate JavaDoc to work --> - - com.squareup.okhttp3 - okhttp - true - com.fasterxml.woodstox woodstox-core diff --git a/hapi-fhir-structures-r4b/src/main/java/org/hl7/fhir/r4b/hapi/fhirpath/FhirPathR4B.java b/hapi-fhir-structures-r4b/src/main/java/org/hl7/fhir/r4b/hapi/fhirpath/FhirPathR4B.java index 53bd7095ff2..c853fa48775 100644 --- a/hapi-fhir-structures-r4b/src/main/java/org/hl7/fhir/r4b/hapi/fhirpath/FhirPathR4B.java +++ b/hapi-fhir-structures-r4b/src/main/java/org/hl7/fhir/r4b/hapi/fhirpath/FhirPathR4B.java @@ -4,13 +4,19 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.fhirpath.FhirPathExecutionException; import ca.uhn.fhir.fhirpath.IFhirPath; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; import ca.uhn.fhir.i18n.Msg; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4b.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4b.model.Base; +import org.hl7.fhir.r4b.model.IdType; +import org.hl7.fhir.r4b.model.TypeDetails; +import org.hl7.fhir.r4b.model.ValueSet; import org.hl7.fhir.r4b.utils.FHIRPathEngine; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; @@ -52,5 +58,54 @@ public class FhirPathR4B implements IFhirPath { myEngine.parse(theExpression); } + @Override + public void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext) { + myEngine.setHostServices(new FHIRPathEngine.IEvaluationContext(){ + @Override + public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { + return null; + } + + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + return null; + } + + @Override + public boolean log(String argument, List focus) { + return false; + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + return null; + } + + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + return null; + } + + @Override + public List executeFunction(Object appContext, List focus, String functionName, List> parameters) { + return null; + } + + @Override + public Base resolveReference(Object appContext, String theUrl, Base refContext) throws FHIRException { + return (Base)theEvaluationContext.resolveReference(new IdType(theUrl), refContext); + } + + @Override + public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { + return false; + } + + @Override + public ValueSet resolveValueSet(Object appContext, String url) { + return null; + } + }); + } } diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 0fee365e3d2..8a9aab02bdb 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java index b731877f9b8..541266b5f39 100644 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java +++ b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java @@ -1,56 +1,111 @@ package org.hl7.fhir.r5.hapi.fhirpath; -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.fhirpath.FhirPathExecutionException; import ca.uhn.fhir.fhirpath.IFhirPath; +import ca.uhn.fhir.fhirpath.IFhirPathEvaluationContext; +import ca.uhn.fhir.i18n.Msg; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.IdType; +import org.hl7.fhir.r5.model.TypeDetails; +import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.utils.FHIRPathEngine; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; public class FhirPathR5 implements IFhirPath { - private FHIRPathEngine myEngine; + private FHIRPathEngine myEngine; - public FhirPathR5(FhirContext theCtx) { - IValidationSupport validationSupport = theCtx.getValidationSupport(); - myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); - } + public FhirPathR5(FhirContext theCtx) { + IValidationSupport validationSupport = theCtx.getValidationSupport(); + myEngine = new FHIRPathEngine(new HapiWorkerContext(theCtx, validationSupport)); + } - @SuppressWarnings("unchecked") - @Override - public List evaluate(IBase theInput, String thePath, Class theReturnType) { - List result; - try { - result = myEngine.evaluate((Base) theInput, thePath); - } catch (FHIRException e) { - throw new FhirPathExecutionException(Msg.code(198) + e); - } + @SuppressWarnings("unchecked") + @Override + public List evaluate(IBase theInput, String thePath, Class theReturnType) { + List result; + try { + result = myEngine.evaluate((Base) theInput, thePath); + } catch (FHIRException e) { + throw new FhirPathExecutionException(Msg.code(198) + e); + } - for (Base next : result) { - if (!theReturnType.isAssignableFrom(next.getClass())) { - throw new FhirPathExecutionException(Msg.code(199) + "FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); - } - } + for (Base next : result) { + if (!theReturnType.isAssignableFrom(next.getClass())) { + throw new FhirPathExecutionException(Msg.code(199) + "FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); + } + } - return (List) result; - } + return (List) result; + } - @Override - public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { - return evaluate(theInput, thePath, theReturnType).stream().findFirst(); - } + @Override + public Optional evaluateFirst(IBase theInput, String thePath, Class theReturnType) { + return evaluate(theInput, thePath, theReturnType).stream().findFirst(); + } @Override public void parse(String theExpression) { myEngine.parse(theExpression); } + @Override + public void setEvaluationContext(@Nonnull IFhirPathEvaluationContext theEvaluationContext) { + myEngine.setHostServices(new FHIRPathEngine.IEvaluationContext() { + @Override + public List resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException { + return null; + } + + @Override + public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { + return null; + } + + @Override + public boolean log(String argument, List focus) { + return false; + } + + @Override + public FunctionDetails resolveFunction(String functionName) { + return null; + } + + @Override + public TypeDetails checkFunction(Object appContext, String functionName, List parameters) throws PathEngineException { + return null; + } + + @Override + public List executeFunction(Object appContext, List focus, String functionName, List> parameters) { + return null; + } + + @Override + public Base resolveReference(Object appContext, String theUrl, Base refContext) throws FHIRException { + return (Base) theEvaluationContext.resolveReference(new IdType(theUrl), refContext); + } + + @Override + public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException { + return false; + } + + @Override + public ValueSet resolveValueSet(Object appContext, String url) { + return null; + } + }); + } } diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 100230b37a1..bcc95a17687 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java index 3966a2f48bb..06a8f6801b2 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java @@ -37,8 +37,12 @@ public class HtmlUtil { private HtmlUtil() { } - public static HtmlPage parseAsHtml(String theRespString, URL theUrl) throws IOException { - StringWebResponse response = new StringWebResponse(theRespString, theUrl); + public static HtmlPage parseAsHtml(String theHtml) throws IOException { + return parseAsHtml(theHtml, new URL("http://foo")); + } + + public static HtmlPage parseAsHtml(String theHtml, URL theUrl) throws IOException { + StringWebResponse response = new StringWebResponse(theHtml, theUrl); WebClient client = new WebClient(BrowserVersion.BEST_SUPPORTED, false, null, -1); client.getOptions().setCssEnabled(false); client.getOptions().setJavaScriptEnabled(false); diff --git a/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-child.html b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-child.html new file mode 100644 index 00000000000..8b60dcd679d --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-child.html @@ -0,0 +1,13 @@ + + + +
+ Fragment-1-content + [[${param}]] +
+ +
+ Fragment-2-content +
+ + diff --git a/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-parent.html b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-parent.html new file mode 100644 index 00000000000..6855bd4857a --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment-parent.html @@ -0,0 +1,7 @@ + + + This is some content + +
+ + diff --git a/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment.properties b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment.properties new file mode 100644 index 00000000000..136bd283980 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narrative-with-fragment.properties @@ -0,0 +1,9 @@ + +# Resource template +bundle.resourceType = Bundle +bundle.style = thymeleaf +bundle.narrative = classpath:ca/uhn/fhir/narrative/narrative-with-fragment-parent.html + +fragment1.fragmentName = MyFragment +fragment1.style = thymeleaf +fragment1.narrative = classpath:ca/uhn/fhir/narrative/narrative-with-fragment-child.html diff --git a/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-resolve.html b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-resolve.html new file mode 100644 index 00000000000..4565b8d0bd1 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-resolve.html @@ -0,0 +1,3 @@ +
+ [[${#fhirpath.evaluateFirst(resource, 'MedicationStatement.medication.resolve().code.text')}]] +
diff --git a/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html new file mode 100644 index 00000000000..a233d8cb458 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html @@ -0,0 +1,3 @@ +
+ [[${#fhirpath.evaluateFirst(resource, 'MedicationStatement.medication.text')}]] +
diff --git a/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath.properties b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath.properties new file mode 100644 index 00000000000..69f162f5770 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/resources/ca/uhn/fhir/narrative/narratives-with-fhirpath.properties @@ -0,0 +1,16 @@ + + +################################################ +# Resources +################################################ + +testFhirPathWithEvaluateSinglePrimitive.resourceType = MedicationStatement +testFhirPathWithEvaluateSinglePrimitive.style = thymeleaf +testFhirPathWithEvaluateSinglePrimitive.profile = http://testFhirPathWithEvaluateSinglePrimitive +testFhirPathWithEvaluateSinglePrimitive.narrative = classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-single-primitive.html + +testFhirPathWithResolve.resourceType = MedicationStatement +testFhirPathWithResolve.style = thymeleaf +testFhirPathWithResolve.profile = http://testFhirPathWithResolve +testFhirPathWithResolve.narrative = classpath:ca/uhn/fhir/narrative/narratives-with-fhirpath-evaluate-resolve.html + diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 5cd4a53514b..2f799e30114 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index d5955377df3..4002e0aa0c2 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index 3c39e5d7097..6638efd4da4 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 3045397a24a..bd5c035c785 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index 0aefb312d9b..c944be1eeab 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 5ab967b8d86..d754c6cbb1a 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 26d8e8ddd15..719e6b0bf6d 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 4209ef59617..034d54b7be1 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index c93493b22e0..ace9225d9fc 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 0b647b02458..584cfb3f017 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io @@ -105,6 +105,7 @@ hapi-fhir-jaxrsserver-base hapi-fhir-jpaserver-base hapi-fhir-sql-migrate + hapi-fhir-jpaserver-ips hapi-fhir-jpaserver-mdm hapi-fhir-testpage-overlay hapi-fhir-jpaserver-uhnfhirtest @@ -117,7 +118,7 @@ tests/hapi-fhir-base-test-mindeps-server hapi-fhir-spring-boot hapi-fhir-jacoco - + @@ -2131,7 +2132,7 @@ ca.uhn.hapi.fhir hapi-fhir-checkstyle - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT @@ -2503,6 +2504,9 @@ + + + diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 5945725f74a..bfd09431ee1 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 8ca23e0f212..037ed097a34 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index c019d3dab10..275585c438c 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.3.12-SNAPSHOT + 6.3.13-SNAPSHOT ../../pom.xml